x402-sdk
x402 clients for AI agents: read the 402, pay, retry. One wire contract, one library per language, no key ever held here.
x402 is HTTP's own payment step: ask for a resource, get 402 with the terms,
pay, ask again with the proof. It is small enough that an SDK's job is not to
hide it but to get the four details right that are easy to get wrong.
| package | install | tests |
|---|---|---|
packages/go |
go get github.com/kotoba-lang/x402-sdk/packages/go |
go test ./... — 12 |
packages/typescript — @com-kotobalabs/x402 |
npm i @com-kotobalabs/x402 |
|
packages/python — com-kotobalabs-x402 |
pip install com-kotobalabs-x402 once published |
|
packages/rust — com-kotobalabs-x402 |
cargo add com-kotobalabs-x402 once published |
cargo test — 11 |
Published under the com-kotobalabs organisation. x402-sdk, x402 and
x402-client were already taken across crates.io, npm and PyPI — checked
before publishing rather than after a rejected upload — and the reverse-DNS
form says whose it is, which is more useful than a generic name anyway. npm
carries it as a scope; PyPI and crates.io have none, so they carry it as a
prefix.
Go and Rust install by name today. Go modules are fetched from the repository, so
a tag publishes them; npm and PyPI need a token created through a web login, and saying npm i
works before anyone has pushed would be a claim about a registry rather than
about this code.
The contract both implement is spec/wire.md, transcribed from
the facilitator that actually runs rather than from a document it might have
drifted from.
The safe first call
from x402_sdk import challenge
c = challenge("https://x402.nexus/gateway/hanmoto/x402/counts")
import { challenge } from "@com-kotobalabs/x402";
const c = await challenge("https://x402.nexus/gateway/hanmoto/x402/counts");
No key, no payment, no side effect. c.accepts states every acceptable
payment completely — price, token, network, recipient, and the EIP-712 domain
to sign under.
import x402 "github.com/kotoba-lang/x402-sdk/packages/go"
c, _ := (&x402.Client{}).Challenge("https://x402.nexus/gateway/hanmoto/x402/counts")
Paying
const res = await fetchWithPayment(url, undefined, {
policy: { networks: ["base-sepolia"],
assets: ["0x036CbD53842c5426634e7929541eC2318f3dCF7e"],
maxAmount: "5000" },
signer, // yours. See below.
});
You do not need ETH. The exact scheme has the facilitator submit the
authorization and pay the gas, so a buyer holding only USDC can pay.
Four things these libraries insist on
The signer is injected (and in Rust, the HTTP transport too). These packages hold no key, sign nothing, and have no cryptography dependency. Your wallet stays wherever you put it — viem, ethers, eth-account, a hardware device, a remote signer. A payment library that wants your key is a place your key can leak from.
A policy is required. selectOffer with no policy refuses with
no-policy rather than paying. There is no default ceiling, because a default
ceiling is a limit nobody chose.
The cheapest allowed offer wins, never the first. Sellers list the same
resource on mainnet and on a testnet in their own order. Taking accepts[0]
is how a buyer pays real money to rehearse.
The EIP-712 domain comes from the offer. Base mainnet USDC calls itself
USD Coin; Base Sepolia USDC calls itself USDC. Signing under the wrong one
produces a signature that recovers a different address, and the payment is
rejected as a bad signature — which reads like your fault. These libraries
refuse an offer that carries no domain rather than guessing one, and they
refuse an unknown network rather than defaulting its chain id.
Refusals are named
Every refusal says which check failed, and all of them, not just the
first: an offer on the wrong network usually names that network's token too,
and a caller told only network-not-allowed will fix the network and be
refused again.
no-policy · scheme-not-allowed · network-not-allowed · asset-not-allowed
over-budget · unparseable-amount · not-a-challenge · no-offers
What they do not do
They do not settle, do not hold funds, and do not implement the transaction
scheme's transfer — an offer asking for it is refused by name. They are the
protocol, not the wallet.
Measured against a live facilitator
challenge: parsed | offers: 2
testnet -> base-sepolia 1000 units domain=USDC/2 chainId=84532
mainnet-only -> base 1000 units domain=USD Coin/2 chainId=8453
1-unit ceiling REFUSED asset-not-allowed+network-not-allowed+over-budget
catalog: 25 SKUs
MIT.
Release files for com-kotobalabs-x402 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| com_kotobalabs_x402-0.1.0.tar.gz | 7.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| com_kotobalabs_x402-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 15.0 kB
Release files / com_kotobalabs_x402-0.1.0.tar.gz
| Download URL | com_kotobalabs_x402-0.1.0.tar.gz |
|---|---|
| Size | 7.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0fa0436342e178b0d3fa6e42c0907495e1f23df1501bde40b0d5465b5181cf99
|
|
BLAKE2b-256 checksum How to use checksums |
d5cbe88ef216a03331f2bb6b7dbdee5a5fa8a07dddff8fd9683d26f755997321
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.9 {"installer":{"name":"uv","version":"0.9.9"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / com_kotobalabs_x402-0.1.0-py3-none-any.whl
| Download URL | com_kotobalabs_x402-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
00fc7fa392d8795364b3659e9c17dd544f70380b31f47db634324a2618c41531
|
|
BLAKE2b-256 checksum How to use checksums |
e2b3c0b380aba3ea76c008fd59a562f14165e2bdb82089115fc0aa95e079417e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.9 {"installer":{"name":"uv","version":"0.9.9"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|