{
  "openapi": "3.1.0",
  "info": {
    "title": "D20DAO Verifiable Randomness API",
    "version": "0.1.0",
    "summary": "Verifiable random numbers for agents, paid per call in USDC over x402.",
    "description": "Each paid call opens one D20DAO VRF request on Arc and returns the proven result: dice, coin flips, ranges, choices, shuffles or a raw 256-bit word. The request is bound on chain to your payment and optional seed, and anyone can replay its proof.",
    "contact": {
      "name": "D20DAO",
      "email": "contact@d20dao.org",
      "url": "https://d20dao.org"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    },
    "x-guidance": "Sells one verifiable random result per call for 0.05 USDC over x402, paid through Circle Gateway on any Gateway network.\nCall it when you need randomness that no one, including this API, can choose or predict: dice, coin flips, raffles, winners, shuffles, fair ordering, random numbers in a range.\nPOST /v1/random with a JSON body. Without payment you get 402 with accepts[]; sign one option and resend with the Payment-Signature header (Circle GatewayClient.pay does both steps).\nOperations: raw {} returns one uint256 word; coinFlip {} returns [0 or 1] and outcome; dice {sides, count 1-128}; range {min, max}; chooseOne {population or items}; chooseMany {population or items, count}; shuffle {population or items}. population and items go up to 256. One call can return many values: ask for them in one request instead of paying again.\nOptional seed (at most 64 bytes) is bound with your payment into the request on chain.\nResponse 200: result, randomness, requestId, clientSeed, requestTx, fulfillmentTx and links, usually within a few seconds. Otherwise 202 with statusUrl: GET it (free) until status is fulfilled.\nThe payment is settled before the request is opened. If a request gets no proof within its 60 s deadline, we open one replacement request at our cost (clientSeed keccak256(abi.encode(ref, 1))) and statusUrl serves its result. If that fails too, status becomes refund_due and we refund manually.\nNot charged: 400 (bad input), 402 (payment refused), 409 (payment reused with a different signature, seed, operation or parameters, or earlier payments of yours still being confirmed), 429, 503 (above cost, or not selling right now).\nResending the same payment returns the same result. Never pay again to re-roll a result you dislike.\nitems are labels: only their number is committed and proven, so results are indices and picked is items[index] from each call. To commit to a list, put its hash in seed as 64 hex characters without 0x, or as base64.\nVerify without trusting us: clientSeed = keccak256(abi.encode(paymentId, seed)); the coordinator getMappedResult(requestId) equals result; replay the proof with @d20dao/vrf-sdk."
  },
  "externalDocs": {
    "description": "D20DAO documentation",
    "url": "https://d20dao.org/docs"
  },
  "servers": [
    {
      "url": "https://api.d20dao.org",
      "description": "D20DAO agent API"
    }
  ],
  "tags": [
    {
      "name": "randomness",
      "description": "Verifiable random results"
    }
  ],
  "paths": {
    "/v1/random": {
      "post": {
        "operationId": "drawRandom",
        "tags": [
          "randomness"
        ],
        "summary": "Draw verifiable randomness (0.05 USDC)",
        "description": "Settles the payment, opens one randomness request on chain and waits up to about 25 seconds for its proof. Without a Payment-Signature header it answers 402 with x402 payment requirements.",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USDC",
            "amount": "0.050000"
          },
          "protocols": [
            {
              "x402": {}
            }
          ]
        },
        "parameters": [
          {
            "name": "Payment-Signature",
            "in": "header",
            "required": false,
            "description": "Base64 x402 v2 payment payload for one of the 402 accepts[] options. Omit it to receive the 402.",
            "schema": {
              "type": "string",
              "description": "Base64-encoded JSON payment payload."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The draw to make.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RandomRequest"
              },
              "example": {
                "operation": "dice",
                "sides": 20,
                "count": 2,
                "seed": "game-42 round 3"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Paid and fulfilled. The PAYMENT-RESPONSE header carries the base64 settlement receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RandomResult"
                },
                "example": {
                  "status": "fulfilled",
                  "requestId": "1234",
                  "operation": "dice",
                  "result": [
                    7,
                    19
                  ],
                  "randomness": "0xabababababababababababababababababababababababababababababababab",
                  "clientSeed": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
                  "paymentId": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
                  "seed": "game-42 round 3",
                  "mapping": {
                    "operation": 1,
                    "lower": "0",
                    "upper": "20",
                    "count": 2,
                    "population": 0
                  },
                  "network": "arc-mainnet",
                  "chainId": 5042,
                  "coordinator": "0xd20da057469C45928912d983F45790C41e290571",
                  "consumer": "0x2B76ACaFe480A554D07291ccE75656A369707E8D",
                  "requestTx": "0x1212121212121212121212121212121212121212121212121212121212121212",
                  "fulfillmentTx": "0x3434343434343434343434343434343434343434343434343434343434343434",
                  "deadline": 1789999999,
                  "links": {
                    "result": "https://example.invalid/v1/random/1234"
                  },
                  "verify": "See verify in a live response."
                }
              }
            }
          },
          "202": {
            "description": "No result yet: the payment is being confirmed (stage settling) or the request is in progress, or refund_due. Read statusUrl (free) after retryAfterSeconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RandomResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body or malformed payment header. Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required, or the payment was refused. The PAYMENT-REQUIRED header carries the same requirements base64-encoded.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "description": "Base64-encoded x402 v2 PaymentRequired object.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequired"
                }
              }
            }
          },
          "409": {
            "description": "payment_conflict: this payment was already used with a different signature, seed, operation, or sides, count, min, max or population; items are compared by length only. payment_pending: earlier payments of yours are still being confirmed with Gateway (one whose settle answer was lost, or two it refused). Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this address. Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Not selling right now (cost above price, settlement failing or unconfirmed, relayer unavailable). Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{paymentId}": {
      "get": {
        "operationId": "getPayment",
        "tags": [
          "randomness"
        ],
        "summary": "Status of a paid call (free)",
        "description": "The state of a paid call and, once served, its result, following a replacement request. Rate limited per IP.",
        "parameters": [
          {
            "name": "paymentId",
            "in": "path",
            "required": true,
            "description": "paymentId from a paid call (also in 202 answers as statusUrl).",
            "schema": {
              "type": "string",
              "pattern": "^0x[0-9a-fA-F]{64}$",
              "description": "bytes32 hex."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call and, once fulfilled, its result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RandomResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed paymentId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No payment with this id reached this API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/random/{requestId}": {
      "get": {
        "operationId": "getRandom",
        "tags": [
          "randomness"
        ],
        "summary": "Read a result (free)",
        "description": "Reads a request this API opened, straight from the chain. Rate limited per IP.",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Request id from a paid call.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{1,78}$",
              "description": "Decimal request id."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request and, once fulfilled, its result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RandomResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No request with this id was opened by this API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "RandomRequest": {
        "type": "object",
        "description": "What to draw. Give operation plus only the fields that operation uses; other fields are rejected.",
        "required": [
          "operation"
        ],
        "additionalProperties": false,
        "properties": {
          "operation": {
            "type": "string",
            "enum": [
              "raw",
              "coinFlip",
              "dice",
              "range",
              "chooseOne",
              "chooseMany",
              "shuffle"
            ],
            "description": "raw: one 256-bit word. coinFlip: 0 (tails) or 1 (heads). dice: count rolls of 1 to sides. range: one integer from min to max. chooseOne: one index below population. chooseMany: count distinct indices. shuffle: every index below population once."
          },
          "sides": {
            "description": "dice only, required. Faces per die, at least 2.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "The value as a JSON number."
              },
              {
                "type": "string",
                "pattern": "^(0|[1-9][0-9]{0,77})$",
                "description": "The value as a decimal string, for values above 2^53."
              }
            ]
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 256,
            "description": "dice: number of rolls, 1 to 128, default 1. chooseMany: number of distinct picks, 1 to population, required."
          },
          "min": {
            "description": "range only, required. Lowest value, inclusive.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "The value as a JSON number."
              },
              {
                "type": "string",
                "pattern": "^(0|[1-9][0-9]{0,77})$",
                "description": "The value as a decimal string, for values above 2^53."
              }
            ]
          },
          "max": {
            "description": "range only, required. Highest value, inclusive; at least min and at most 2^256-1.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "The value as a JSON number."
              },
              {
                "type": "string",
                "pattern": "^(0|[1-9][0-9]{0,77})$",
                "description": "The value as a decimal string, for values above 2^53."
              }
            ]
          },
          "population": {
            "type": "integer",
            "minimum": 1,
            "maximum": 256,
            "description": "chooseOne, chooseMany, shuffle: how many items to draw from (1 to 256); results are zero-based indices. Give this or items."
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 256,
            "items": {
              "type": "string",
              "maxLength": 200,
              "description": "One item, at most 200 bytes of UTF-8."
            },
            "description": "chooseOne, chooseMany, shuffle: display labels. Only their number goes on chain (as population), so the proof covers the indices, not the labels or their order. The response adds picked. Give this or population."
          },
          "seed": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional, at most 64 bytes of UTF-8, default empty. Bound with your payment into the on-chain clientSeed; use it to commit to context such as a hash of your list, as 64 hex characters without 0x or as base64."
          },
          "network": {
            "type": "string",
            "enum": [
              "arc-mainnet"
            ],
            "description": "Chain the randomness request is opened on; each deployment serves one. Independent of the network you pay on.",
            "default": "arc-mainnet"
          }
        },
        "examples": [
          {
            "operation": "dice",
            "sides": 20,
            "count": 2,
            "seed": "game-42 round 3"
          },
          {
            "operation": "chooseMany",
            "items": [
              "ana",
              "bo",
              "cy",
              "dee"
            ],
            "count": 2
          },
          {
            "operation": "coinFlip"
          }
        ]
      },
      "RandomResult": {
        "type": "object",
        "description": "A randomness request opened for your call, and its result once fulfilled.",
        "required": [
          "status",
          "requestId",
          "network"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "fulfilled",
              "pending",
              "expired",
              "refunded",
              "rejected",
              "refund_due"
            ],
            "description": "fulfilled: result is final. pending: in progress (stage settling: the payment is still being confirmed); read statusUrl later. expired or refunded: this request got no proof in time (a paid call is then served by one replacement request, see replacedBy). rejected: the payment was not settled, nothing charged. refund_due: not served; refunded manually if you were charged."
          },
          "stage": {
            "type": "string",
            "enum": [
              "settling",
              "requesting",
              "expiring",
              "replacing"
            ],
            "description": "pending only: settling the payment, opening the request, refunding an expired request before replacing it, or opening the replacement."
          },
          "statusUrl": {
            "type": "string",
            "format": "uri",
            "description": "Free URL for this paid call, keyed by paymentId; follows a replacement."
          },
          "message": {
            "type": "string",
            "description": "What the status means for you."
          },
          "requestId": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Coordinator request id. GET /v1/random/{requestId} reads it again for free."
          },
          "operation": {
            "type": "string",
            "enum": [
              "raw",
              "coinFlip",
              "dice",
              "range",
              "chooseOne",
              "chooseMany",
              "shuffle"
            ],
            "description": "The operation drawn."
          },
          "result": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "integer",
                  "description": "A value below 2^53."
                },
                {
                  "type": "string",
                  "description": "A decimal value, for raw words and large ranges."
                }
              ],
              "description": "One mapped value."
            },
            "description": "Mapped values, identical to the coordinator getMappedResult(requestId). Choice and shuffle values are zero-based indices."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "heads",
              "tails"
            ],
            "description": "coinFlip only: 1 is heads, 0 is tails."
          },
          "picked": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "One picked item."
            },
            "description": "The labels at the result indices, from the items sent with this call. Not stored or proven: status reads return indices only."
          },
          "randomness": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "The VRF word accepted on chain for this request."
          },
          "clientSeed": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "The request clientSeed: keccak256(abi.encode(bytes32 paymentId, string seed))."
          },
          "paymentId": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "keccak256(abi.encode(uint256 chainId, address payer, bytes32 nonce)) of the x402 authorization that paid for this call."
          },
          "ref": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "keccak256(abi.encode(bytes32 paymentId, string seed)): the clientSeed of the first request for this payment."
          },
          "seed": {
            "type": "string",
            "description": "The seed sent with the call, or empty."
          },
          "replaces": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "On a replacement request: the expired request it replaces."
          },
          "replacedBy": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "On an expired request: the replacement request that serves the payment."
          },
          "replacementId": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The replacement request, when one was opened."
          },
          "settlement": {
            "type": "string",
            "description": "Circle Gateway transfer id of the payment."
          },
          "mapping": {
            "type": "object",
            "description": "The RandomnessMapping.Spec stored with the request.",
            "properties": {
              "operation": {
                "type": "integer",
                "description": "Operation code: 0 raw, 1 dice, 2 coinFlip, 3 range, 4 chooseOne, 5 chooseMany, 6 shuffle."
              },
              "lower": {
                "type": "string",
                "description": "Lower bound as a decimal string (range min)."
              },
              "upper": {
                "type": "string",
                "description": "Upper bound as a decimal string (dice sides, range max)."
              },
              "count": {
                "type": "integer",
                "description": "Values returned."
              },
              "population": {
                "type": "integer",
                "description": "Items drawn from (choices and shuffles)."
              }
            }
          },
          "network": {
            "type": "string",
            "enum": [
              "arc-mainnet",
              "arc-testnet"
            ],
            "description": "Chain the request was opened on."
          },
          "chainId": {
            "type": "integer",
            "description": "Chain id of that network."
          },
          "coordinator": {
            "type": "string",
            "pattern": "^0x[0-9a-fA-F]{40}$",
            "description": "D20DAO VRF coordinator proxy that holds the request."
          },
          "consumer": {
            "type": "string",
            "pattern": "^0x[0-9a-fA-F]{40}$",
            "description": "D20AgentRelay contract that opened the request."
          },
          "requestTx": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "Transaction that opened the request."
          },
          "fulfillmentTx": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "Transaction that verified the proof and fulfilled the request."
          },
          "deadline": {
            "type": "integer",
            "description": "Unix time after which an unfulfilled request can only be refunded."
          },
          "links": {
            "type": "object",
            "description": "Where to look.",
            "properties": {
              "result": {
                "type": "string",
                "format": "uri",
                "description": "Free URL for this request."
              },
              "status": {
                "type": "string",
                "format": "uri",
                "description": "Free URL for this paid call."
              },
              "replacement": {
                "type": "string",
                "format": "uri",
                "description": "Free URL for the replacement request."
              },
              "request": {
                "type": "string",
                "format": "uri",
                "description": "Block explorer page of the request transaction."
              },
              "fulfillment": {
                "type": "string",
                "format": "uri",
                "description": "Block explorer page of the fulfillment transaction."
              },
              "proof": {
                "type": "string",
                "format": "uri",
                "description": "D20DAO explorer page that replays the proof."
              }
            }
          },
          "payment": {
            "type": "object",
            "description": "How this call was paid.",
            "properties": {
              "network": {
                "type": "string",
                "description": "CAIP-2 network of the payment."
              },
              "payer": {
                "type": "string",
                "pattern": "^0x[0-9a-fA-F]{40}$",
                "description": "Paying wallet."
              },
              "amount": {
                "type": "string",
                "description": "Amount in USDC atomic units (6 decimals)."
              },
              "settlement": {
                "type": "string",
                "description": "Settlement reference returned by Circle Gateway."
              }
            }
          },
          "verify": {
            "type": "string",
            "description": "How to check this result without trusting this API."
          },
          "retryAfterSeconds": {
            "type": "integer",
            "description": "pending only: seconds to wait before reading statusUrl."
          }
        }
      },
      "PaymentRequired": {
        "type": "object",
        "description": "x402 v2 payment requirements, also sent base64-encoded in the PAYMENT-REQUIRED header.",
        "properties": {
          "x402Version": {
            "type": "integer",
            "description": "x402 protocol version, 2."
          },
          "error": {
            "type": "string",
            "description": "Why payment is required or was refused."
          },
          "resource": {
            "type": "object",
            "description": "The paid resource.",
            "properties": {
              "url": {
                "type": "string",
                "description": "Resource URL."
              },
              "description": {
                "type": "string",
                "description": "What the payment buys."
              },
              "mimeType": {
                "type": "string",
                "description": "Response media type."
              }
            }
          },
          "accepts": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One way to pay: a Circle Gateway batched USDC payment on one network.",
              "properties": {
                "scheme": {
                  "type": "string",
                  "description": "x402 scheme, always exact."
                },
                "network": {
                  "type": "string",
                  "description": "CAIP-2 network, for example eip155:5042002 (Arc Testnet)."
                },
                "asset": {
                  "type": "string",
                  "pattern": "^0x[0-9a-fA-F]{40}$",
                  "description": "USDC contract on that network."
                },
                "amount": {
                  "type": "string",
                  "description": "Price in USDC atomic units (6 decimals): 50000 is 0.05 USDC."
                },
                "payTo": {
                  "type": "string",
                  "pattern": "^0x[0-9a-fA-F]{40}$",
                  "description": "Seller address that receives the payment in Gateway."
                },
                "maxTimeoutSeconds": {
                  "type": "integer",
                  "description": "Validity window the authorization must cover."
                },
                "extra": {
                  "type": "object",
                  "description": "Gateway signing domain.",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "EIP-712 domain name, GatewayWalletBatched."
                    },
                    "version": {
                      "type": "string",
                      "description": "EIP-712 domain version."
                    },
                    "verifyingContract": {
                      "type": "string",
                      "pattern": "^0x[0-9a-fA-F]{40}$",
                      "description": "Gateway wallet contract used as the EIP-712 verifying contract."
                    }
                  }
                }
              }
            },
            "description": "Accepted payment options, one per Gateway network."
          },
          "extensions": {
            "type": "object",
            "description": "x402 extensions; bazaar carries the request and response schemas."
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "An error. Nothing is charged for 400, 404, 409, 429 and 503 responses.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "description": "What went wrong.",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code, such as invalid_request or payment_invalid."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation."
              },
              "field": {
                "type": "string",
                "description": "The request field at fault, when there is one."
              }
            }
          }
        }
      }
    }
  }
}
