- For developers
- x402
- ai agents
- payments
- usdc
x402 example: how AI agents pay for an API with USDC
A working x402 example on Base and Solana: an AI agent pays USDC and gets an API key and credit in one HTTP call. The 402 flow, spend caps, rotation, errors.
An AI agent pays for an API with x402 by calling the endpoint, receiving an HTTP 402 response that says what to pay and where, signing a USDC payment with its wallet, and sending the same request again with a PAYMENT-SIGNATURE header. Space Context uses this for POST /v1/agent/credits: one paid call on Base or Solana returns a live API key and prepaid credit, with no account, no card and no gas token to hold. Below is the complete flow, with the client code from the docs.
In short
- Endpoint:
POST https://api.spacecontext.app/v1/agent/credits, USDC on Base (eip155:8453) or Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp), from $1 to $500 per payment. - The official x402 client for TypeScript wraps
fetch. It refuses to pay more than $1 at once unless you raisespendControls. - The facilitator pays the network fee: the wallet needs only USDC.
- The paying wallet is the account. The key is shown once;
rotate_keywith the current key replaces it. - You can build and test everything else for free with a test key first.
What is x402?
HTTP has a status code for payments that has never had a defined use. RFC 9110 still says only: “The 402 (Payment Required) status code is reserved for future use.” x402 gives it a use. It describes itself as “an open, neutral standard for internet-native payments”, and the mechanism fits in one sentence from its site: “If a request arrives without payment, the server responds with HTTP 402, prompting the client to pay and retry.”
A third party, the facilitator, does the blockchain work. In the x402 docs, it verifies that a payment payload meets the server’s requirements and settles it on chain, and it “does not hold funds or act as a custodian.” Space Context implements x402 v2.
For an agent, this removes the steps that usually need a person: signing up, adding a card, copying a key into a config file.
How does the x402 payment flow work on the wire?
Four steps, as the agent payments docs describe them:
- Ask.
POST /v1/agent/creditswithout payment answers 402 with aPAYMENT-REQUIREDheader: base64 of a JSON whoseacceptslists one option per network, Base first. Each option names the amount in atomic USDC units (6 decimals: $1.00 is1000000), the USDC contract or mint, the address to pay (payTo) and, on Solana,extra.feePayer, the facilitator account that pays the network fee. - Sign. The client picks one option. On Base it signs an EIP-3009
transferWithAuthorization; on Solana a transaction that moves exactly that amount of USDC into thepayTowallet’s USDC account, which the client signs and the facilitator co-signs as fee payer. - Retry. It sends the same request, with the same body, adding
PAYMENT-SIGNATURE. - Settle. Space Context checks the payment against what it asked (network, asset, recipient, amount, signature) before anything is sent on. The facilitator verifies and settles it, and the answer carries the key, the balance and a
PAYMENT-RESPONSEheader with the transaction.
The same JSON as the header is in the body as error.payment_required, so you can inspect the options with curl:
curl -s -X POST https://api.spacecontext.app/v1/agent/credits | jq .error.payment_required.acceptsThe 402 also carries extensions.bazaar, which describes the call for x402 catalogs: an agent that browses a catalog can find this endpoint without reading any docs.
Building the rest of your integration first? A free test key runs every API flow on the demo space with no charge, so you can pay with USDC only when you go live.
x402 example on Base (TypeScript)
Install the client, the EVM scheme and viem:
npm install @x402/fetch @x402/evm viem # BaseThen wrap fetch with the wallet’s private key (0x…). This example buys five scans:
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
import { privateKeyToAccount } from 'viem/accounts';
const pay = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`)) }],
spendControls: { maxAmountPerPayment: '$5' }, // the client's own cap: $1 unless raised
});
const res = await pay('https://api.spacecontext.app/v1/agent/credits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount_cents: 500 }),
});
const credit = await res.json();
// credit.api_key: store it now, it is shown only once.x402 example on Solana (TypeScript)
npm install @x402/fetch @x402/svm @solana/kit # SolanaWith the wallet’s secret key in base58 (what Phantom or Solflare export):
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactSvmScheme, toClientSvmSigner } from '@x402/svm';
import { createKeyPairSignerFromBytes, getBase58Encoder } from '@solana/kit';
const signer = await createKeyPairSignerFromBytes(getBase58Encoder().encode(process.env.SOLANA_PRIVATE_KEY!));
const pay = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', client: new ExactSvmScheme(toClientSvmSigner(signer)) }],
spendControls: { maxAmountPerPayment: '$5' },
});
// …then the same pay('https://api.spacecontext.app/v1/agent/credits', …) call as above.Either way, the answer looks like this:
{
"object": "agent_credit",
"network": "eip155:8453",
"payer": "0x5e1f…c0de",
"transaction": "0x8a3b…41f2",
"livemode": true,
"amount_cents": 500,
"balance_cents": 500,
"api_key": "sc_live_…",
"api_key_prefix": "sc_live_ab12",
"notice": "Store api_key now: it is shown only this once. …",
"docs": "https://spacecontext.app/developers/docs/"
}Why does the x402 client stop at $1?
The x402 client has a spending cap of its own: it refuses to pay more than $1 in one payment unless you raise it with spendControls. A call with no body buys $1, one scan, and goes through as is. To buy more, raise maxAmountPerPayment and send amount_cents, as in the examples above ('$5' and 500).
Treat that cap as the agent’s card limit. Set it to what the task needs, not to the maximum the endpoint accepts ($500). The server holds its side of the deal too: the amount follows the body, so a payment signed for $5 does not buy $10.
Who pays the gas fee?
The facilitator, on both networks. The wallet needs only USDC, no ETH and no SOL.
- Base. EIP-3009 lets a token holder sign a transfer that a third party submits on chain. The agent signs a
transferWithAuthorization; the facilitator submits it and pays the gas. - Solana. The client builds a transaction whose fee payer is the facilitator account named in
extra.feePayer. The agent signs the transfer; the facilitator co-signs as fee payer and sends it.
How can a payment be the API credential?
A signature from the wallet proves control of the wallet, so the wallet can be the account: no email, no password, no console. Every payment from the same wallet tops up the same account. A Base wallet and a Solana wallet are two accounts.
A signed payment has one weakness: whoever holds it can submit it, and the request body is not part of what the wallet signs. Space Context designs around that:
- The key is shown once, in the answer of the payment that opens the account. Later payments from the wallet top it up and return only
api_key_prefix. A payment alone never hands out a key for an account that already exists. - Rotation needs the current key. Pay again with
{"rotate_key": true}andAuthorization: Bearer <the current key>: a new key comes back and the old one is revoked. Without a current key the request answers403 AGENT_KEY_REQUIRED, and nothing is charged. - The same payment twice (a retry, a relay) is recognized by its transaction: nothing is credited or created twice.
- Nothing is taken on trust. The server compares the signed payment with its own requirements (network, asset, recipient, amount, signature) before the facilitator sees it.
A rotation, with the pay function from the examples above. Like any payment without amount_cents, it also adds $1 of credit:
const res = await pay('https://api.spacecontext.app/v1/agent/credits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.SPACECONTEXT_API_KEY}`,
},
body: JSON.stringify({ rotate_key: true }),
});Lost the key? Write to support with the paying address and one of its transactions.
What does the agent get for its money?
- A live key with every scope:
spaces:read,scans:read,scans:writeandspaces:sensitive. Keep it in a secret store: whatever holds it can read the codes recorded in your spaces and spend the credit. - Credit for live scans at $1.00 each. Reading, search and answers are included, and the credit does not expire. Agent accounts have no free scan. Failed scans are refunded to the balance.
- The whole API. Use the key as
Authorization: Bearer sc_live_…, or asSPACECONTEXT_API_KEYfor the CLI and the SDK. A first scan takes three steps: create a space, upload a video, start the scan. The quickstart walks through it, and turning a video into structured JSON shows what comes back.
Paying accepts the Terms; section 30 covers payments by agents.
Which x402 errors should an agent handle?
Every answer uses the usual error envelope, and reason says why a payment was refused. Nothing is charged in any of these cases, except where noted.
| Status | Code | When |
|---|---|---|
| 400 | PAYMENT_INVALID | PAYMENT-SIGNATURE is malformed or does not match what was asked: another amount, network, asset or recipient, a Solana transaction for another fee payer, a signature that does not verify |
| 402 | PAYMENT_REQUIRED | No payment yet: the options are in PAYMENT-REQUIRED |
| 402 | PAYMENT_FAILED | The facilitator refused or could not settle it, e.g. not enough USDC in the wallet (invalid_exact_evm_insufficient_balance on Base, invalid_exact_svm_transaction_simulation_failed on Solana) |
| 403 | AGENT_KEY_REQUIRED | rotate_key, or a key for an account that has none, without one of the account’s current keys |
| 429 | RATE_LIMIT_EXCEEDED | More than 5 requests a minute from one IP address |
| 500 | INTERNAL_ERROR | The payment settled but could not be recorded: it is reconciled by hand; keep transaction |
| 502 | PAYMENT_FAILED | The settlement could not be confirmed: it may have gone through; write to support with nonce (Base) or signature (Solana) |
| 503 | PAYMENTS_UNAVAILABLE | The chosen network cannot be served right now; try again later or pay on the other one |
Two rows deserve a rule in your agent: on a 500, store transaction; on a 502, store the nonce or signature, and do not pay again until you know whether the first payment went through.
Start without paying
Space Context turns a video of a physical space into a structured Space Document that agents read through the API, the SDK, the CLI and the MCP server. An agent can buy its own access with x402. You can build the whole integration before that on a free test key, which runs every flow on the demo space with no charge and no model called.
FAQ
How does an AI agent pay for an API with x402?
It calls the endpoint, receives HTTP 402 with a PAYMENT-REQUIRED header listing what to pay and where, signs a USDC payment with its wallet, and sends the same request again with a PAYMENT-SIGNATURE header. A facilitator verifies and settles the payment, and the server answers the request.
Does the agent's wallet need ETH or SOL for gas?
Not for Space Context. The facilitator pays the network fee on both Base and Solana, so the wallet needs only USDC.
Why does my x402 client refuse to pay more than $1?
x402 clients refuse to pay more than $1 at once unless you raise their limit with spendControls, for example maxAmountPerPayment: '$5'. A call to POST /v1/agent/credits with no body buys $1, one scan, and goes through without changing the limit.
What happens if the agent loses the API key it bought?
The key is shown only once, in the answer of the payment that opens the account. To replace a key you still have, pay again with {"rotate_key": true} and the current key as a bearer token; if it is lost, write to support with the paying address and one of its transactions.
Can I test the integration without spending USDC?
Yes. A Space Context test key (sc_test_…) is free and runs every flow on the demo space with no charge and no model called. Create one in the console.