checkout402_
Guides

c402 — the seller CLI#

Run your checkouts from a terminal. Four ways to sell exist and they all hit the same API: the web page at /create, the REST API, the MCP tools, and this.

c402 sells. pay402 buys (pip install "pay402[evm]").

A buyer never needs c402 — paying takes no account, and a buy tool that opened with "not signed in" would contradict the one thing that makes this interesting. The other direction was pure friction, so c402 pay exists and simply calls pay402; it works signed out and takes the same flags.

Written 2026-07-28. Source: clients/c402.py.


Install#

There is nothing to install. It is one stdlib-only file:

python clients/c402.py --help

Put it on your PATH if you want the short name:

ln -s "$PWD/clients/c402.py" /usr/local/bin/c402 && chmod +x /usr/local/bin/c402

The single optional dependency is eth-account, needed only for login --wallet. Every other command runs on a bare Python install — an agent that has to resolve a dependency tree before it can create a checkout will not create the checkout.


Sign in#

Three ways in. Pick by what you are.

An agent, with a wallet — no email, no browser#

c402 login --wallet $AGENT_PRIVATE_KEY

Signs a challenge, proves control of the address, and mints an automation key in one round trip. The signing wallet is registered as your verified payout address. This is the fast path and the one to reach for in automation.

Use --wallet - to be prompted instead of putting a key in your shell history.

A human, with an email#

c402 login --email you@company.com

A CLI has no inbox, so this sends the link and stops. Open it, then c402 keys create and paste the result:

c402 login --key c402_live_…

CI, with a key already in the environment#

export CHECKOUT402_API_KEY=c402_live_…
c402 whoami          # no login step at all

Where the key lives#

~/.checkout402/config.json, mode 0600. It can create live checkouts, so it is never world-readable and the CLI never prints it back in full.

c402 logout forgets it locally. That is not the same as revoking it — if a key may have leaked, c402 keys revoke <id> is the one that matters.

Precedence: --key › CHECKOUT402_API_KEY › the config file. Same shape for the API base: --base › CHECKOUT402_BASE_URL › config › https://api.checkout402.com.


Profile#

c402 whoami
c402 profile show
c402 profile set --name "Klaus Kopper" \
                 --bio "Swiss company data, priced per call." \
                 --website-url https://checkout402.com

Editable: --name, --bio, --website-url, --avatar-url, --slug, --profile-type (individual | team | company).

Pass an empty string to clear a field. Omitting it leaves it alone.

--slug is your public handle. Changing it breaks URLs anyone has already saved, so claim a good one early. A taken handle is a clean 409.

verified is not settable — it is a manual review, never self-serve.


Checkouts#

Create#

c402 checkouts create \
  --title "Swiss company financials" \
  --description "Latest filed financials for a Swiss registered company." \
  --price 0.10 \
  --pay-to 0xYourPayoutWallet \
  --payload '{"company_name": "Example AG", "revenue": 12500000}'

Prints the URL. Share it: a human gets a pay page, an agent gets a 402. Same URL.

Fulfilment — what the buyer receives after settlement:

--fulfillment What happens Needs
inline --payload returned verbatim --payload
relay your API is called, response forwarded --origin-url
mcp one tool on your MCP server is called --origin-url, optionally --mcp-tool
webhook receipt only, you're notified async nothing beyond title, price and payout wallet

--fulfillment has no default. The mode is inferred from what you send — --payload is inline, --origin-url is relay, --mcp-tool makes it MCP. The one case that must be stated is selling a whole MCP server (--fulfillment mcp with no --mcp-tool), because an --origin-url on its own is indistinguishable from a relay.

A webhook endpoint is not a prerequisite for webhook fulfilment

webhook needs nothing registered before you can create and sell one. It is the mode where checkout402 has no goods to hand over, so the buyer gets a receipt and you fulfil in your own time — that is all the word describes.

Registered webhook endpoints are a separate, orthogonal feature: a settled payment fires checkout.paid in all five modes, not just this one. So an endpoint is neither required for webhook fulfilment nor specific to it. If you want to be notified of sales, register one — for inline too.

Offer card — optional, and the difference between an agent buying and an agent moving on. It answers what do I provide, what will I get, is it suitable before any money moves:

