Skip to content
Space Context Developers
  • 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.

7 min read

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 raise spendControls.
  • The facilitator pays the network fee: the wallet needs only USDC.
  • The paying wallet is the account. The key is shown once; rotate_key with 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:

  1. Ask. POST /v1/agent/credits without payment answers 402 with a PAYMENT-REQUIRED header: base64 of a JSON whose accepts lists one option per network, Base first. Each option names the amount in atomic USDC units (6 decimals: $1.00 is 1000000), the USDC contract or mint, the address to pay (payTo) and, on Solana, extra.feePayer, the facilitator account that pays the network fee.
  2. 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 the payTo wallet’s USDC account, which the client signs and the facilitator co-signs as fee payer.
  3. Retry. It sends the same request, with the same body, adding PAYMENT-SIGNATURE.
  4. 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-RESPONSE header 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:

Terminal
curl -s -X POST https://api.spacecontext.app/v1/agent/credits | jq .error.payment_required.accepts

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

Terminal
npm install @x402/fetch @x402/evm viem          # Base

Then wrap fetch with the wallet’s private key (0x…). This example buys five scans:

TypeScript
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)

Terminal
npm install @x402/fetch @x402/svm @solana/kit   # Solana

With the wallet’s secret key in base58 (what Phantom or Solflare export):

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

JSON
{
  "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} and Authorization: Bearer <the current key>: a new key comes back and the old one is revoked. Without a current key the request answers 403 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:

TypeScript
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:write and spaces: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 as SPACECONTEXT_API_KEY for 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.

StatusCodeWhen
400PAYMENT_INVALIDPAYMENT-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
402PAYMENT_REQUIREDNo payment yet: the options are in PAYMENT-REQUIRED
402PAYMENT_FAILEDThe 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)
403AGENT_KEY_REQUIREDrotate_key, or a key for an account that has none, without one of the account’s current keys
429RATE_LIMIT_EXCEEDEDMore than 5 requests a minute from one IP address
500INTERNAL_ERRORThe payment settled but could not be recorded: it is reconciled by hand; keep transaction
502PAYMENT_FAILEDThe settlement could not be confirmed: it may have gone through; write to support with nonce (Base) or signature (Solana)
503PAYMENTS_UNAVAILABLEThe 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.

Get a free test key

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.