Pay from an agent
Build an agent that buys from any Swop seller. Read the 402, sign a USDC authorization, retry, keep the receipt.
There are two ways for an agent to pay:
| You are building | Use |
|---|---|
| An agent with its own wallet (a server key, a hosted wallet) | The x402 flow on this page. Any Swop product, any x402 server |
| An assistant acting for a Swop user from their Swop wallet | The Swop MCP: swop_get_store to browse, then swop_pay_x402_link, which previews, waits for the user's yes and respects their caps |
The rest of this page covers the first case.
Spend real money carefully
This moves real USDC on Base mainnet. Use a dedicated wallet holding only what the agent may spend, show your user the product, price and seller before paying, and set your own per-purchase and daily limits. Swop can't undo an on-chain payment.
What you need
- A wallet on Base holding enough USDC for the purchase. The buyer pays no gas: the facilitator submits the transfer.
- A library that can sign EIP-712 typed data:
viemorethersin Node,eth-accountin Python.
Request the product and read the 402
const BUY_URL =
'https://apps.apiswop.co/api/v5/x402/store/travis.swop.id/buy/6a7f63002e22feb6ad48dd7c';
const first = await fetch(BUY_URL, { headers: { Accept: 'application/json' } });
if (first.status !== 402) throw new Error(`Expected 402, got ${first.status}`);
const challenge = await first.json();
// Pick the offer you can pay: USDC on Base, exact scheme.
const req = challenge.accepts.find(
(a) => a.scheme === 'exact' && a.network === 'eip155:8453',
);
const amount = req.amount ?? req.maxAmountRequired; // USDC base units (6 decimals)
console.log(`${challenge.resource.description}\nPrice: ${Number(amount) / 1e6} USDC to ${req.payTo}`);Check the price against what your user approved before going further.
Sign a transferWithAuthorization
The signature authorizes exactly amount USDC to move from your wallet to payTo, valid for a short window.
Retry with the payment header
Encode the payment as base64 JSON and send it in PAYMENT-SIGNATURE (v1 servers use X-PAYMENT; Swop accepts both).
Keep the receipt
A 200 carries orderId and the on-chain transaction. Store both.
Complete example
import { randomBytes } from 'node:crypto';
import { privateKeyToAccount } from 'viem/accounts';
const BUY_URL = process.argv[2];
const MAX_USDC = 5; // your own spending limit for this purchase
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);
// 1. Ask for the price.
const first = await fetch(BUY_URL, { headers: { Accept: 'application/json' } });
if (first.status !== 402) throw new Error(`Expected 402, got ${first.status}: ${await first.text()}`);
const challenge = await first.json();
const req = challenge.accepts.find((a) => a.scheme === 'exact' && a.network === 'eip155:8453');
if (!req) throw new Error('No USDC-on-Base offer');
const amount = BigInt(req.amount ?? req.maxAmountRequired);
if (amount > BigInt(MAX_USDC * 1e6)) throw new Error('Over budget');
// 2. Sign an EIP-3009 transferWithAuthorization for exactly that amount.
const now = Math.floor(Date.now() / 1000);
const authorization = {
from: account.address,
to: req.payTo,
value: amount.toString(),
validAfter: String(now - 60),
validBefore: String(now + (req.maxTimeoutSeconds ?? 300)),
nonce: `0x${randomBytes(32).toString('hex')}`,
};
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: BigInt(authorization.validAfter),
validBefore: BigInt(authorization.validBefore),
},
});
// 3. Retry with the payment attached.
const payment = {
x402Version: challenge.x402Version,
scheme: req.scheme,
network: req.network,
payload: { signature, authorization },
};
const paid = await fetch(BUY_URL, {
headers: {
Accept: 'application/json',
'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(payment)).toString('base64'),
},
});
// 4. Keep the receipt.
const body = await paid.json();
if (paid.status !== 200) throw new Error(`Payment failed (${paid.status}): ${body.error}`);
console.log('Order', body.data.orderId, 'tx', body.data.receipt?.transaction);AGENT_PRIVATE_KEY=0x… node buy.mjs "https://apps.apiswop.co/api/v5/x402/store/travis.swop.id/buy/6a7f63002e22feb6ad48dd7c"Why the payment body is flat
The x402 v2 specification describes a payment that carries the chosen offer as accepted. Today's deployed facilitators still parse the flat shape { x402Version, scheme, network, payload }, so that's what Swop's own payer sends and what this example sends. Copy the network value exactly as the 402 stated it (eip155:8453).
Buying physical goods or items with options
Read variants and requiredOrderFields from the store listing, ask your user for the values, and send them as base64 JSON in an X-ORDER header on both requests:
const order = { Size: 'L', name: 'Ada Lovelace', email: 'ada@example.com', line1: '1 Market St', city: 'San Francisco', postalCode: '94105', country: 'US' };
const xOrder = Buffer.from(JSON.stringify(order)).toString('base64');
// add 'X-ORDER': xOrder to the headers of the first request and the paid retryThe payment then goes to escrow. After the seller ships, your agent can confirm receipt with a signature from the same wallet to release the funds.
Troubleshooting
| Response | Likely cause |
|---|---|
402 with Payment verification failed: … | Wrong amount, to, chain id or USDC contract in the signed message, an expired validBefore, or not enough USDC in the wallet |
402 with Payment settlement failed: … | The facilitator couldn't submit the transfer. Retry with a fresh nonce |
400 with missing | Options or shipping details absent. Send X-ORDER |
200 with duplicate: true | You retried a payment that already settled. The original orderId is returned; nothing was charged twice |