c402 checkouts create --title "…" --price 0.10 --pay-to 0x… \
  --input-schema  '{"company_uid": "string"}' \
  --output-schema '{"revenue": "number", "employees": "number"}' \
  --example-input '{"company_uid": "CHE-123.456.789"}' \
  --freshness "updated daily" \
  --coverage "all 26 cantons" \
  --source "Zefix commercial register" \
  --origin-url https://api.yourco.com/v1/financials \
  --fulfillment relay

Any JSON flag accepts @file.json — real payloads and schemas outgrow a shell argument:

c402 checkouts create --payload @goods.json --output-schema @schema.json …

Only publish claims you can stand behind. freshness, coverage and source are commercial assertions a buyer will rely on. If an agent is filling these in on your behalf, it should be proposing them, not deciding them.

List, show, update#

c402 checkouts list
c402 checkouts show chk_abc123
c402 checkouts update chk_abc123 --coverage "all 26 cantons" --freshness "hourly"

update changes --title, --description, and the offer-card fields.

It deliberately cannot change --price or --pay-to: those alter what a buyer may already have been quoted, so they need a decision, not a flag. Create a new checkout instead.

An unknown field is a 422, not a silent no-op — the CLI will not tell you a change landed when it didn't.

Test, then go live#

A checkout an agent makes for you is a test checkout — it settles without touching a chain, so nothing it creates starts out able to take real money. Arming is its own command:

c402 checkouts go-live chk_abc123

That arms chk_abc123 in place — same id, same URL. Nothing new is minted: the link you have already shared is the one that now takes real money. Running it twice changes nothing the second time.

Rehearsal does not go away when you arm. Every checkout carries a test face at /c/chk_abc123/test — same product, simulated settlement, forever, and it can never be armed. Point your scripts there; share the live URL. c402 checkouts list prints the test URL under each row.

Retire one#

c402 checkouts delete chk_abc123

Retires it. This is soft: the row survives, so every receipt that points at it still resolves and your payout history is untouched. Anyone opening the link gets a clear "no longer available".

c402 checkouts restore chk_abc123

Brings it back. The status is recomputed rather than remembered — a one-shot that had already been paid returns as paid, not open, so restoring can never re-open something for a second payment.

What you still owe#

c402 owed-calls

Unspent calls across every live package key, and what that is worth. A package is paid up front and delivered later, so revenue from one is partly a liability — cash you hold against calls you have not served yet. Retiring a checkout tells you the same number and does not block you.

Problems buyers reported#

c402 reports list
c402 reports list --checkout-id chk_abc123
c402 reports resolve <report-id> --status resolved --note "fixed the timeout"

Reports arrive with our own delivery records joined alongside, so you see what actually happened next to what the buyer says happened. You can resolve one; you cannot delete it.

What you earned#

c402 stats

Revenue, buyers and calls per offer, all time. --json for the machine-readable shape.

Asking a narrower question#

stats answers "how am I doing". report answers "how am I doing since Tuesday, on this one checkout", and draws the trend:

c402 report --days 30
c402 report --since 2026-07-01 --until 2026-08-01 --interval week
c402 report --checkout chk_abc123

The same window flags work on every command that takes one — --days, --since, --until, --checkout, --network, and --test to report on rehearsal traffic instead of live. Test and live are never mixed into one figure: --test gives you a total that is entirely test.

The ledger#

The individual payments behind those totals, newest first:

c402 payments --days 7
c402 payments --checkout chk_abc123 --limit 200
c402 payments --since 2026-07-01 --csv > july.csv

The activity log#

What happened on the account itself — every state-changing action, newest first, each row naming the credential that did it (session, key:XRY3TQNC, oauth:<client>, wallet):

c402 events
c402 events --action checkout. --limit 200

--action filters by prefix, so checkout. is every checkout lifecycle event and key. is every mint and revoke. Append-only and kept indefinitely; secrets never appear in it (key prefixes only, hashed request fingerprints, never a payload). Details in the records guide.

Buying one#

Mostly to rehearse your own checkout before you share it — point at its /test face, which settles exactly like the live one and moves no money:

c402 pay https://api.checkout402.com/c/chk_abc123/test --quote
c402 pay https://api.checkout402.com/c/chk_abc123/test --key $PAY402_KEY --max-usd 1.00

Buyer inputs ride --param (repeatable), and a POST/PUT/PATCH origin's JSON body rides --body — both are signed with the payment, so one payment buys one exact query:

c402 pay https://api.checkout402.com/c/chk_abc123/test --test-payer 0xME \
  --param lang=de --body '{"query": "{ company(uid: \"CHE-1\") { revenue } }"}'

