Skip to content
Merged
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
2,346 changes: 2,168 additions & 178 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ keyring = { version = "3", features = [
"crypto-rust",
"async-io",
] }
x402-reqwest = { version = "2.0.1", default-features = false, features = ["json"] }
mpp = { version = "0.10.4", default-features = false, features = ["client", "tempo", "middleware", "reqwest-rustls-tls"] }
# `telemetry` pulls `tracing` (the v2_eip155_upto client references it
# unconditionally, so `client` alone won't compile). The CLI already uses tracing.
x402-chain-eip155 = { version = "2.0.1", features = ["client", "telemetry"] }
alloy-signer-local = "2"
x402-types = { version = "2.0.1", default-features = false }
# The payment crates (x402-reqwest, mpp) are built on reqwest 0.13, while the
# keyed API client (src/api) stays on 0.12. Alias the 0.13 line under a second
# name so the payment adapters speak the same reqwest types those crates expose
# without forcing a crate-wide HTTP upgrade.
reqwest_payments = { package = "reqwest", version = "0.13", default-features = false, features = ["rustls", "query"] }

[dev-dependencies]
assert_cmd = "2"
Expand Down
69 changes: 53 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Trade tokens across Solana and EVM chains from your terminal. Built for both hum
## Quick Start

```bash
# Build from source
cargo install --path .
# Install (macOS / Linux) — see Installation below for Windows / from source
curl -fsSL https://raw.githubusercontent.com/0xProject/0x-cli/main/scripts/install.sh | sh

# Configure
0x config init
Expand All @@ -27,6 +27,7 @@ cargo install --path .
- **4 APIs**: EVM Swap (Allowance Holder), Gasless Swap, Solana Swap, Cross-Chain
- **21 chains**: Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, Linea, Scroll, Blast, Mantle, Berachain, Sonic, Unichain, World Chain, Abstract, Ink, Monad, HyperEVM, Solana, Tron
- **Agent-first**: Auto-detect non-TTY for JSON output, structured error codes, stable exit codes, inline `RESPONSE:` schemas in every `--help`
- **Pay-per-request (no API key)**: `--pay x402-evm` or `--pay mpp` settles ~$0.01 USDC per request via the 0x agent gateway — for autonomous agents with a funded wallet
- **Safe by default**: OS keyring for wallet secrets, transaction simulation before every execution, `--dry-run` mode, exact token approvals
- **Rich UX**: Colored tables, progress spinners, interactive confirmation, shell completions

Expand All @@ -46,10 +47,10 @@ Pin a specific version or change the install directory:

```bash
# Install a specific version
curl -fsSL https://raw.githubusercontent.com/0xProject/0x-cli/main/scripts/install.sh | ZEROX_VERSION=v0.1.0 sh
curl -fsSL https://raw.githubusercontent.com/0xProject/0x-cli/main/scripts/install.sh | ZEROEX_VERSION=v0.1.0 sh

# Install somewhere on your PATH
curl -fsSL https://raw.githubusercontent.com/0xProject/0x-cli/main/scripts/install.sh | ZEROX_BIN_DIR=/usr/local/bin sh
curl -fsSL https://raw.githubusercontent.com/0xProject/0x-cli/main/scripts/install.sh | ZEROEX_BIN_DIR=/usr/local/bin sh
```

On **Windows**, download the `.zip` for `x86_64-pc-windows-msvc` from the
Expand Down Expand Up @@ -119,10 +120,10 @@ By default, `wallet.evm` and `wallet.solana` (when given key material rather tha
| `0x config set wallet.evm <key> --plaintext` | `~/.0x-config/config.toml` |
| `0x config set wallet.solana /path/to/file.json` | `~/.0x-config/config.toml` (it's a path) |
| `0x config set wallet.solana <base58>` | OS keyring |
| `ZEROX_EVM_PRIVATE_KEY` / `ZEROX_SOLANA_KEYPAIR` env var | Read directly, never persisted |
| `ZEROEX_EVM_PRIVATE_KEY` / `ZEROEX_SOLANA_KEYPAIR` env var | Read directly, never persisted |
| `0x config set wallet.tron <hex-key>` | OS keyring |
| `0x config set wallet.tron <hex-key> --plaintext` | `~/.0x-config/config.toml` |
| `ZEROX_TRON_PRIVATE_KEY` env var | Read directly, never persisted |
| `ZEROEX_TRON_PRIVATE_KEY` env var | Read directly, never persisted |

`0x config show` reports keyring-stored wallets as `<stored in keyring>`. If the OS keyring is unavailable (e.g. headless Linux with no DBus), use `--plaintext` or the env vars.

Expand All @@ -132,13 +133,13 @@ Environment variables always take precedence over config file values.

| Variable | Description |
|----------|-------------|
| `ZEROX_API_KEY` | 0x API key |
| `ZEROX_EVM_PRIVATE_KEY` | EVM private key (hex) |
| `ZEROX_SOLANA_KEYPAIR` | Solana keypair file path or base58 |
| `ZEROX_TRON_PRIVATE_KEY` | Tron private key (hex) |
| `ZEROX_DEFAULT_CHAIN` | Default chain name or ID |
| `ZEROX_RPC_URL` | Override RPC URL for any chain |
| `ZEROX_TELEMETRY` | Set falsy (`0`/`false`/`off`) to disable usage telemetry |
| `ZEROEX_API_KEY` | 0x API key |
| `ZEROEX_EVM_PRIVATE_KEY` | EVM private key (hex) |
| `ZEROEX_SOLANA_KEYPAIR` | Solana keypair file path or base58 |
| `ZEROEX_TRON_PRIVATE_KEY` | Tron private key (hex) |
| `ZEROEX_DEFAULT_CHAIN` | Default chain name or ID |
| `ZEROEX_RPC_URL` | Override RPC URL for any chain |
| `ZEROEX_TELEMETRY` | Set falsy (`0`/`false`/`off`) to disable usage telemetry |
| `DO_NOT_TRACK` | Set to `1` to disable usage telemetry |
| `NO_COLOR` | Disable colored output |

Expand Down Expand Up @@ -277,6 +278,42 @@ Nothing to configure — the keypair lives in memory only.
0x status 0xdef456... --type cross-chain --chain base --poll --poll-interval 10
```

