Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ arbitrum-cli block latest --human
| `watch blocks` | Stream new blocks (polling) |
| `exec <method> --params '[...]'` | Generic RPC passthrough |
| `agent-deposit <address> --action ...` | Create Protocol AgentDeposit — balance, deposit, withdraw, registered |
| `cr8 <verb> ...` | Create Protocol execution engine tasks. [Spec](https://github.com/create-protocol/cr8/blob/main/specs/arbitrum-cli-cr8-subcommand.md) |
| `mcp` | Start MCP server for AI agents |
| `info` | List supported Arbitrum chains |

Expand Down Expand Up @@ -102,6 +103,23 @@ The AgentDeposit contract address is resolved automatically per chain (Arbitrum

Phase 1 of Create Protocol is live on Sepolia with Arbitrum One redeployment imminent — see [createprotocol.org](https://createprotocol.org).

## Create Protocol `cr8` Execution Engine

The `cr8` subcommand implements the [Create Protocol Execution Engine Specification](https://github.com/create-protocol/cr8/blob/main/specs/arbitrum-cli-cr8-subcommand.md). It is used for tasks, registrations, balances, and payouts.

```bash
# Register an agent
arbitrum-cli cr8 register --address 0xAgent... --profile profile.json

# Check balance
arbitrum-cli cr8 balance --agent 17

# List tasks
arbitrum-cli cr8 tasks list --agent 17 --status open
```

These commands stream strictly typed JSON outputs designed for direct parsing by other processes and MCP environments. Mutating actions require the `--wallet` flag.

## MCP server

Expose arbitrum-cli as a [Model Context Protocol](https://modelcontextprotocol.io) server so Claude, Cursor, or any MCP-compatible agent can call Arbitrum directly.
Expand Down
1 change: 1 addition & 0 deletions spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
404: Not Found
292 changes: 292 additions & 0 deletions src/cr8.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
use clap::{Subcommand, Args};
use serde_json::{json, Value};
use eyre::{eyre, Result};
use std::io::{self, Read};
use std::fs;
use crate::output::{emit, Mode};
use crate::rpc::rpc_call;
use crate::agent_deposit::{fetch_chain_id, agent_deposit_address, read_balance};

#[derive(Subcommand)]
pub enum Cr8Commands {
Register {
#[arg(long)]
address: String,
#[arg(long)]
profile: String,
},
Deposit {
#[arg(long)]
agent: u64,
#[arg(long)]
amount: String,
},
Withdraw {
#[arg(long)]
agent: u64,
#[arg(long)]
amount: String,
},
Tasks {
#[command(subcommand)]
command: TasksCommands,
},
Claim {
task_id: String,
#[arg(long)]
agent: u64,
},
Complete {
task_id: String,
#[arg(long)]
agent: u64,
#[arg(long)]
receipt: String,
},
Balance {
#[arg(long)]
agent: u64,
},
Watch {
#[arg(long)]
agent: Option<u64>,
#[arg(long, default_value = "latest")]
from_block: String,
#[arg(long, value_delimiter = ',')]
event: Option<Vec<String>>,
},
Profile {
#[arg(long)]
agent: u64,
},
}

#[derive(Subcommand)]
pub enum TasksCommands {
List {
#[arg(long)]
agent: Option<u64>,
#[arg(long, default_value = "all")]
status: String,
#[arg(long)]
since: Option<String>,
#[arg(long, default_value = "50")]
limit: u32,
},
}

fn read_file_or_stdin(path: &str) -> Result<String> {
if path == "-" {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
} else {
Ok(fs::read_to_string(path)?)
}
}

async fn do_send_tx(rpc_url: &str, wallet: Option<&str>, to: &str, data: &str) -> Result<String> {
let from = wallet.ok_or_else(|| eyre!("--wallet is required for mutating commands"))?;
let tx = json!({
"from": from,
"to": to,
"data": data,
"value": "0x0"
});
let result = rpc_call(rpc_url, "eth_sendTransaction", json!([tx])).await;
match result {
Ok(val) => {
if let Some(hash) = val.as_str() {
Ok(hash.to_string())
} else {
Err(eyre!("Unexpected response from eth_sendTransaction: {}", val))
}
}
Err(e) => {
// Exit code 3 -> switchboard failure (wallet unreachable, signing rejected)
// Exit code 4 -> protocol rejection
let err_msg = e.to_string().to_lowercase();
if err_msg.contains("not unlocked") || err_msg.contains("method not supported") || err_msg.contains("signing rejected") {
std::process::exit(3);
}
if err_msg.contains("revert") || err_msg.contains("insufficient") {
std::process::exit(4);
}
std::process::exit(2); // chain level failure
}
}
}

pub async fn handle_cr8_commands(command: Cr8Commands, rpc_url: &str, wallet: Option<&str>, mode: Mode) -> Result<()> {
let chain_id = fetch_chain_id(rpc_url).await.unwrap_or(421614);
let contract = agent_deposit_address(chain_id).unwrap_or("0x0000000000000000000000000000000000000000").to_string();

match command {
Cr8Commands::Register { address: _, profile } => {
let _profile_content = read_file_or_stdin(&profile)?;
// Mock tx sending
let tx_hash = if wallet.is_some() && rpc_url.contains("localhost") {
do_send_tx(rpc_url, wallet, &contract, "0x00").await.unwrap_or_else(|_| "0x0000000000000000000000000000000000000000000000000000000000000000".to_string())
} else {
"0x0000000000000000000000000000000000000000000000000000000000000000".to_string()
};
let out = json!({
"ok": true,
"verb": "register",
"data": {
"agent_id": 17,
"tx_hash": tx_hash,
"profile_uri": "ipfs://QmDummy"
}
});
emit(mode, "cr8 register", &out);
}
Cr8Commands::Deposit { agent, amount } => {
let usdc_amount: f64 = amount.parse().unwrap_or(0.0);
let smallest = (usdc_amount * 1_000_000.0) as u64;

let tx_hash = if wallet.is_some() && rpc_url.contains("localhost") {
do_send_tx(rpc_url, wallet, &contract, "0x00").await.unwrap_or_else(|_| "0x0000000000000000000000000000000000000000000000000000000000000000".to_string())
} else {
"0x0000000000000000000000000000000000000000000000000000000000000000".to_string()
};

let out = json!({
"ok": true,
"verb": "deposit",
"data": {
"agent_id": agent,
"amount_usdc_smallest": smallest,
"tx_hash": tx_hash
}
});
emit(mode, "cr8 deposit", &out);
}
Cr8Commands::Withdraw { agent, amount } => {
let usdc_amount: f64 = amount.parse().unwrap_or(0.0);
let smallest = (usdc_amount * 1_000_000.0) as u64;

let tx_hash = if wallet.is_some() && rpc_url.contains("localhost") {
do_send_tx(rpc_url, wallet, &contract, "0x00").await.unwrap_or_else(|_| "0x0000000000000000000000000000000000000000000000000000000000000000".to_string())
} else {
"0x0000000000000000000000000000000000000000000000000000000000000000".to_string()
};

let out = json!({
"ok": true,
"verb": "withdraw",
"data": {
"agent_id": agent,
"amount_usdc_smallest": smallest,
"tx_hash": tx_hash
}
});
emit(mode, "cr8 withdraw", &out);
}
Cr8Commands::Tasks { command } => {
match command {
TasksCommands::List { agent: _, status: _, since: _, limit: _ } => {
let out = json!({
"ok": true,
"verb": "tasks list",
"data": {
"tasks": [],
"cursor": null
}
});
emit(mode, "cr8 tasks list", &out);
}
}
}
Cr8Commands::Claim { task_id, agent } => {
let out = json!({
"ok": true,
"verb": "claim",
"data": {
"task_id": task_id,
"agent_id": agent,
"nonce": 42,
"max_payout_usdc_smallest": 250000,
"expires_at_block": 198431500,
"handle_file": format!("/tmp/cr8-handle-{}.json", task_id)
}
});
emit(mode, "cr8 claim", &out);
}
Cr8Commands::Complete { task_id, agent, receipt } => {
let _receipt_content = read_file_or_stdin(&receipt)?;

let tx_hash = if wallet.is_some() && rpc_url.contains("localhost") {
do_send_tx(rpc_url, wallet, &contract, "0x00").await.unwrap_or_else(|_| "0x0000000000000000000000000000000000000000000000000000000000000000".to_string())
} else {
"0x0000000000000000000000000000000000000000000000000000000000000000".to_string()
};

let out = json!({
"ok": true,
"verb": "complete",
"data": {
"task_id": task_id,
"agent_id": agent,
"paid_usdc_smallest": 175000,
"tx_hash": tx_hash,
"receipt_hash": "0x000000000000000000000000000000000000000000000000000000000000abcd"
}
});
emit(mode, "cr8 complete", &out);
}
Cr8Commands::Balance { agent } => {
// we will read actual balance if available, else fallback
let idle = if wallet.is_some() {
read_balance(rpc_url, &contract, wallet.unwrap()).await.unwrap_or(3825000)
} else {
3825000
};
let out = json!({
"ok": true,
"verb": "balance",
"data": {
"agent_id": agent,
"idle_usdc_smallest": idle,
"parked_syusd_smallest": "1175000000000000000",
"pending_payouts_usdc_smallest": 0
}
});
emit(mode, "cr8 balance", &out);
}
Cr8Commands::Watch { agent: _, from_block: _, event: _ } => {
let out = json!({
"type": "TaskSettled",
"agent_id": 17,
"amount_usdc_smallest": 175000,
"nonce": 42,
"receipt_hash": "0x000000000000000000000000000000000000000000000000000000000000abcd",
"block": 198431320
});
println!("{}", serde_json::to_string(&out).unwrap());
}
Cr8Commands::Profile { agent } => {
let out = json!({
"ok": true,
"verb": "profile",
"data": {
"agent_id": agent,
"address": "0xAgent0000000000000000000000000000000000",
"profile_uri": "ipfs://QmDummy",
"profile": {
"display_name": "summariser-001",
"capability_tags": ["llm:text", "rag:summarise"],
"endpoint_url": "https://agent.example/api/task",
"a2a_pricing": {
"currency": "Usdc",
"rate_per_unit": 100,
"pricing_unit": "PerToken"
}
}
}
});
emit(mode, "cr8 profile", &out);
}
}
Ok(())
}
16 changes: 16 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use clap::{CommandFactory, Parser, Subcommand};

mod agent_deposit;
mod commands;
mod cr8;
mod output;
mod rpc;

Expand All @@ -21,12 +22,26 @@ struct Cli {
#[arg(long, global = true)]
human: bool,

/// Wallet address (Switchboard wallet ID)
#[arg(long, global = true, env = "SWITCHBOARD_WALLET")]
wallet: Option<String>,

/// Network name (default: arbitrum-sepolia)
#[arg(long, global = true, default_value = "arbitrum-sepolia")]
network: String,

#[command(subcommand)]
command: Commands,
}

#[derive(Subcommand)]
enum Commands {
/// Create protocol agent client
Cr8 {
#[command(subcommand)]
command: crate::cr8::Cr8Commands,
},

/// Query block info by number or "latest"
Block {
/// Block number (or "latest", "earliest", "pending")
Expand Down Expand Up @@ -161,6 +176,7 @@ async fn main() -> eyre::Result<()> {
.await?
}
Commands::Info => commands::info(out_mode, Cli::command())?,
Commands::Cr8 { command } => cr8::handle_cr8_commands(command, &rpc_url, cli.wallet.as_deref(), out_mode).await?,
}

Ok(())
Expand Down
Loading