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. Neither needs the other.

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: --keyCHECKOUT402_API_KEY › the config file. Same shape for the API base: --baseCHECKOUT402_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 (default) --payload returned verbatim --payload
relay your API is called, response forwarded --origin-url
webhook receipt only, you're notified async a registered webhook

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.


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.


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.