Skip to content

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
Terminal window
npx tx402 call https://api.example.com/paid --max-spend "0.10 USDC" --dry-run

It 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 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:

Terminal window
npx tx402 call "$URL" --max-spend "0.10 USDC" > response.json

The 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.

{
"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.

#!/usr/bin/env bash
set -uo pipefail
npx tx402 call "$URL" --max-spend "0.10 USDC" --json > out.json
code=$?
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 ;;
esac

Exit 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.

--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.

Terminal window
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.