Skip to content
D20DAO

Verifiable Randomness for AI Agents on Arc: An x402 Workflow

Give an agent a fixed list, a spending limit and a verifiable draw. Use D20DAO's MCP tools and x402 API on Arc, then check the result independently.

A tie-break with a public record

Suppose a dispatcher has three equally eligible agents for the next support shift. It needs to choose one, and the team wants to check that choice afterward. A model's answer does not provide the fixed input, request record and proof needed for that review.

D20DAO supplies the random draw; your application supplies the eligibility rules. Its MCP server exposes three tools: draw, get_result and setup_status. A paid draw costs 0.05 USDC and opens a randomness request on Arc. This workflow fixes the candidate order, authorizes one draw, recovers its result and checks the selected index. It needs no new smart contract.

Commit to the decision before asking

The API's chooseOne operation returns a zero-based index. Sending items adds convenient display labels, but only their count enters the onchain mapping. Those labels alone do not commit the request to a particular list or order.

Save the script below as prepare.mjs and run node prepare.mjs in an empty directory. It saves a manifest containing a unique decision ID and ordered candidates, then hashes its exact UTF-8 bytes. The 64-character SHA-256 digest fits the seed's 64-byte limit without a 0x prefix. Publish the manifest and the one-draw rule before requesting. A hash commits to the bytes; it does not hide a short, guessable list or prove eligibility.

prepare.mjs — creates local files; makes no API call
import { createHash } from "node:crypto";
import { writeFileSync } from "node:fs";

const manifest = {
  decision: "support-shift-2026-10-21",
  candidates: ["agent-a", "agent-b", "agent-c"],
};
const bytes = JSON.stringify(manifest);
const seed = createHash("sha256").update(bytes, "utf8").digest("hex");
const request = {
  operation: "chooseOne",
  population: manifest.candidates.length,
  seed,
};
writeFileSync("draw-manifest.json", bytes, { flag: "wx" });
writeFileSync("draw-request.json", JSON.stringify(request, null, 2), { flag: "wx" });
console.log(request);

Connect the tools with spending disabled

Follow Circle's setup guide to install its CLI, create or select your agent wallet and understand its funding steps. The human account holder accepts Circle's terms. D20DAO's server delegates signing to that CLI; it does not ask you to paste a private key into the MCP configuration.

Add the configuration below to a client that supports mcpServers. This article pins @d20dao/mcp 0.1.2. The server runs with access to your Circle session, so review the package before upgrading. Start with D20_DAILY_LIMIT_USDC set to 0, then call setup_status. That free tool checks the CLI, login, wallet, Gateway balance and recorded spending. With paid draws disabled, ready stays false even if the other checks pass. Paying requires a funded Gateway balance, separate from the wallet balance.

MCP client configuration — paid draws disabled
{
  "mcpServers": {
    "d20dao": {
      "command": "npx",
      "args": ["-y", "@d20dao/mcp@0.1.2"],
      "env": { "D20_DAILY_LIMIT_USDC": "0" }
    }
  }
}

Authorize the expense and the exact input

After reviewing the manifest and funding requirements, the wallet holder can set a budget and restart the MCP server. With no earlier recorded usage that day, 0.10 USDC allows at most two 0.05-USDC reservations; this decision still authorizes only one draw. Keep your client's approval for paid tools enabled and pass the contents of draw-request.json as the draw tool's arguments. Never let a disappointing outcome authorize another call.

The default cap is 1 USDC per UTC day. Processes using the same D20_MCP_HOME share its spending record, including across wallets and mainnet/testnet. Each draw reserves its cost before payment; an unknown outcome retains that reservation. This local safeguard is not a universal wallet limit: another home, computer or payment tool has separate accounting. Keep wallet controls and application authorization in place too.

Payment and randomness use different network settings

Underneath draw, the API uses x402: an unpaid POST to /v1/random returns HTTP 402 with accepted payment options. A client authorizes one option, then resends the request with that payment. Settlement precedes the onchain request. The published price is 0.05 USDC; Gateway payment amounts use six decimal places.