--max-usd is required to actually pay and nothing is signed above it — an unbounded paid call inside a loop is the failure worth guarding. --key is the wallet that pays, not your API key, and defaults to $PAY402_KEY so it stays out of shell history. Works on Base and Solana; the chain is picked from what the checkout offers and what you have a library for.

--csv writes to stdout so it pipes into a spreadsheet or your own books. The buyer column is the same subject that appears on the receipt, so a row here matches a support conversation without anyone handling a wallet address.


MCP servers#

Selling tools on your own MCP server starts with asking it what it has:

c402 mcp tools https://mcp.yourco.com/mcp
c402 mcp tools https://mcp.yourco.com/mcp --auth-type bearer --auth-value "$MCP_KEY"
c402 mcp tools https://mcp.yourco.com/mcp --origin-key 3f1c…

Prints every tool with its description, then the exact checkouts create line to sell one. Nothing is stored — this is not a catalogue. It exists so the name you price is one your server actually advertises; a name typed from memory creates a checkout that takes the money and then fails on a tool the server never had.

Two shapes, and the flags decide which:

# One price for the whole server. The buyer names the tool at call time,
# by sending tool=<name> with the payment.
c402 checkouts create --title "Company data tools" --price 0.10 --pay-to 0x… \
  --fulfillment mcp --origin-url https://mcp.yourco.com/mcp

# One checkout, one tool. Any other tool is refused.
c402 checkouts create --title "Company search" --price 0.10 --pay-to 0x… \
  --origin-url https://mcp.yourco.com/mcp --mcp-tool search_companies

Add --calls 500 to either — a repeat tool call costs you something, so a package here is real. The pin survives that path: a package key is a bearer token with unsigned parameters, so --mcp-tool is enforced on every call rather than by the signature.

--origin-method does not apply; MCP over Streamable HTTP is JSON-RPC over POST and the stored method is always POST. Upstream credentials work as they do for relay, with --origin-auth-type and --origin-auth-value.


Keys#

c402 keys create --name ci --mode test    # test is the default
c402 keys list
c402 keys revoke <key-id>

A key is shown once. Modes are real: a c402_test_… key drives every endpoint but cannot move real money, and pairing a live key with --test is a 400 rather than a surprise.

New keys default to test on purpose. A key minted by a one-liner should not be able to spend.

Upstream credentials#

The key your own endpoint needs, stored encrypted so a relay checkout can reach it — never shown to a buyer, never returned by the API:

c402 origin-keys add --auth-type bearer --value "$UPSTREAM_KEY" --name "acme prod"
c402 origin-keys list
c402 origin-keys rotate <key-id> --value "$NEW_KEY"
c402 origin-keys revoke <key-id>

list shows checkouts_using per key, and rotate replaces the value everywhere at once — which is the point of storing it once instead of pasting it into each checkout. revoke warns if checkouts still depend on it.

Webhooks#

c402 webhooks add https://yourco.com/hooks/c402
c402 webhooks list

add returns the signing secret once — every checkout.paid delivery is HMAC-signed with it, so verify the signature before trusting the body. list shows failure counts per endpoint.


For agents#

Every command takes --json and prints the raw API object:

c402 checkouts list --json | jq -r '.checkouts[] | select(.status=="open") | .url'

Exit codes: 0 success, 1 a stated error (printed to stderr, no traceback), 2 bad arguments, 130 interrupted.

Bulk creation is a shell loop, no bulk endpoint required:

jq -c '.[]' products.json | while read -r p; do
  c402 checkouts create --json \
    --title "$(jq -r .title <<<"$p")" \
    --price "$(jq -r .price <<<"$p")" \
    --pay-to "$PAYOUT" \
    --origin-url "$(jq -r .url <<<"$p")" --fulfillment relay
done

For an agent working inside your repo, the MCP surface at /mcp is usually the better tool — it already knows your routes and schemas. See CLAUDE_CREATE.md.


Troubleshooting#

Message Meaning
not signed in No key found. c402 login … or set CHECKOUT402_API_KEY.
a live key can only create live-mode resources Drop --test, or use a test key.
cannot reach … Wrong --base, or the service is down.
--wallet needs eth-account pip install eth-account, or log in with --key.
handle '…' is taken Someone owns that slug. Pick another.
422 … extra_forbidden You passed a field that command doesn't accept.