The CLI
Both packages install the same tx402 command with the same flags, the same --json document,
and the same exit codes. npx tx402 works with no install at all.
tx402 call <URL> [options]
--method <METHOD> HTTP method (default: GET) --body @<file> Request body, read from a file --max-spend <MONEY> Per-request cap, e.g. "0.10 USDC" --network <CAIP2> Restrict payment to one network --dry-run Parse, evaluate policy, and plan routes. Never signs. --json Emit one JSON object on stdout --timeout <MS> Paid-retry timeout in whole milliseconds -h, --help Show this message -v, --version Show version--dry-run is the one to start with
Section titled “--dry-run is the one to start with”npx tx402 call https://api.example.com/paid --max-spend "0.10 USDC" --dry-runIt runs the real decision path — decode, policy, route planning, ranking — and stops before the budget reservation. No signer is invoked and no budget is consumed, so you can run it freely.
The guarantee is structural, not a convention: on the dry-run path the signer is wrapped in a
guard that raises if it is ever reached, and the test suite asserts the signature count is zero
and that the merchant never received a PAYMENT-SIGNATURE header.
Route planning does read your payer address and balance, because a route cannot be scored without knowing whether it is fundable. “Never invokes a signer” means never produces a signature.
stdout and stderr are a contract
Section titled “stdout and stderr are a contract”stdout carries the response body — or, with --json, exactly one JSON object. Nothing else,
ever.
stderr carries everything else: warnings, the human-readable plan, and errors.
That split is what makes this work even when the call is noisy:
npx tx402 call "$URL" --max-spend "0.10 USDC" > response.jsonThe SDK itself never writes to the console at all. The CLI renders from the structured event stream, which is why a warning can never corrupt your redirected output.
--json
Section titled “--json”{ "schemaVersion": 1, "ok": true, "exitCode": 0, "dryRun": false, "inspection": { "requirementCount": 2, "headerHash": "sha256:…" }, "route": { "network": "eip155:8453", "scheme": "exact", "healthScore": 0.84, "rank": 1, "candidateCount": 2 }, "status": 200, "body": "…", "timings": { "elapsedMs": 452, "events": 9 }, "error": null}One object, on both success and failure. On failure ok is false, exitCode names the
classification, and error carries the typed error — code, message, retryability, and its
required context keys.
error never carries the underlying cause, which is where a URL with credentials or a
signer payload would live. That is enforced by the error’s own serializer, not by the CLI
remembering to strip it.
schemaVersion changes only on a breaking shape change, so a script can pin it.
Exit codes in a script
Section titled “Exit codes in a script”#!/usr/bin/env bashset -uo pipefail
npx tx402 call "$URL" --max-spend "0.10 USDC" --json > out.jsoncode=$?
case $code in 0) jq -r .body out.json ;; 3) echo "refused by local policy — this is the guardrail working" >&2; exit 0 ;; 4) echo "fund the wallet" >&2; exit 1 ;; 7) echo "network failure — safe to retry" >&2; exit 75 ;; 8) echo "AMBIGUOUS: payment may have settled. Do not retry." >&2; exit 1 ;; *) echo "failed with $code" >&2; exit 1 ;;esacExit 1 is never used by tx402 — it is reserved for the runtime crashing. Every code and every
error that maps to it is in the error reference.
The CLI accepts no flag that carries a private key, and it never will. Anything on a command
line lands in shell history, in ps output, and in CI logs.
For development only, it reads TX402_DEV_PRIVATE_KEY (EVM) and TX402_DEV_SOLANA_KEYPAIR
(Solana), and prints a warning to stderr every single time it does. The warning is not
behind a verbosity flag and does not fire only once per session: a once-per-session warning is a
warning people learn to stop seeing.
For anything beyond a low-balance test wallet, use the SDK with an external signer. See key management.
Bodies
Section titled “Bodies”--body takes @<file> only. An inline body is refused, because a body is exactly the kind of
thing that contains a token and exactly the kind of thing that ends up in ~/.bash_history.
npx tx402 call "$URL" --method POST --body @payload.json --max-spend "0.10 USDC"A missing or unreadable file is a usage error raised before any network request, so a dry run does not first cost the merchant a round trip to discover your file is not there. The underlying OS error is not forwarded — it quotes an absolute path, which ends up in CI logs more often than anyone intends.