A supported Gateway balance on another network, such as Base, can fund a draw whose randomness request runs on Arc Mainnet, chain 5042. D20_PAY_CHAIN selects the MCP payment source; it does not move the randomness request. D20_NETWORK=testnet selects the Arc Testnet API and requires the corresponding testnet wallet setup. Check current accepted payment networks rather than assuming every chain is available. Robinhood's contract service is a separate integration.

Recover the existing draw instead of paying again

Save the tool response, paymentId, requestId, seed and transaction links with the manifest. At the HTTP layer, 200 returns a fulfilled result. A 202 response needs inspection: it can describe work in progress or refund_due. For pending work, respect retryAfterSeconds and follow statusUrl. MCP users call the free get_result tool with paymentId; that follows the paid call through a replacement request. A requestId lookup addresses that particular request.

A timeout is not evidence that payment failed. The MCP server can recover using the signed payment, and repeating that payment does not buy another outcome. Its five-minute identical-draw cache lives in one running process; restarting loses that protection. If charged is unknown, stop new draws and investigate the existing payment. If the first request expires unserved, the API allows one replacement at its cost; a further failure becomes refund_due and needs manual refund handling.

Repeat the index calculation locally

MCP returns a compact summary. For this check, fetch the full JSON from the free GET https://api.d20dao.org/v1/payments/{paymentId} endpoint, substituting your saved paymentId, and save the fulfilled response as response.json. It includes randomness and mapping, which the MCP summary omits. Run npm install @d20dao/vrf-sdk@0.6.2 in your example directory, then node verify-choice.mjs using Node.js 22.13 or newer. The script checks the commitment, mapping parameters and index against your candidate list; it makes no network request.

Use the SDK mapping, not a substitute modulo formula. Someone could fabricate both a word and a matching result, so passing this local calculation is only one part of verification.

verify-choice.mjs — offline mapping check
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { builtins, mapRandomness } from "@d20dao/vrf-sdk";

const bytes = readFileSync("draw-manifest.json");
const manifest = JSON.parse(bytes.toString("utf8"));
const request = JSON.parse(readFileSync("draw-request.json", "utf8"));
const response = JSON.parse(readFileSync("response.json", "utf8"));
const seed = createHash("sha256").update(bytes).digest("hex");
assert.equal(request.seed, seed);
assert.equal(response.seed, seed);
assert.equal(response.status, "fulfilled");
assert.equal(request.operation, "chooseOne");
assert.equal(request.population, manifest.candidates.length);
const spec = builtins.chooseOne(manifest.candidates.length);
assert.deepEqual(response.mapping, {
  ...spec, lower: String(spec.lower), upper: String(spec.upper),
});
const values = mapRandomness(response.randomness, spec);
assert.deepEqual(response.result.map(String), values.map(String));
console.log(manifest.candidates[Number(values[0])]);

Check the payment binding and the Arc proof

Open links.proof in the full HTTP result, or proof in the MCP summary, for the request's Explorer replay. For an independent verification, collect canonical chain receipts, the epoch packet, accepted proof and historical contract configuration. Arc combines a committed epoch packet with a target block after publication. replayEpochCommitment checks that packet; replayCoordinator checks request binding, the VRF, mapping and acceptance timing. Use independently trusted chain data: an API response or index row is not proof of inclusion.

Also establish that this request belongs to your payment and manifest. The initial clientSeed is keccak256(abi.encode(paymentId, seed)); the documented replacement derives a new seed from that initial value and the replacement counter 1. Check the payment authorization, request history and stored mapping. A valid proof for somebody else's request is not evidence for your decision.

Publish the evidence, then apply the choice

Keep the manifest, its exact bytes, the request arguments, payment record and verification outcome together. Publish the chosen candidate only after the checks required by your application pass. Proofs do not establish honest eligibility rules, guarantee delivery or prevent an organizer from hiding other draws. Arc's chain, beacon and upgrade assumptions still matter.

The examples here prepare and check data locally; they do not make a funded call. For a contract-controlled giveaway or NFT reveal, continue with the linked Solidity guides. Those examples use Robinhood Chain and show where an application enforces its one-request rule onchain.