Guides
Your first x402 payment
Pay one cent for a real resource and walk away with an API key, in about fifteen minutes.
Pay GET /v0/test/x402 one cent and it hands back an API key, so the cent is not wasted. It exists
to be the first thing you pay: the whole path — wallet, signing, settlement — for the price of a
cent.
You need: a wallet with a little USDC on Base. You do not need: ETH, an account, an API key, or a facilitator of your own.
1. See the challenge#
curl -isL "https://www.roundhouse.studio/api/v0/test/x402"HTTP/1.1 402 Payment Required
content-type: application/json
{
"x402Version": 1,
"error": "payment required",
"accepts": [
{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x…",
"resource": "https://www.roundhouse.studio/api/v0/test/x402",
"maxTimeoutSeconds": 60
}
]
}Read it before you pay it, every time. maxAmountRequired is "10000" — atomic units, and USDC has
six decimals, so that is one cent. Not ten thousand dollars. This is
the most expensive misreading in x402.
2. Fund a wallet#
USDC on Base, at the address you are about to sign with. A dollar is plenty for a hundred of these.
No ETH required: the exact scheme is a signed authorization, and
the facilitator pays the gas.
3. Pay it — the easy way#
Any standard x402 client works, because this endpoint speaks the standard wire format.
import { wrapFetchWithPayment } from 'x402-fetch';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.AGENT_KEY);
const pay = wrapFetchWithPayment(fetch, account);
const response = await pay('https://www.roundhouse.studio/api/v0/test/x402');
const body = await response.json();
console.log(response.headers.get('x-payment-response')); // your receipt
console.log(body);
// {
// api_key: 'rh_live_…',
// expires_at: '2026-09-23T…',
// limits: { sql_per_minute: 240 },
// settlement: { tx_hash: '0x…', network: 'base' }
// }That is it. Skip to step 5 unless you want to see what the wrapper did.
4. Pay it — by hand#
Worth doing once. Everything the wrapper hides is here.
import { privateKeyToAccount } from 'viem/accounts';
const URL = 'https://www.roundhouse.studio/api/v0/test/x402';
const account = privateKeyToAccount(process.env.AGENT_KEY);
// (a) Take the terms from the 402 rather than assuming them.
const challenge = await fetch(URL).then((r) => r.json());
const req = challenge.accepts[0];
// (b) Build the authorization. `value` is req.maxAmountRequired, untouched.
const authorization = {
from: account.address,
to: req.payTo,
value: req.maxAmountRequired,
validAfter: '0',
validBefore: String(Math.floor(Date.now() / 1000) + 600),
nonce: `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex')}`,
};
// (c) Sign typed data over the TOKEN's domain. Never personal_sign.
const signature = await account.signTypedData({
domain: {
name: req.extra?.name ?? 'USD Coin',
version: req.extra?.version ?? '2',
chainId: 8453,
verifyingContract: req.asset,
},
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
message: {
...authorization,
value: BigInt(authorization.value),
validAfter: 0n,
validBefore: BigInt(authorization.validBefore),
},
});
// (d) Base64 the envelope into X-PAYMENT and retry the original request.
const envelope = {
x402Version: challenge.x402Version,
scheme: req.scheme,
network: req.network,
payload: { authorization, signature },
};
const paid = await fetch(URL, {
headers: { 'x-payment': Buffer.from(JSON.stringify(envelope)).toString('base64') },
});
console.log(paid.status, await paid.json());Tip
If (d) comes back 402 again, decode the error body and read the stage. A
verify failure is deterministic — retrying unchanged will fail identically
forever. The five causes cover nearly every case, and
address casing is the sneakiest of them.
5. Keep the receipt#
echo "$X_PAYMENT_RESPONSE" | base64 -d | jq
# { "success": true, "txHash": "0x…", "networkId": "base", "payer": "0x…" }The transaction hash is your proof. Within a couple of minutes the same settlement appears in the index from the other side:
curl -sL "https://www.roundhouse.studio/api/v0/entities/<your-wallet>/settlements?limit=5" | jqYou have just written your first row into the public payment record. That row is what gives you a history other agents can check.
6. Spend the key#
curl -sL -X POST "https://www.roundhouse.studio/api/v0/sql" \
-H "authorization: Bearer $ROUNDHOUSE_KEY" \
-H 'content-type: application/json' \
-d '{"sql":"select count(*) as settlements_today from settlements where block_time > now() - interval '1 day'"}' | jq240 SQL queries a minute, for 30 days, versus 30 anonymous. The key is returned exactly once — only its hash is stored, so save it now.
409 key_already_issued on a retry means your payment succeeded and a key was already minted
against it. Pay again for another.
Hand it to an agent#
Make your first x402 payment and report each step.
Target: GET https://www.roundhouse.studio/api/v0/test/x402 ($0.01 USDC on Base)
1. Fetch it unpaid. Show me the decoded 402 body and state the price in dollars,
converting from atomic units explicitly so I can check your arithmetic.
2. Confirm the scheme, network and asset are ones you can pay. If not, stop.
3. Pay it. Sign EIP-3009 typed data over the token's domain — never
personal_sign, and checksum every address before hashing.
4. Show me the decoded X-PAYMENT-RESPONSE, including the transaction hash.
5. Store the returned api_key in an environment variable. Do not print it.
6. Look up the settlement at
GET /api/v0/entities/<your wallet>/settlements and confirm it is indexed.
If any step fails, name the stage and stop — do not retry a `verify` failure.Next steps#
- Anatomy of a payment — every field you just used
- Set up an agent — make this repeatable and policy-bound
- Query the index — spend the key