x402: How an Agent Funds a Prize Pool

The x402 payment flow for programmatic funding in USDC on Base — the offer and settle endpoints, what the facilitator does, and the config gate behind it.

By VoltradePublished August 31, 20266 min read

Funding a prize pool programmatically is the awkward part of an autonomous launch. The default path works but it is stateful: create the competition, read a dollar amount out of the response, send an ERC-20 transfer, wait for a receipt, then call a verify endpoint with the transaction hash and hope you did not crash between step three and step five.

x402 collapses that. It is a small standard that revives HTTP's long-dormant 402 Payment Required status: the server answers with machine-readable payment requirements, the client signs an authorization, retries with the payment attached, and a facilitator verifies and settles it on-chain. Two requests instead of a five-step state machine.

Voltrade implements it for competition funding, in USDC on Base. This post covers exactly how, including the configuration gate that decides whether it is available to you at all.

Read the gate first

x402 funding is deployment-gated and it is off by default. The module is a deliberate no-op until an environment variable is set, and there are two conditions, both required:

  • X402_FACILITATOR_URL must be set — the base URL of a facilitator that will verify and settle payments (Coinbase, Cloudflare, or self-hosted).
  • NEXT_PUBLIC_BASE_PAYMENT_ADDRESS must be set — the Base payment address that becomes the payTo in the requirements. It is the same address the manual flow pays into, so funds land where prize pools are paid from either way.

If either is missing, x402 is inert. That is not a failure mode to work around; it is the intended default. Build your agent against the manual verify flow, and treat x402 as a fast path you detect at runtime rather than assume.

The detection is free, because the offer endpoint tells you:

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://voltrade.xyz/api/v1/competitions/<slug>/x402-offer"

When x402 is not configured, that returns 200 with a plain object saying so, plus the manual payment breakdown you would have used anyway:

{ "data": { "x402": false,
    "message": "x402 is not enabled on this deployment. Fund with USDC on Base and call verify-payment { txHash }.",
    "payment": { "prizePool": 500, "feePercent": 10, "totalAmount": 550 } } }

One request, and your agent knows which branch to take. Branch on the HTTP status: 402 means x402 is live, 200 means fall back.

The offer

When x402 is configured and the competition genuinely needs funding, the same endpoint returns an actual 402 with the standard body:

{
  "x402Version": 1,
  "accepts": [{
    "scheme": "exact",
    "network": "base",
    "maxAmountRequired": "550000000",
    "resource": "https://voltrade.xyz/api/v1/competitions/<slug>/x402-settle",
    "description": "Fund the prize pool for \"My Competition\"",
    "mimeType": "application/json",
    "payTo": "0x<platform base payment address>",
    "maxTimeoutSeconds": 300,
    "asset": "0x<USDC on Base>",
    "extra": { "name": "USD Coin", "version": "2" }
  }],
  "error": "Payment required (x402)."
}

The fields that matter to a client implementer:

  • scheme: "exact" — pay the stated amount, not a range.
  • maxAmountRequired is in atomic units. USDC has 6 decimals, so 550000000 is $550. Do not parse it as dollars.
  • asset and extra are the USDC contract on Base and the EIP-712 domain name and version that an EIP-3009 transfer authorization is signed against.
  • resource is the settle endpoint you will retry against. It is returned rather than assumed so a client can follow it.
  • maxTimeoutSeconds: 300 is the window the signed authorization is good for.

Three preconditions are checked before an offer is issued at all, and each returns something specific rather than a generic error:

ConditionResponse
Competition does not exist404 not_found
Already funded, or needs no funding200 with paymentRequired: false
Payment network is not Base400 unsupported — x402 funding is Base/USDC only

That last one is a real constraint. Competitions tracking Solana fund in USDC on Solana, and x402 here covers the Base rail only. Solana-tracked competitions use the manual verify path.

The settle

Your x402 client signs the authorization from the offer, base64-encodes the payment payload, and retries with it in an X-PAYMENT header:

curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-PAYMENT: <base64 payment payload>" \
  "https://voltrade.xyz/api/v1/competitions/<slug>/x402-settle"
##   -> { "data": { "settled": true, "competitionSlug": "...",
##                  "status": "PENDING_PAYMENT", "txHash": "0x...", "payer": "0x..." } }

Server-side, that one request does five things in order, and any of them can stop it:

  1. Checks the gate. No facilitator configured is a 501 x402_not_configured, with the message pointing at the manual flow.
  2. Resolves the calling wallet. No wallet session and no bound wallet on the key is a 403 wallet_not_bound.
  3. Checks the header. A missing X-PAYMENT is a 402 payment_required telling you to fetch the offer first.
  4. Checks permission on this specific competition. You must be its creator, its partner, or an admin — otherwise 403 forbidden. An agent cannot fund a stranger's competition into a state it controls. Already-verified funding is a 400 already_verified, so a duplicate settle is refused rather than double-charged.
  5. Verifies, then settles. The payload is decoded and posted to the facilitator's /verify, and only on a valid result to its /settle. A malformed base64 body, a facilitator rejection, or a settlement failure all come back as 402 settlement_failed carrying the facilitator's own reason string rather than a generic message.

On success the competition is marked funded: paymentVerified set, the settlement transaction hash and the amount recorded, and the verification timestamp stamped. From the platform's point of view it is now indistinguishable from a competition funded by hand — the same publish step follows.

What this replaces

Side by side, with the same $500 prize pool and 10% platform fee:

Manualx402
Requests to the API2 (create, verify-payment)2 (x402-offer, x402-settle)
On-chain work you writeERC-20 transfer, wait for receiptnone — the facilitator settles
State you must persistthe txHash, across a crashnone
Failure surfaceyour transfer landed but verify never ranone request either settles or does not
Availabilityalwaysonly where a facilitator is configured

The interesting row is the fourth. In the manual flow there is a window where money has moved and the platform does not know about it yet; recovering from a crash in that window means finding your own transaction hash again. The x402 flow does not have that window, because the payment and the notification are the same request.

The interesting cost is the last row. x402 is optional infrastructure. The wallet-plus-verify flow always works, on every deployment, on both payment networks. So the correct posture for an agent is: probe the offer, take the fast path if it is 402, fall back cleanly if it is 200.

Where it fits in a launch

The full creator sequence is resolve pools, create, fund, publish — covered in launch a competition from an AI agent. x402 replaces exactly one link in that chain: the "send a transfer, then verify it" pair becomes "offer, settle". Everything before and after is unchanged, including the constraint that non-admin creators must fund a pool of at least $50 plus the platform fee before anything can be published.

If you are deciding how big that pool should be in the first place, what a prize pool actually buys is the sizing argument, and how projected rewards are calculated covers what entrants will see.

For the credential that makes any of this callable, see wallet as identity for agents. The agent docs and the full API reference carry the endpoint-level detail.

agentsx402payments

Keep reading

Every trade is a competition

Join a live volume competition or PnL challenge across top venues — or launch your own in minutes.