### Pay per request (x402 / MPP)

Instead of a `0x-api-key`, `price` and `swap` can pay **~$0.01 USDC per request**
through the 0x agent gateway — useful for an autonomous agent with a funded wallet
and no key. Add `--pay` with one of two payment rails:

| `--pay` | Rail | Wallet needs |
|---------|------|--------------|
| `x402-evm` | [x402](https://www.x402.org) over Base — signs an EIP-3009 USDC authorization (off-chain, no gas) | USDC on Base |
| `mpp` | [MPP](https://mpp.dev) over Tempo (chainId 4217) — broadcasts a USDC.e transfer | USDC.e **and** native gas on Tempo |

```bash
# Pay for a price check with x402 (EVM wallet signs; no API key needed)
0x price --chain base --sell 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
--buy 0x4200000000000000000000000000000000000006 --amount 1000000 \
--pay x402-evm --max-payment 0.05

# Pay for a swap quote via MPP/Tempo, then execute on-chain as usual
0x swap --chain base --sell 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
--buy 0x4200000000000000000000000000000000000006 --amount 1000000 \
--pay mpp --yes
```

- **EVM AllowanceHolder only.** Rejected (free, `INPUT_INVALID`) with `--gasless`,
Solana/Tron, or `--buy-amount` (exact-out).
- **`--max-payment <USD>`** (default `0.05`) caps the spend. If the gateway asks for
more, the CLI refuses **before signing/broadcasting** — `PAYMENT_EXCEEDS_LIMIT`
(exit 41), nothing spent.
- **The payment is real, non-refundable money** charged per request — including when
a later on-chain swap reverts. Don't poll `price --pay` in a loop. The settlement
(tx hash, payer, amount) is reported under `metadata.payment` in JSON output.
- `swap --pay` pays only for the **quote**; the on-chain swap still uses your wallet
and RPC. See exit codes 40–44 and the `PAYMENT_*` rows under [Error Codes](#error-codes).
- For `--pay mpp`, point the Tempo RPC wherever you like with `--tempo-rpc <url>` or
`0x config set rpc.tempo <url>` (defaults to `https://rpc.tempo.xyz`).

## AI Agent Integration

The CLI is designed as a first-class tool for AI agents and scripts.
Expand Down Expand Up @@ -350,7 +387,7 @@ On error:
"message": "No API key configured",
"category": "config",
"retryable": false,
"suggestion": "Run '0x config set api_key <your-key>' or set ZEROX_API_KEY env var"
"suggestion": "Run '0x config set api_key <your-key>' or set ZEROEX_API_KEY env var"
}
}
```
Expand Down Expand Up @@ -458,7 +495,7 @@ Every interactive prompt has a flag equivalent:
- **Redaction**: `0x config show` and `0x config get` never reveal secret material. Wallets stored in the keyring show as `<stored in keyring>`; plaintext wallets show as `***redacted***`; Solana file paths show verbatim because the path itself isn't sensitive.
- **Transaction simulation**: EVM and Solana transactions are simulated via `eth_call` or `simulate_transaction` before submission. Tron cross-chain transactions are not pre-simulated.
- **Approval strategy**: Default is `exact` (only approve the needed amount). Use `--approval unlimited` for max approval.
- **Environment variables**: Sensitive values like private keys can be set via env vars (`ZEROX_EVM_PRIVATE_KEY`, `ZEROX_SOLANA_KEYPAIR`) to avoid persisting them at all — read-once, never written to disk or keyring.
- **Environment variables**: Sensitive values like private keys can be set via env vars (`ZEROEX_EVM_PRIVATE_KEY`, `ZEROEX_SOLANA_KEYPAIR`) to avoid persisting them at all — read-once, never written to disk or keyring.

## Telemetry

Expand All @@ -485,7 +522,7 @@ The CLI sends **anonymous, opt-out** usage statistics (via Amplitude) to help us

```bash
0x config set telemetry.enabled false # persistent
export ZEROX_TELEMETRY=0 # per-shell
export ZEROEX_TELEMETRY=0 # per-shell
export DO_NOT_TRACK=1 # cross-tool standard
```

Expand Down
16 changes: 8 additions & 8 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
# checksum, and installs it to a bin directory on your PATH.
#
# Knobs (all optional, set as environment variables):
# ZEROX_VERSION version to install, e.g. v0.1.0 (default: latest release)
# ZEROX_BIN_DIR install directory (default: ~/.local/bin)
# ZEROEX_VERSION version to install, e.g. v0.1.0 (default: latest release)
# ZEROEX_BIN_DIR install directory (default: ~/.local/bin)
#
# Examples:
# curl -fsSL .../install.sh | sh
# curl -fsSL .../install.sh | ZEROX_VERSION=v0.1.0 sh
# curl -fsSL .../install.sh | ZEROX_BIN_DIR=/usr/local/bin sh
# curl -fsSL .../install.sh | ZEROEX_VERSION=v0.1.0 sh
# curl -fsSL .../install.sh | ZEROEX_BIN_DIR=/usr/local/bin sh

set -eu

Expand Down Expand Up @@ -82,7 +82,7 @@ esac
target="${cpu}-${os_name}"

# --- resolve version --------------------------------------------------------
version="${ZEROX_VERSION:-}"
version="${ZEROEX_VERSION:-}"
if [ -z "$version" ]; then
info "${DIM}Resolving latest release...${RESET}"
# The /releases/latest page 302-redirects to /releases/tag/<version>; parse
Expand All @@ -96,7 +96,7 @@ if [ -z "$version" ]; then
| awk '/^[[:space:]]*Location:/ {print $2}' | tail -n1)"
fi
version="${loc##*/}"
[ -n "$version" ] || err "could not determine the latest version; set ZEROX_VERSION=vX.Y.Z"
[ -n "$version" ] || err "could not determine the latest version; set ZEROEX_VERSION=vX.Y.Z"
fi
case "$version" in v*) ;; *) version="v$version" ;; esac

Expand Down Expand Up @@ -148,11 +148,11 @@ binpath="$tmp/${BIN}-${target}/${BIN}"
chmod +x "$binpath"

# --- choose install dir -----------------------------------------------------
bin_dir="${ZEROX_BIN_DIR:-$HOME/.local/bin}"
bin_dir="${ZEROEX_BIN_DIR:-$HOME/.local/bin}"
mkdir -p "$bin_dir" 2>/dev/null || err "cannot create install dir: $bin_dir"
if [ ! -w "$bin_dir" ]; then
err "install dir is not writable: $bin_dir
Re-run with a writable dir, e.g. ZEROX_BIN_DIR=\$HOME/.local/bin
Re-run with a writable dir, e.g. ZEROEX_BIN_DIR=\$HOME/.local/bin
or with elevated permissions for a system path."
fi

Expand Down
18 changes: 18 additions & 0 deletions skills/0x-trade/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,29 @@ On error, `status` is `"error"` and a structured `error` object replaces `data`:
| 11 | Transaction reverted on-chain | Don't retry as-is; explain to the user. |
| 12 | Pending / timed out — tx may still land | Poll with `0x status` (see references). |
| 20 | User declined the confirmation prompt | Stop; don't re-run. |
| 40 | Agent-payment challenge invalid (`--pay`) — no payable scheme offered | Nothing spent; report it. |
| 41 | Payment exceeded `--max-payment` — refused before signing | **Nothing spent.** Only raise the cap if the amount is expected. |
| 42 | Payment signing failed | Verify the payment wallet; nothing spent. |
| 43 | Payment settlement failed | **Money may have been spent** with no usable result — don't blindly retry; check the wallet. |
| 44 | Payment wallet unfunded (USDC/USDC.e or native gas) | Fund the payment wallet. |
| 25 | Preview emitted, confirmation required (`--yes` missing) | Show the quote to the user, or re-run with `--yes`. |
| 30 | Dry-run completed | Report the simulated result. |

The full error-code catalog (code → category → retryable → action) is in `references/errors.md`.

## Paying per request instead of an API key (`--pay`)

`price` and `swap` accept `--pay <x402-evm|mpp>` to pay ~$0.01 in USDC per request through the 0x agent gateway instead of using an API key — useful for an autonomous agent with a funded wallet and no key.

- **EVM AllowanceHolder only.** Rejected (`INPUT_INVALID`) with `--gasless`, Solana, or Tron.
- **`x402-evm`**: signs an EIP-3009 USDC authorization on Base; needs USDC in the EVM wallet.
- **`mpp`**: broadcasts a USDC.e transfer on Tempo (chainId 4217); needs USDC.e **and** native gas there. Override the RPC with `--tempo-rpc` / `ZEROEX_TEMPO_RPC_URL`.
- **`--max-payment <USD>`** (default `0.05`) caps the spend; the CLI refuses *before* signing/broadcasting if the gateway asks for more (`PAYMENT_EXCEEDS_LIMIT`, exit 41 — nothing spent).
- **Every paid request costs real, non-refundable money**, including when the on-chain swap later reverts. Don't poll `price --pay` in a loop. The settlement (tx hash, payer, amount) is in `metadata.payment`.
- `swap --pay` pays only for the *quote*; the on-chain swap still uses your wallet + RPC as usual.

See the `PAYMENT_*` rows in `references/errors.md` (exit 40–44) for recovery — exit 41 means nothing was spent; exit 43 means money may have moved without a usable result.

## Going deeper — read on demand

Read the matching reference only when the task needs it:
Expand Down
24 changes: 12 additions & 12 deletions skills/0x-trade/references/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ Non-interactive (agent-driven) setup:

| Var | Overrides |
|-----|-----------|
| `ZEROX_API_KEY` | `api_key` |
| `ZEROX_EVM_PRIVATE_KEY` | `wallet.evm` |
| `ZEROX_SOLANA_KEYPAIR` | `wallet.solana` (path or base58) |
| `ZEROX_TRON_PRIVATE_KEY` | `wallet.tron` |
| `ZEROX_DEFAULT_CHAIN` | `defaults.chain` |
| `ZEROX_RPC_URL` | RPC for the current command |
| `ZEROX_OUTPUT` | `-o/--output` format |
| `ZEROX_PROFILE` | Config profile to use (overrides active_profile) |
| `ZEROX_TELEMETRY` | Set falsy (`0`/`false`/`off`) to disable usage telemetry |
| `ZEROEX_API_KEY` | `api_key` |
| `ZEROEX_EVM_PRIVATE_KEY` | `wallet.evm` |
| `ZEROEX_SOLANA_KEYPAIR` | `wallet.solana` (path or base58) |
| `ZEROEX_TRON_PRIVATE_KEY` | `wallet.tron` |
| `ZEROEX_DEFAULT_CHAIN` | `defaults.chain` |
| `ZEROEX_RPC_URL` | RPC for the current command |
| `ZEROEX_OUTPUT` | `-o/--output` format |
| `ZEROEX_PROFILE` | Config profile to use (overrides active_profile) |
| `ZEROEX_TELEMETRY` | Set falsy (`0`/`false`/`off`) to disable usage telemetry |
| `DO_NOT_TRACK` | Set to `1` to disable usage telemetry (cross-tool standard) |
| `NO_COLOR` | Disables colored output |

Expand All @@ -78,14 +78,14 @@ section.
```

When a profile is active, every API command prints `Profile '<name>' → <url>` on
stderr. The banner is suppressed by --quiet. `ZEROX_PROFILE` selects a profile per-environment; `--api-key` /
`ZEROX_API_KEY` still beat the profile's key.
stderr. The banner is suppressed by --quiet. `ZEROEX_PROFILE` selects a profile per-environment; `--api-key` /
`ZEROEX_API_KEY` still beat the profile's key.

## Telemetry

The CLI sends anonymous usage stats (which command ran, exit code, duration, chain name, CLI version, OS) to help prioritize work. It is **opt-out** and deliberately minimal:

- **Never sent:** token addresses, amounts, transaction/trade hashes, wallet addresses, API keys, RPC URLs, error messages, or IP.
- **Identifier:** a random `telemetry.install_id` (a UUID, not a device/hardware fingerprint), shown in `config show`.
- **Opt out** any of three ways: `0x config set telemetry.enabled false`, `ZEROX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.
- **Opt out** any of three ways: `0x config set telemetry.enabled false`, `ZEROEX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.
- Events spool to `~/.0x-config/telemetry-queue.jsonl` and flush in the background, so they never add latency to a command.
2 changes: 1 addition & 1 deletion skills/0x-trade/references/cross-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Swap a token on one chain for a token on another in a single command. Supports EVM↔EVM, EVM↔Solana, and EVM↔Tron. The CLI fetches multiple bridge quotes, executes the origin-chain transaction, and can track the bridge until funds land on the destination.

**Tron:** Tron addresses are base58check (`T…`). A Tron wallet (`wallet.tron` / `ZEROX_TRON_PRIVATE_KEY`) is required to bridge from or to Tron. Tron is bridging-only — it is not available in `swap`, `price`, or `gasless`.
**Tron:** Tron addresses are base58check (`T…`). A Tron wallet (`wallet.tron` / `ZEROEX_TRON_PRIVATE_KEY`) is required to bridge from or to Tron. Tron is bridging-only — it is not available in `swap`, `price`, or `gasless`.

## Quote + execute

Expand Down
7 changes: 6 additions & 1 deletion skills/0x-trade/references/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Every error carries: `code` (stable), `category`, `retryable`, `message`, and us
|--------------|----------|-----:|:---------:|----------|
| `CONFIG_NOT_FOUND` | config | 3 | no | Run `0x config init`. |
| `CONFIG_INVALID` | config | 3 | no | Inspect `~/.0x-config/config.toml`; fix or re-init. |
| `API_KEY_MISSING` | config | 5 | no | `0x config set api_key <key>` or `ZEROX_API_KEY`. |
| `API_KEY_MISSING` | config | 5 | no | `0x config set api_key <key>` or `ZEROEX_API_KEY`. |
| `WALLET_NOT_FOUND` | config | 3 | no | `0x config set wallet.evm <key>` / `wallet.solana <path>`. |
| `WALLET_INVALID` | config | 3 | no | The stored key/keypair doesn't parse — re-set it. |
| `KEYRING_UNAVAILABLE` | config | 3 | no | No OS keyring (headless Linux) — re-set the secret with `--plaintext` or use env vars. |
Expand All @@ -32,6 +32,11 @@ Every error carries: `code` (stable), `category`, `retryable`, `message`, and us
| `BRIDGE_FAILED` | bridge | 1 | no | Check `data.failure_reason` via `0x status`. |
| `BRIDGE_TIMEOUT` | bridge | 12 | yes | Bridge in flight — keep polling `0x status --type cross-chain`. |
| `USER_CANCELLED` | input | 20 | no | The user said no. Stop. |
| `PAYMENT_CHALLENGE_INVALID` | payment | 40 | no | The agent gateway's 402 offered no scheme the CLI can pay (`--pay`). Nothing spent — report it. |
| `PAYMENT_EXCEEDS_LIMIT` | payment | 41 | no | Required payment exceeded `--max-payment`; the CLI refused to sign. **Nothing spent.** Raise the cap only if the amount is expected. |
| `PAYMENT_SIGNING_FAILED` | payment | 42 | no | Signing the x402 payment authorization failed — verify the configured payment wallet. Nothing spent. |
| `PAYMENT_SETTLEMENT_FAILED` | payment | 43 | no | Payment submitted but rejected/failed on settlement. **Money may have been spent** without a usable response — do not blindly retry; check the wallet. |
| `PAYMENT_WALLET_UNFUNDED` | payment | 44 | no | Payment wallet lacks USDC/USDC.e (or native gas for an MPP push). Fund it. |

## SIMULATION_FAILED (exit 10) — the special case

Expand Down
2 changes: 1 addition & 1 deletion skills/0x-trade/references/solana.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Read-only — no keypair needed, no keychain prompt.
--yes -o json-envelope
```

Requires a Solana keypair: `0x config set wallet.solana <path-or-base58>` or `ZEROX_SOLANA_KEYPAIR`. The transaction is simulated before submission; simulation failures exit 10 with the first simulation logs in `error.details.simulation_logs`.
Requires a Solana keypair: `0x config set wallet.solana <path-or-base58>` or `ZEROEX_SOLANA_KEYPAIR`. The transaction is simulated before submission; simulation failures exit 10 with the first simulation logs in `error.details.simulation_logs`.

## Flag differences vs EVM (important)

Expand Down
Loading
Loading