# Build with AI Source: https://docs.prizerail.com/ai/assistants Prizerail publishes skill files for AI assistants and coding agents such as Claude Code and Cursor. Point an assistant at one, or add it to one that supports skills. ## Look up and verify draws [https://www.prizerail.com/verify/SKILL.md](https://www.prizerail.com/verify/SKILL.md) This skill teaches an assistant to look up a campaign's draws and verify one, using only the [public endpoints](/verification/public-endpoints). Ask things like: * "Has today's Acme draw happened yet?" * "Who won the latest Acme draw, and what did they get?" * "Verify the draw at [https://www.prizerail.com/campaigns/acme-daily/draws/d7k2m9q4x1wz/proof.json](https://www.prizerail.com/campaigns/acme-daily/draws/d7k2m9q4x1wz/proof.json)" The skill tells the assistant to run the [verify script](/verification/verify-a-draw) instead of computing hashes itself, since keccak256 can't be done reliably by reasoning. ## Coming next * An integration skill that reads your codebase, asks whether you want Prizerail to fetch winners or you to send them, and writes the endpoint or worker for your stack, with winner-id hashing and tests for the exact-count and no-repeats rules. * A URL checker that tests a winners URL the way Prizerail fetches it: https on a public host, no redirects, under 8 seconds, the right shape and count. These docs are also published as [llms.txt](https://docs.prizerail.com/llms.txt) and [llms-full.txt](https://docs.prizerail.com/llms-full.txt) for assistants that read documentation directly. # Authentication Source: https://docs.prizerail.com/api/authentication Every API request goes to `https://www.prizerail.com/api/v1` and carries your key as a bearer token: ```http theme={null} Authorization: Bearer pr_prod_… ``` * **Owners create keys** in the partner portal under **Team → API keys**. A key is shown once, when it's made. * **One key covers every campaign** of yours. * **Environments are separate.** Keys starting `pr_test_` only work in our test environment, and `pr_prod_` keys only in production. * **Revoking is immediate.** Every request with a revoked key fails from then on. To rotate, create the new key, deploy it, then revoke the old one. A missing, unknown or revoked key returns: ```json theme={null} 401 { "error": "unauthorized", "detail": "Unknown or revoked API key." } ``` A campaign that belongs to another account returns `404 not_found`, the same as one that doesn't exist. # Drawings Source: https://docs.prizerail.com/api/drawings ## List drawings ```http theme={null} GET /api/v1/campaigns/{slug}/drawings?status=open ``` | Query | Meaning | | - | - | | `status` | Optional. `open`, `locked`, `entropy_requested`, `settled` or `paid`. Leave it off for every status. | Open drawings come soonest first, so the first one is the next cutoff. Other statuses come newest first. Up to 50 are returned, with no paging yet. For a campaign's full history, the public [`draws.json`](/verification/public-endpoints) pages back with `before`. ```json theme={null} { "drawings": [ { "id": "d7k2m9q4x1wz", "number": null, "partnerRound": null, "drawAt": "2026-10-15T16:00:00.000Z", "cutoffAt": "2026-10-15T15:00:00.000Z", "status": "open", "winnersExpected": 3, "needsAttention": null, "winners": [], "verification": { … } } ] } ``` ## Get a drawing ```http theme={null} GET /api/v1/campaigns/{slug}/drawings/{id} ``` ```json theme={null} { "drawing": { "id": "d7k2m9q4x1wz", "number": 12, "partnerRound": null, "drawAt": "2026-10-15T16:00:00.000Z", "cutoffAt": "2026-10-15T15:00:00.000Z", "status": "paid", "winnersExpected": 3, "needsAttention": null, "winners": [ { "position": 0, "winner": "user-8231", "displayName": null, "avatarUrl": null, "randomValue": "0x9c3e…", "roll": 61204, "amountCents": 500, "heldForManualPayout": false } ], "verification": { "winnersCommitment": "0x7a1f…", "lockedAt": "2026-10-15T15:00:04.000Z", "entropyProvider": "mock", "entropyRequestId": "…", "entropy": "0x52be…", "entropyRevealedAt": "2026-10-15T16:00:41.000Z", "prizeTableId": "…" } } } ``` ## Fields | Field | Meaning | | - | - | | `number` | The drawing's place in the campaign, 1, 2, 3 with no gaps. It's given when the random value is requested, so it's `null` until then. It's also the drawing ID onchain. | | `partnerRound` | Your own number for the draw, when Prizerail reads one from your source. Shown only, never used in the draw. | | `drawAt`, `cutoffAt` | ISO 8601, UTC. | | `status` | `open`, then `locked` (list frozen, fingerprint recorded), `entropy_requested` (waiting for the random value), `settled` (prizes decided) and `paid` (sent to your wallet). | | `winnersExpected` | How many winners this drawing takes, fixed when it was created. | | `needsAttention` | A short reason when a drawing is stuck, such as no winners by the cutoff. Otherwise null. | | `winners[].randomValue`, `roll`, `amountCents` | Present once settled. Amounts are US cents. | | `winners[].heldForManualPayout` | True for a prize at or above the campaign's large-prize threshold, which Prizerail pays by hand rather than in the automatic transfer. | | `verification` | Everything needed to check the draw. See [Verify a draw](/verification/verify-a-draw). | | `verification.entropyProvider` | Where the random value came from. `pyth` (Pyth Entropy on Base) for real draws, `mock` in the test environment. | # Errors Source: https://docs.prizerail.com/api/errors Errors are JSON with an `error` code and a readable `detail`: ```json theme={null} { "error": "bad_request", "detail": "Expected exactly 3 winners, got 2." } ``` | Status | Code | Meaning | | - | - | - | | 400 | `bad_request` | The request is wrong: not JSON, the wrong number of winners, a repeated winner, a bad name or avatar, or an unknown status filter. | | 401 | `unauthorized` | No key, or an unknown or revoked one. | | 404 | `not_found` | No such campaign, drawing or guarantee under your account. | | 409 | `locked` | The cutoff has passed and this list differs from the locked one. | | 409 | `pull_campaign` | Prizerail fetches this campaign's winners from your URL. | | 409 | `campaign_ended` | The campaign has ended and takes no more winners. | | 400 or 409 | `rejected` | The guarantee pick isn't allowed. See [Guarantees](/api/guarantees). | The `detail` is written for people, so it's fine to log or show to your team. Match on `error`, which won't change. ## Retries * A `5xx` or a network error means Prizerail didn't finish the request. Retry it with a growing wait, say 10 seconds, then a minute, for up to 10 minutes. Every call is safe to repeat: sending the same winners again returns `200`, and reading never changes anything. * There's no rate limit today. If you ever get a `429`, wait for the seconds in its `Retry-After` header before trying again. Polling once a minute per campaign is plenty, since drawings change state on a 5-minute cycle. * The API is versioned by its path, `/api/v1`. Within v1, fields are added but never removed or renamed. Changes are listed in the [changelog](/changelog). # Guarantees Source: https://docs.prizerail.com/api/guarantees A campaign can guarantee that each period has at least one prize of a set size, for example \$100 every 30 days. When a period ends with no winner at or above the threshold, you pick which of that period's winners gets the guarantee. Periods run back to back from the campaign's start. Paused time doesn't count, so a week's pause pushes the period's end back a week. A period is checked once all its drawings are drawn. The guarantee is its own payout, on top of whatever the picked winner already won in the period. There's no deadline to pick, but nothing is paid until you do, and the period stays `awaiting_pick`. ## List guarantee periods ```http theme={null} GET /api/v1/campaigns/{slug}/guarantees ``` ```json theme={null} { "guarantees": [ { "id": "g3n8p2…", "period": 2, "periodStart": "2026-10-01T00:00:00.000Z", "periodEnd": "2026-10-31T00:00:00.000Z", "status": "awaiting_pick", "amountCents": 10000 } ] } ``` | `status` | Meaning | | - | - | | `not_needed` | A prize in the period already met the threshold. | | `awaiting_pick` | Waiting for you to pick a winner. | | `picked` | Picked. The payout is on its way. | | `paid` | Paid. | ## Pick a winner ```http theme={null} POST /api/v1/campaigns/{slug}/guarantees/{id}/pick Content-Type: application/json ``` ```json theme={null} { "winner": "user-4417" } ``` Returns `201 { "payoutId": "p9x…" }`. The winner must have won in that period. If they won more than once, add `"drawingId"` to say which win. A guarantee can be picked once. A second pick returns `409`, even for the same winner, so keep the `payoutId` from the first response rather than retrying blindly. If a request timed out, read the guarantee list first: `picked` means it went through. | Status | Code | When | | - | - | - | | 400 | `rejected` | That winner didn't win in this period, or won more than once and no `drawingId` was sent. | | 404 | `not_found` | No such guarantee. | | 409 | `rejected` | The guarantee was already picked, or the campaign has no payout wallet. | You can also pick in the partner portal, on the campaign's page. # Let Prizerail fetch winners Source: https://docs.prizerail.com/api/pull-winners The usual way. Publish each drawing's winners at a URL and Prizerail fetches them before the cutoff, so there's nothing to call. Set the URL in the campaign's **Winners** section in the [partner portal](/getting-started/partner-portal). Rather post the list yourself? See [Send winners](/api/send-winners). ## The URL The URL can name the drawing with placeholders, filled in for each draw: | Placeholder | Becomes | | - | - | | `{drawingId}` | The drawing's id | | `{drawAt}` | The draw time in UTC, ISO 8601, URL-encoded | | `{date}` | The draw date in the campaign's time zone, e.g. `2026-10-15` | For example `https://api.acme.com/prizerail/{drawingId}`. `{date}` only tells draws apart on daily, weekly and monthly campaigns. An hourly campaign has 24 draws on one date, so use `{drawingId}` or `{drawAt}` there. The URL must be https on a public host, with no redirects. Prizerail sends `Accept: application/json` and gives up after 8 seconds. Prizerail doesn't sign its requests yet. If your list shouldn't be readable by anyone who finds the URL, put an unguessable token in the path, such as `https://api.acme.com/prizerail/8f3a2c…/{drawingId}`. The URL is stored like a secret and never shown publicly. ## The response The same body the [send winners](/api/send-winners) endpoint takes, with the same rules: exactly the expected count, no repeats, names and avatars optional. ```json theme={null} { "winners": ["u_8f3a…", "u_27c1…", "u_b904…"] } ``` ## When it's fetched * Every 5 minutes in the 2 hours before each cutoff. The last fetch can be up to 5 minutes before the cutoff, so have the list final at least 10 minutes early. * Until your draw has happened, return `404`, an empty list, or no `winners` key. Prizerail tries again. * Nothing is stored until the full list is there. * If the list changes before the cutoff, the newer one replaces the earlier one. * At the cutoff the list locks, exactly as if it had been posted. If the URL still errors in the last 5 minutes before the cutoff, Prizerail staff are alerted. A list that never arrives leaves the drawing flagged, and you can extend its cutoff in the portal and try again. ## Confirming it arrived Read the drawing with the API. Once a fetch has succeeded, its `winners` holds your list: ```bash theme={null} curl https://www.prizerail.com/api/v1/campaigns/acme-daily/drawings/d7k2m9q4x1wz \ -H "Authorization: Bearer $PRIZERAIL_KEY" ``` An empty `winners` with `status` still `open` means nothing has been accepted yet. Once `status` is `locked`, `verification.winnersCommitment` is the fingerprint of the list that locked. See [Drawings](/api/drawings). # Send winners Source: https://docs.prizerail.com/api/send-winners If you'd rather call us than publish a URL, switch the campaign's **Winners** section to push in the partner portal, and your backend posts each drawing's winners before its cutoff. Posting to a pull campaign returns `409 pull_campaign`. ## The loop Drawings are created about two days ahead, so your backend can run this on a schedule, once your draw has happened: 1. `GET /api/v1/campaigns/{slug}/drawings?status=open`. Open drawings come soonest first, so the first one is the next cutoff. Take its `id` and `winnersExpected`. 2. Pick that many winners by your own rules. 3. `POST` them to that drawing, below. Keep the `id` with your own record of the draw, so a retry posts to the same drawing. 4. Before the next draw, the next open drawing is first in the list. If the post fails or times out, send the same list again. A retry is always safe. ```http theme={null} POST /api/v1/campaigns/{slug}/drawings/{id}/winners Content-Type: application/json ``` ```json theme={null} { "winners": [ "user-8231", { "winner": "user-4417", "displayName": "Ada", "avatarUrl": "https://…/ada.png" }, "user-0092" ] } ``` Send exactly as many winners as the drawing's `winnersExpected`. Each is either: * a string, the winner's id, or * an object with `winner` plus an optional `displayName` (up to 40 characters) and `avatarUrl` (https, up to 2,048 characters), shown on the campaign page. The same winner can't appear twice in one list. **Order matters:** each winner's position is part of how their prize is decided. The response is the drawing, with the winners filled in: `201` the first time, `200` after that. ## Replacing and retrying | When | Sending again | | - | - | | Before the cutoff | Replaces the list. | | After the cutoff, same list | Returns `200`. A retry is always safe. | | After the cutoff, different list | Returns `409 locked`. | Names and avatars aren't locked. Resend the same winners with new ones to update them at any time, even after the draw. ## No winners by the cutoff A drawing with no winners at its cutoff is flagged and waits. Extend its cutoff in the partner portal, then send the list. ## Winner ids Winner ids are public in each draw's proof. Use an id that can't be guessed or linked back to a person: an HMAC of your user id with a secret key, or a random id you store. A plain hash of a sequential or public user id can be reversed by trying ids. Keep the key fixed for the life of the campaign, and never use emails or usernames. ## Errors | Status | Code | When | | - | - | - | | 400 | `bad_request` | Not JSON, the wrong number of winners, a repeated winner, or a bad name or avatar. | | 404 | `not_found` | No such campaign or drawing under your account. | | 409 | `locked` | The cutoff has passed and this list differs from the locked one. | | 409 | `pull_campaign` | Prizerail fetches this campaign's winners from your URL. | | 409 | `campaign_ended` | The campaign has ended. | # Prize tables Source: https://docs.prizerail.com/campaigns/prize-tables A prize table splits a fixed number of rolls between prizes. Each prize owns a run of rolls as wide as its **weight**, biggest prize first from 0. Each winner rolls a number from 0 to one less than the total, and the run it lands in is their prize. It's the order the Prizerail contract takes. The default table, with 100,000 rolls: | Prize | Weight | Rolls | Odds | | - | - | - | - | | \$100,000 | 1 | 0 | 1 in 100,000 | | \$1,000 | 10 | 1 to 10 | 1 in 10,000 | | \$100 | 400 | 11 to 410 | 1 in 250 | | \$20 | 4,000 | 411 to 4,410 | 4.0% | | \$10 | 50,000 | 4,411 to 54,410 | 50.0% | | \$5 | 45,589 | 54,411 to 99,999 | 45.6% | Each winner wins \$9.58 on average with this table. ## Rules * A table has at least two prizes. * The weights must add up to the total rolls exactly, and each is a whole number above 0. The total can be up to 2,147,483,647. * Each prize is a whole number of cents, up to \$1,000,000. ## Changing the table Publish a new version from the campaign's **Prize table** in the partner portal. Drawings that are still open switch to it. Drawings that have locked keep the version they locked with, and each draw's proof names the table it used. ## Large prizes Prizes at or above the campaign's large-prize threshold, \$500 by default, aren't sent in the automatic transfer. They're held and paid by Prizerail by hand, and show `heldForManualPayout: true` in the API. # Status and lifecycle Source: https://docs.prizerail.com/campaigns/status A campaign's status moves one way: ``` draft ──► active ◄──► paused │ │ │ └──────► ended ◄───────┘ ``` | Status | Draws | Public page and proofs | | - | - | - | | `draft` | None | Hidden | | `active` | On the schedule | Shown | | `paused` | None | Shown | | `ended` | None. Final. | Shown | ## Rules * **Nothing goes back to draft.** A campaign that has run has published its proofs, and a draft's are hidden. * **Ended is final.** To run again, create a new campaign. * **Activating needs a payout wallet,** and once a campaign has started the wallet can be changed but not removed. ## Pausing * Draws stop, and the guarantee period clock stops with them. * Upcoming drawings are removed, including any winners already sent for them. On resume, new drawings are made, so send those lists again. A pull campaign fetches them again on its own. * A list already past its cutoff when you pause still draws. ## Ending * Upcoming drawings are removed, and the end date is set to now. * Payouts and guarantee checks still finish. * Sending winners returns `409 campaign_ended`. ## End dates A campaign with an end date stops scheduling draws after it. Once the end date has passed and every drawing is drawn, the campaign ends on its own. Moving the end date earlier removes the drawings after it. # Changelog Source: https://docs.prizerail.com/changelog Changes to the partner API (`/api/v1`) and the public endpoints. Within v1, fields are added but never removed or renamed. The first version of the partner API and these docs. * Campaigns fetch winners from your URL by default, or take them by `POST`. * Drawings are numbered 1, 2, 3 per campaign with no gaps, given when the random value is requested. `partnerRound` carries your own round number when Prizerail reads one from your source. * Proofs are `prizerail-draw-proof/2`: rolls start at 0, tiers run top prize first with thresholds, and the formulas match the Prizerail contract. * Guarantee periods are 30 days from the campaign's start, and paused time doesn't count. # Concepts Source: https://docs.prizerail.com/getting-started/concepts ## Campaign A campaign is one scheduled draw: hourly, daily, weekly or monthly, at a time in its own time zone. It has a prize table, a number of winners per drawing, a payout wallet and an optional guarantee. It's addressed by its **slug**, the last part of its public page address. ## Drawing Each run of the schedule is a **drawing**. Drawings are created about two days ahead. Each has: * `drawAt`: when the draw happens. * `cutoffAt`: the deadline for the winner list. Each campaign sets how long before `drawAt` its cutoff falls, from minutes to days. * `number`: its place in the campaign, 1, 2, 3 with no gaps. It's given when the random value is requested, so an open drawing has none yet. ## Winner A winner is a string you choose, up to 200 characters, such as an id for the user. It can carry a display name and avatar for the campaign page. Those are shown only and are never part of the proof. Winner ids are public in each draw's proof. Use an id that can't be guessed or linked back to a person: an HMAC of your user id with a secret key, or a random id you store. A plain hash of a sequential or public user id can be reversed by trying ids. Keep the key fixed for the life of the campaign, so the same user always maps to the same id, and never use emails or usernames. ## Cutoff Until the cutoff you can replace the list. At the cutoff it locks and its fingerprint is recorded. A drawing with no winners by its cutoff is flagged and waits until the cutoff is extended and the list arrives. ## Prize table A prize table splits a fixed number of rolls, such as 100,000, between prizes. Each winner rolls once, and the roll picks the prize. See [Prize tables](/campaigns/prize-tables). ## Guarantee A campaign can guarantee at least one prize of a set size each period, for example \$100 every 30 days. When a period ends without one, you pick which of that period's winners gets it. See [Guarantees](/api/guarantees). ## Payouts After each draw, the prizes below the campaign's large-prize threshold go to your payout wallet as one USDC transfer on Base. Prizes at or above it are held, and Prizerail staff send them to the same wallet by hand after a check. Either way the money comes to you, and you pay your winners however your app does it: a transfer, in-app credit, or anything else. # Partner portal Source: https://docs.prizerail.com/getting-started/partner-portal The partner portal at [prizerail.com/login](https://www.prizerail.com/login) is where your team runs its campaigns. Sign in with your email. You get a link, no password. ## What you can do | Task | Where | Who | | - | - | - | | Create a campaign | Campaigns → New campaign | Anyone on the team | | Set the winners URL, or switch to posting winners | The campaign's Winners | Anyone on the team | | Change its schedule, guarantee, wallet and public page | The campaign's page | Anyone on the team | | Activate, pause or end it | The campaign's Status | Anyone on the team | | Publish a new prize table version | The campaign's Prize table | Anyone on the team | | Extend a drawing's cutoff | The drawing's row, before it locks | Anyone on the team | | Pick a guarantee winner | The campaign's Guarantee | Anyone on the team | | Set your logo and links | Team → Campaign pages | Owners | | Invite or remove teammates | Team | Owners | | Create or revoke API keys | Team → API keys | Owners | New campaigns start as drafts. A draft's page and proofs are hidden until it's activated. Activating needs a payout wallet. ## Changes we're told about Prizerail staff get a note when a partner changes a campaign's status, payout wallet or prize table, since those changes affect what's paid. # Quickstart Source: https://docs.prizerail.com/getting-started/quickstart From a new campaign to your first drawn prize, in four steps. ## 1. Create your campaign Sign in to the partner portal at [prizerail.com/login](https://www.prizerail.com/login) and choose **Campaigns → New campaign**. You set: * Its name and slug. The slug is the last part of its public page: `https://www.prizerail.com/campaigns/acme-daily` is `acme-daily`. * The schedule: how often it draws, at what time, in which time zone, and how long before the draw the winner list closes (the cutoff). * How many winners each draw takes. * The prize table. The default one pays $5 to $100,000 out of 100,000 rolls. See [Prize tables](/campaigns/prize-tables). * The payout wallet: a USDC address on Base where prizes are sent. New campaigns start as drafts. A draft doesn't draw and its page is hidden. ## 2. Publish your winners at a URL For each draw, serve its winner list as JSON at a URL Prizerail can fetch, exactly as many winners as the campaign draws: ```json theme={null} { "winners": ["u_8f3a…", "u_27c1…", "u_b904…"] } ``` The URL can include placeholders that Prizerail fills in for each draw, such as `{drawingId}` or `{date}`: ``` https://api.acme.com/prizerail/{drawingId} ``` Until your draw has happened, return `404` or an empty list. Prizerail checks every 5 minutes in the 2 hours before each cutoff, and the list locks at the cutoff. See [Let Prizerail fetch winners](/api/pull-winners) for every placeholder and rule. Winner ids are public in each draw's proof, so use ids that can't be guessed or linked to a person. An HMAC of your user id with a secret key works well. See [Concepts](/getting-started/concepts#winner). ## 3. Set the URL and activate In the portal, open the campaign and put the URL in its **Winners** section. Then set its **Status** to active. Drawings appear about two days ahead, and the first one draws at the next scheduled time. ## 4. Read the results An owner creates an API key under **Team → API keys**. The key is shown once, in a dialog, so copy it into your backend's secrets before closing it. One key covers every campaign of yours. After the draw, each winner has a roll and a prize: ```bash theme={null} curl https://www.prizerail.com/api/v1/campaigns/acme-daily/drawings/d7k2m9q4x1wz \ -H "Authorization: Bearer $PRIZERAIL_KEY" ``` When `status` is `paid`, the prizes are in your wallet, ready for you to pay your winners. See [Drawings](/api/drawings). Rather post the winners yourself before every draw? Switch the campaign to push and see [Send winners](/api/send-winners). ## Trying it out Ask us for a test account. Test keys look like `pr_test_…` (production keys are `pr_prod_…`) and each works only in its own environment. In the test environment draws use test randomness and payouts aren't real, so you can run the whole flow before anything is paid. To see a whole draw quickly, make your test campaign hourly with a 5-minute cutoff. Each drawing then locks, settles and pays within the hour, and you can watch its `status` move through every stage. # Introduction Source: https://docs.prizerail.com/index Prizerail runs prize draws for apps. Your app picks the winners. Prizerail locks the list, decides each winner's prize with a random value, and pays the prizes to your wallet in USDC. Every draw publishes a proof that anyone can check. **Onchain randomness is coming.** Prizes will be decided by Pyth Entropy through the Prizerail contract on Base, so nobody, Prizerail included, can pick or predict the random value. The proof will show that the list was locked before it existed. See [Verify a draw](/verification/verify-a-draw#what-a-check-proves). ## How a draw works 1. **Prizerail gets the winners.** Before each draw's cutoff, Prizerail fetches the list from a URL you publish, or your backend posts it to the API. 2. **The list locks.** At the cutoff, Prizerail records a fingerprint of the list. It can't change after that. 3. **A random value is drawn.** Only after the lock. 4. **Each winner rolls.** The random value, each winner's position and their id decide their roll, and the roll picks a prize from your campaign's prize table. 5. **Prizes are paid.** Small prizes go to your wallet as one USDC transfer per draw. Large prizes are paid by hand. 6. **The proof is published,** with every input and result. ## Where to start * New here? Read the [Quickstart](/getting-started/quickstart). * Want the vocabulary first? See [Concepts](/getting-started/concepts). * Checking a draw? Go to [Verify a draw](/verification/verify-a-draw). Questions, or ready to set up a campaign? [Get in touch](https://www.prizerail.com/get-in-touch). # How a draw is decided Source: https://docs.prizerail.com/verification/how-draws-work Prizerail never picks who wins, only what each winner gets. The partner's list is locked before the random value exists, and each prize follows from the two by a fixed formula. ## The steps 1. **The partner sends the winners.** Until the cutoff, which each campaign sets before the draw, the list can change. 2. **The list locks.** At the cutoff, Prizerail records a fingerprint of the list, a hash of every winner in order. From then on the list can't change without the fingerprint changing. 3. **A random value is drawn.** Only after the lock is a random value requested from a source Prizerail doesn't control. 4. **Each winner rolls.** Each winner's roll comes from the random value, the campaign, the draw's number and the winner's position in the list. The roll picks a prize from the campaign's prize table. 5. **The proof is published,** with every input and result. ## The formulas These are the same formulas the Prizerail contract runs onchain. The hashes are keccak256, as Ethereum and Solidity use it, over Solidity's `abi.encode` of the values. Each winner is hashed on its own, and the **winner list fingerprint** covers those hashes in position order: ``` winnerHash = keccak256(bytes(winner)) fingerprint = keccak256(abi.encode(bytes32[] winnerHashes)) ``` Each winner at position `i` (0 for the first) gets a **random value** and a **roll** from 0 to one less than the prize table's total rolls. `campaignId` is the campaign's ID (on its page and in the proof) as ASCII padded to 32 bytes, and `drawingId` is the draw's number: ``` randomValue = keccak256(abi.encode(bytes32 entropy, bytes32 campaignId, uint64 drawingId, uint256 i, uint256 k)) roll = randomValue mod denominator ``` `k` starts at 0. A random value below `2^256 mod denominator` is discarded and `k` goes up by one, which keeps every roll exactly as likely as any other. It almost never happens, but a check has to do the same. The roll's run in the [prize table](/campaigns/prize-tables) is the prize. With the default table, a roll of 3,000 falls in 411 to 4,410, so that winner gets $20. Amounts in the proof are US cents, so `2000` is $20. ## Gotchas * **Order matters.** The fingerprint hashes the winners in position order, 0 first. Sorting them changes it. * **Rolls start at 0** and run to one less than the total. * **Tiers run top prize first.** The biggest prize owns the lowest rolls, and the proof lists tiers in that order with each one's threshold. * **The winner's id isn't in the random value.** It's in the fingerprint, which is locked before the random value exists, so the same winner listed twice still gets two separate rolls. * **Python's `hashlib.sha3_256` isn't keccak256.** They pad differently, so a check written with it always fails. # Public endpoints Source: https://docs.prizerail.com/verification/public-endpoints Everything a draw rests on is public, with no key and with CORS open, at `https://www.prizerail.com`. ## A campaign's draws ```http theme={null} GET /campaigns/{slug}/draws.json ``` The campaign (name, status, cadence, time zone, winners per draw, payout chain and token) and its draws, newest first. Each draw has its `id`, `number`, `drawAt`, `cutoffAt`, `stage`, number of `winners`, `winnersCommitment`, `lockedAt`, `settledAt`, `paidAt` and a link to its `proof`. | Query | Meaning | | - | - | | `limit` | Up to 100. Default 30. | | `before` | A date. Pass the response's `nextBefore` to page back. | ## A draw's proof ```http theme={null} GET /campaigns/{slug}/draws/{id}/proof.json ``` Available once a draw is drawn: | Field | Holds | | - | - | | `campaign` | `id` (the contract's campaign ID), `slug` and `name`. | | `drawing` | `id`, `number` (the contract's drawing ID) and `drawAt`. | | `winnerList` | `winners` in order, their `commitment` and `lockedAt`. | | `entropy` | `provider`, `requestId`, `value` and `revealedAt`. | | `prizeTable` | `version`, `denominator` and `tiers`, top prize first, each with `amountCents`, `weight` and `threshold`. | | `results` | Each winner's `position`, `winner`, `displayName`, `randomValue`, `roll` and `amountCents`. | `displayName` is the partner's public name for the winner, shown only and never hashed. ## The verify script ```http theme={null} GET /verify/verify-draw.mjs ``` See [Verify a draw](/verification/verify-a-draw). ## Stages The public pages call a drawing a draw and describe its stage in plain words. The partner API uses the same ids with its own `status` names: | Public `stage` | API `status` | Meaning | | - | - | - | | `open` | `open` | Taking winners until `cutoffAt`. | | `locked` | `locked` | The winner list is locked and its fingerprint recorded. | | `drawing` | `entropy_requested` | The random value has been requested. | | `drawn` | `settled` | Prizes are decided and the proof is published. | | `paid` | `paid` | Prizes are sent to the partner to pay out. | A draw's `id` here is the same as the drawing's `id` in the API, so `/campaigns/{slug}/draws/{id}/proof.json` and `/api/v1/campaigns/{slug}/drawings/{id}` describe the same draw. An unknown campaign returns `404`, and so does the proof of a draw that isn't drawn yet. Amounts are US cents, and times are ISO 8601 in UTC. # Verify a draw Source: https://docs.prizerail.com/verification/verify-a-draw Every draw can be checked by anyone, with no account and no key. ## In your browser On a campaign's page, each drawn draw has a **Verify in your browser** button that runs every check and shows the result. It runs on Prizerail's own site, though, so for a check that doesn't trust us, run the script yourself. ## With the script It needs Node.js 18 or later and nothing else: ```bash theme={null} curl -sO https://www.prizerail.com/verify/verify-draw.mjs node verify-draw.mjs https://www.prizerail.com/campaigns/{slug}/draws/{id}/proof.json ``` The script is short and readable, with the hashing written out so there's nothing to install. It tests its own keccak256 against published values first, then: * checks that the prize table's weights add up to its rolls * recomputes the winner list fingerprint * recomputes every winner's random value, roll and prize, and compares each with the proof It prints `✓` for a check that passes, `✗` for one that fails, and `i` for something the math can't settle, such as where the random value came from. It exits 0 only if nothing failed. ## With a library To cross-check with a library you already trust, viem's `keccak256(encodeAbiParameters(…))` with the types in [How a draw is decided](/verification/how-draws-work) gives the same values. ## What a check proves A passing check proves the prizes follow from the locked list and the random value, exactly as published. It doesn't prove who should be on the list: the partner chooses the winners by its own rules. Random values marked `mock` are test randomness from the test environment. They can't be checked independently, and real campaigns never use them: in a test draw, that the list locked before the random value existed rests on Prizerail's own timestamps. Real draws use onchain random values (Pyth Entropy through the Prizerail contract on Base), which are requested only after the lock and can be checked against the chain.