Overview
VRE commits the rules of an outcome on Solana before the result exists. Anyone can later recompute the result from the transaction signature, using only public RPC data, and compare it with what the program recorded.
What a MATCH proves
- The rules did not change. Participants, weights or scores, formula and winner count are the exact bytes stored on-chain under the committed artifact hash, and that artifact was bound to the runtime before resolution.
- The winners follow from the rules. Replaying the committed bytes with the randomness the program recorded yields the same winners, in the same order.
- The records agree. Input, output, winner count, winner ids and effect digests match across the event, the resolution account and the replay.
What it does not prove
- That the list was fair. VRE proves the list did not change after commit, not that it was complete or honest when committed.
- That the operator could not foresee the result on every path. This depends on the randomness source; see §02.
- That a prize was paid. Attestation paths prove the winners. Paying them is the integrator's step.
Network
A plain Markdown copy of this page for agents and tools lives at /spec.md, indexed from /llms.txt.
Resolution Model
Every path has the same shape: commit the rules, bind a runtime to them, resolve, replay.
- Commit. submit_compiled_artifact, init_compiled_artifact_chunk, write_compiled_artifact_chunk and finalize_compiled_artifact store the artifact bytes in chunk accounts keyed by their SHA-256. review_compiled_artifact, signed by the ProgramConfig admin, approves the artifact for binding.
- Bind. initialize_outcome_config creates a runtime, identified by runtime_id, bound to one artifact hash.
- Resolve. One of the paths in §06 writes an OutcomeResolution account and emits a resolution event.
- Replay. Any verifier rebuilds the artifact from its chunks, recomputes randomness and winners, and compares them with the records (§04).
Randomness sources
| Source | Where the entropy comes from | Operator can / cannot | Status |
|---|---|---|---|
| rng_v1 | A master_seed set by the operator when the runtime is created. | Cannot change the rules or the recorded result. Can compute the result before sending the transaction. | Legacy. No public endpoint uses it; not planned for mainnet. |
| rng_v2 · single phase (v1.2) | Hashes of three Solana slots fixed at prepare, that do not exist yet. | Cannot predict the result. Can decline to settle after the slots land; an unsettled draw stays on-chain and expires. | Canonical. Default. |
| rng_v2 · two phase (v1.3.2) | Same slot hashes; anyone may seal them in. | Cannot predict and cannot abort: after seal the outcome is fixed, and skipping attest forfeits the stake. | Canonical. Opt-in per draw. |
| Unicorn-G (B8) | A VDF output verified with Groth16. | Designed to remove residual validator influence on the seed. | Implemented; not deployed to the canonical program; see §06. |
The resolved slots and slot hashes are written into the resolution account and the event, so replay never needs historical SlotHashes.
Which path the public endpoints use today
| Endpoint | Resolution path |
|---|---|
| POST /api/examples/draw POST /api/partner/draw POST /api/live-raffle | rng_v2 single phase, in one request: commit, prepare_rng_v2, wait for the target slots, settle_rng_v2, replay. |
| POST /api/partner/snapshot/finalize | rng_v2 single phase on a format 7 snapshot artifact, in the background: publish the snapshot, commit, prepare_rng_v2, wait, settle_rng_v2 with the claimed winners, replay. |
| POST /api/partner/sortition/seal POST /api/examples/sortition/seal | Sortition seal on future slot hashes. |
| POST /api/partner/conditions/resolve | resolve_conditions: deterministic scoring of committed slips against revealed facts. No randomness. |
Artifact Formats
Rules are serialized into a binary artifact that starts with the magic "W3O1" and a u16 format version. All integers are little-endian. An artifact is at most 8,192 bytes. Participant ids are printable ASCII, zero-padded to 64 bytes, and strictly ascending by bytes.
| fmt | Name | Winners selected by | Randomness | Shape |
|---|---|---|---|---|
| 1 | W3O1 v1 | Program, from the on-chain table | rng_v1 · rng_v2 | Weighted single winner, per-outcome SOL effects. |
| 2 | W3O1 v2 | Program | rng_v1 · rng_v2 | Weighted multi-winner, without replacement. |
| 3 | W3O1 v3 | Program | rng_v1 · rng_v2 | Native formulas with signed scores and stable order. |
| 4 | Snapshot v4 | Caller claim, checked by snapshot replay | rng_v1 | Legacy snapshot line. |
| 5 | Outcome Standard V1.1 compact | Program | rng_v1 · rng_v2 | Named entries, one payout per winner, up to 103 entries. |
| 6 | Outcome Standard V1.2 scale | Caller claim, snapshot replay | rng_v1 | Snapshot hash and URI on-chain, list off-chain. |
| 7 | Outcome Standard V1.2.1 scale | Caller claim bound by winner_claim_hash, snapshot and Merkle replay | rng_v1 · rng_v2 | Large lists off-chain, optional Merkle root on-chain. |
| 8 | composite_rank_desc | Program | rng_v1 · rng_v2 (accepted, not yet in the end-to-end suite) | Multi-field weighted score, ranked descending. |
| 9 | Conditions fc7 | Program scores slips against facts | none | resolve_conditions only. |
| 10 | Slot SF1 | Program replays reels | future slot hashes | prepare_slot_spin and settle_slot_spin. |
Artifacts resolved through rng_v2 must be payout-free: the header payout and every effect amount are zero. The program then records who won, and the integrator pays.
Selection rules
- weighted_random. Round r takes a u64 from the first 8 bytes of the randomness when r = 0, and from SHA-256 of a domain tag, the randomness and r otherwise. It selects the entry whose cumulative weight covers u64 mod total_weight among entries not yet chosen.
- rank_desc. Score descending, ties by order ascending.
- rank_asc. Score ascending, ties by order ascending.
- first_n. order ascending.
- closest_to. |score − target| ascending, computed in 128 bits, ties by order ascending.
- Compact winner digest. On rng_v2 the program binds the full winner list as winner_claim_hash = SHA256("VRE_RNG_V2_WINNERS" ‖ count_u16le ‖ Σ(len_u8 ‖ id_bytes)).
W3O1 v1–v3 layout (fmt 1, 2, 3)
| Header field | Type | Notes |
|---|---|---|
| magic | bytes[4] | "W3O1" |
| format_version | u16 | 1, 2 or 3 |
| min_input_lamports · max_input_lamports | u64 · u64 | Must equal the runtime bounds. |
| outcome_count · total_effect_count | u16 · u16 | |
| v1: reserved | bytes[8] | Zero. One winner, weighted_random. |
| v2: winners_count · reserved | u16 · bytes[6] | weighted_random. |
| v3: winners_count · formula_code · reserved · target_score | u16 · u8 · bytes[5] · i64 | 1 weighted_random, 2 rank_desc, 3 rank_asc, 4 first_n, 5 closest_to. |
| Outcome record | Type | Notes |
|---|---|---|
| outcome_id_len · outcome_id | u8 · bytes[64] | Printable ASCII, zero-padded. |
| weight | u32 | Greater than zero. |
| v3 only: score · order | i64 · u16 | order values are unique. |
| first_effect_index · effect_count | u16 · u16 | Slice of the effect table. |
| effect | u8 · bytes[7] · u64 | Type 1 = transfer SOL, reserved zero, amount. |
Compact V1.1 layout (fmt 5)
| Field | Type | Notes |
|---|---|---|
| magic · format_version | bytes[4] · u16 | fmt 5 |
| min_input_lamports · max_input_lamports | u64 · u64 | |
| winners_count · formula_code · reserved · target_score | u16 · u8 · bytes[5] · i64 | 48-byte header in total. |
| entry_count · payout_lamports | u16 · u64 | One payout for every winner. |
| entry | u8 · bytes[64] · u32 · i64 · u16 | id_len · id · weight · score · order, 79 bytes each. |
Scale layout (fmt 6, 7)
| Field | Type | Notes |
|---|---|---|
| magic · format_version | bytes[4] · u16 | fmt 6 or 7 |
| min_input_lamports · max_input_lamports | u64 · u64 | |
| winners_count · formula_code · flags · reserved | u16 · u8 · u8 · bytes[4] | Flag bit 0: Merkle root present. |
| target_score · snapshot_count · payout_lamports | i64 · u32 · u64 | |
| snapshot_hash | bytes[32] | SHA-256 of the canonical snapshot. |
| merkle_root | bytes[32] | Only when flag bit 0 is set. |
| snapshot_uri_len · snapshot_uri | u16 · bytes | 1–512 bytes, no control characters. |
Composite, conditions and slot layouts are encoded and replayed by the SDK modules listed in §08.
Verification
Given a transaction signature and a program ID, the verifier:
- Fetches the transaction logs and finds the resolution event emitted by that program. Conditions and slot events go to their dedicated replays.
- Loads the runtime's OutcomeConfig, the OutcomeResolution, the ApprovedOutcomeArtifact header and, for rng_v2, the RngV2State.
- Checks the artifact hash is the same in the event, config, resolution and header, and that the artifact is finalized with a status that allows binding.
- Rebuilds the artifact from its chunk accounts and re-hashes it.
- Recomputes the randomness: from master_seed for rng_v1, from the recorded slots and slot hashes for rng_v2, checking any operator reveal against its commitment.
- Replays winner selection. Compact artifacts replay the on-chain table; scale artifacts fetch the snapshot from the committed URI or a hash-addressed mirror (see Snapshot sources in §08) and check its hash, count and Merkle root.
- Compares input, output, winner count, winner ids and their padding, winner digest and effect digest. All equal: MATCH / OK. Otherwise the first failing check is returned as the reason.
HTTP response
Request fields: signature (or sig) and an optional programId, which defaults to the canonical program. RPC and local-artifact overrides are disabled on the public host.
SDK result
verifyOutcome returns the same data with status and reason in place of verification_result and verification_reason. Fields appear only when they apply.
| Group | Fields |
|---|---|
| Always | status, reason, outcome_id, resolve_id, compiled_artifact_hash, runtime_id, program_id |
| Winners and rules | outcome_ids, winners_count, artifact_format_version, resolution_formula, target, standard_version, entry_count, outcomes[] |
| rng_v2 | rng_version, target_slots, resolved_slots, slot_hashes, operator_commit_hash, winner_claim_hash, entropy_source, protocol_revision |
| Scale snapshot | snapshot_hash, snapshot_count, snapshot_uri, merkle_root |
| Conditions | entrant_count, winning_score_sum, winners_digest, facts_ref_root |
| Slot spin | actor, input_lamports, payout_lamports, raw_win_lamports, reel_stops, randomness, settled_at_slot |
Reason codes
MATCH always carries OK. A MISMATCH carries one of these codes.
| Code | Meaning |
|---|---|
| OK | Every transaction, account, artifact, randomness, winner and digest check passed. |
| ERR_TX_NOT_FOUND_OR_NO_LOGS | The RPC could not return the transaction or its logs. |
| ERR_PROGRAM_ID_MISMATCH | The requested program was not invoked, or the event came from another program. |
| ERR_EVENT_DISCRIMINATOR_MISMATCH | Program data was present but is not a supported resolution event. |
| ERR_EVENT_NOT_FOUND_FOR_PROGRAM | No resolution event from the requested program. |
| ERR_OUTCOME_CONFIG_NOT_FOUND | The runtime's OutcomeConfig is missing or undecodable. |
| ERR_RESOLUTION_ACCOUNT_NOT_FOUND | The OutcomeResolution account is missing or undecodable. |
| ERR_ARTIFACT_HEADER_NOT_FOUND | The ApprovedOutcomeArtifact header is missing or undecodable. |
| ERR_ARTIFACT_CHUNK_MISSING | An artifact chunk is missing, unreadable, misplaced or incomplete. |
| ERR_CONFIG_HASH_MISMATCH | The config's artifact hash differs from the event. |
| ERR_RESOLUTION_HASH_MISMATCH | The resolution account's hash, runtime or resolve id differs from the event. |
| ERR_ARTIFACT_HASH_MISMATCH | The header, rebuilt bytes or a supplied file do not hash to the event's artifact hash. |
| ERR_ARTIFACT_NOT_FINALIZED | The artifact was never finalized. |
| ERR_ARTIFACT_STATUS_INVALID | The artifact status does not allow binding under the program config. |
| ERR_ARTIFACT_FORMAT_VERSION_MISMATCH | The format version differs between event, resolution account and artifact bytes. |
| ERR_RANDOMNESS_MISMATCH | Recomputed randomness differs from the event or the resolution account. |
| ERR_RNG_V2_EVIDENCE_MISSING | Resolved slots, slot hashes or commit evidence are missing. |
| ERR_RNG_V2_SNAPSHOT_HASH_MISMATCH | The snapshot hash in the event differs from the artifact, or is not zero for a compact artifact. |
| ERR_SNAPSHOT_UNAVAILABLE | No source — the committed URI, a hash-addressed mirror or a local copy — served the snapshot with the committed hash. The result is unverifiable for now, not disproven. |
| ERR_RNG_V2_STATE_MISMATCH | The event differs from the prepared RngV2State. |
| ERR_RNG_V2_RESOLUTION_MISMATCH | rng_v2 evidence differs between the event and the resolution account. |
| ERR_RNG_V2_OPERATOR_REVEAL_MISMATCH | The operator reveal is missing or does not hash to its commitment. |
| ERR_INPUT_MISMATCH | Input lamports differ between event, account and replay. |
| ERR_OUTPUT_MISMATCH | Output lamports differ between event, account and replay. |
| ERR_WINNER_COUNT_MISMATCH | Winner count differs between event, account, artifact and replay. |
| ERR_OUTCOME_ID_MISMATCH | A winner id, its length or its zero padding differs from the replay. |
| ERR_WINNER_CLAIM_HASH_MISMATCH | The recorded winner digest differs from the digest of the replayed winner list. |
| ERR_EFFECTS_DIGEST_MISMATCH | Effect count or effect digest differs from the replay. |
| ERR_CONDITIONS_ENTRANT_COUNT_MISMATCH | Conditions: the entrant count differs from the committed artifact. |
| ERR_CONDITIONS_FACTS_ROOT_MISMATCH | Conditions: the facts root differs from the event. |
| ERR_CONDITIONS_SCORE_SUM_MISMATCH | Conditions: the recomputed winning score sum differs from the event. |
| ERR_CONDITIONS_WINNERS_DIGEST_MISMATCH | Conditions: the recomputed ranking digest differs from the event. |
| ERR_SLOT_RANDOMNESS_MISMATCH | Slot spin: randomness from the frozen slot evidence differs from the event. |
| ERR_SLOT_REEL_STOPS_MISMATCH | Slot spin: replayed reel stops differ from the event. |
| ERR_SLOT_WIN_MISMATCH | Slot spin: the replayed raw win differs from the event. |
| ERR_SLOT_PAYOUT_MISMATCH | Slot spin: min(raw win, max-win cap) differs from the payout. |
| ERR_REPLAY_UNHANDLED | Internal error or unsupported shape. On the public devnet RPC it can also mean a temporary rate limit: retry after 20–30 seconds before treating it as a failure. |
Canonical Program
One program on devnet carries every path. Integrators use it directly; a dedicated instance is possible under a partner agreement.
Key separation
The upgrade authority can replace the code and nothing else. The ProgramConfig admin signs operational calls, including review_compiled_artifact, and cannot upgrade the program, so a compromised operator host cannot swap the code. Both are single keys on devnet. Mainnet governance is decided separately and is not implied by the devnet setup.
ProgramConfig
| Field | Type | Devnet value |
|---|---|---|
| admin | Pubkey | CJN7pVC8LCsia4QPy1yWiTX3iQMtVbnwjoUmyNK4ce2Y |
| allow_unreviewed_binding | bool | false |
| fee_lamports | u64 | 0 |
| treasury | Pubkey | GQcdfnLVQn8Jur7SAyokBGL9uspveS2GbUo7r95kgLQm |
Account addresses
| Account | PDA seeds |
|---|---|
| ProgramConfig | ["outcome_program_config"] |
| ApprovedOutcomeArtifact | ["approved_outcome_artifact", artifact_hash] |
| artifact chunk | ["approved_outcome_artifact_chunk", artifact_hash, index_u32le] |
| OutcomeConfig | ["outcome_config", runtime_id] |
| vault | ["outcome_vault", runtime_id] |
| OutcomeResolution | ["outcome_resolution", runtime_id, resolve_id_u64le] |
| RngV2State | ["rng_v2_state", runtime_id, resolve_id_u64le] |
Protocol fee
fee_lamports is 0 on devnet and can be changed by the admin without an upgrade. When set, it is charged by resolve_outcome* and settle_rng_v2 from the calling operator, snapshotted at prepare_rng_v2 and taken from the stake at attest_winners, and charged on paid sortition registration. Verification is always free.
Instructions
| Group | Instructions |
|---|---|
| Config and admin | initialize_program_config, set_program_config, recover_program_config_admin, admin_pause, admin_withdraw |
| Artifact commit | submit_compiled_artifact, init_compiled_artifact_chunk, write_compiled_artifact_chunk, finalize_compiled_artifact, review_compiled_artifact, initialize_outcome_config |
| rng_v1 resolve | resolve_outcome, resolve_outcome_v4, resolve_outcome_v121, refresh_master_seed |
| rng_v2 | prepare_rng_v2, settle_rng_v2, seal_randomness, attest_winners, expire_rng_v2, expire_sealed_rng_v2 |
| Sortition | create_sortition_draw, register_sortition_ticket, register_sortition_ticket_paid, seal_sortition_draw, claim_sortition_ticket, init_sortition_economics, close_sortition_ticket, sweep_sortition_tickets |
| Conditions | resolve_conditions |
| Slot | prepare_slot_spin, settle_slot_spin |
| Unicorn-G (source only) | initialize_unicorn_g_config, set_unicorn_g_config, prepare_rng_v2_unicorn, seal_randomness_unicorn |
Compute and heap
rng_v2 prepare_rng_v2, settle_rng_v2 and attest_winners on a compact artifact parse the whole outcome table on-chain. Add ComputeBudgetProgram.requestHeapFrame up to 256 KiB and a compute limit of about 1.4M units. Measured on 103 entries: settle 174k–266k CU, attest 142k–231k CU. Other instructions keep the default 32 KiB heap.
Resolution Paths
| Version | Path | Status on the canonical program |
|---|---|---|
| rng_v1 | Operator-seed resolve | Legacy · retained |
| v1.2 | Single-phase settle | Canonical · default |
| v1.3.2 | Two-phase seal and attest | Canonical · opt-in |
| 2026-09-09 | Compact artifacts on rng_v2 | Canonical |
| v1.4.0 | Sortition-by-Claim | Canonical |
| v1.4.1 | Sortition economics | Canonical |
| fc7 | Conditions | Canonical · attestation only |
| SF1 | Slot spins | Instructions on the canonical program; demo game on its own program |
| B8 | Unicorn-G entropy | Research · not in the current build |
Single-phase settle (v1.2) · default
prepare_rng_v2 fixes the rules, the stake and three future target slots, 8, 16 and 24 slots ahead. A Solana leader produces four slots in a row, so the first target is always at least two leader groups after the slot that included prepare, and the three targets fall in three different leader groups. Groups are not validators: one validator can lead several groups, and a skipped target moves to the next produced slot after the previous resolved one. A devnet draw on 2026-09-13 shows both: eight slots were skipped, targets ending 680, 688, 696 resolved to 688, 689, 696, and one validator produced all three. Measured over 5,000 recent slots on 2026-09-13: on mainnet one validator produced two of the three hashes in 3.1% of draws with these offsets, against 2.3% with 75/90/105, and never all three; on devnet, with 12 leaders and a 22.5% skip rate, one validator produced two or more in about 73% of draws and all three in about 10%, whatever the offsets. After the last target slot and before expiry, the same operator calls settle_rng_v2, which reads SlotHashes, derives the randomness, resolves the artifact and writes the resolution. The operator cannot predict the result. It can still choose not to settle; the prepared draw then remains visible on-chain and expires through expire_rng_v2, which refunds the stake. The public draw endpoints run this path in one request.
Two-phase seal and attest (v1.3.2)
- prepare — the operator opens the draw with a stake and a bounty.
- seal — seal_randomness is permissionless. Once the target slots land, anyone can seal, fixing the outcome and earning the bounty.
- attest — attest_winners, by the operator or the configured attester, records the resolution, takes the fee and refunds the rest of the stake.
- expire — a never-sealed draw expires and its bounty goes to the treasury; a sealed draw not attested by the deadline, 480 slots after expiry, can be closed by anyone with expire_sealed_rng_v2.
Use it when the operator has an interest in the result and must not be able to back out.
Compact artifacts on rng_v2
Since 2026-09-09 both rng_v2 lifecycles accept compact formats 1, 2, 3, 5 and 8, not only scale format 7. For compact artifacts the program selects the winners itself, so callers pass empty winner arguments; non-empty ones fail with RngV2CompactArgsMustBeEmpty. The resolution stores winner_claim_hash as the digest of the winner list, and the list is emitted in OutcomeResolvedV5. Scale artifacts keep caller-claimed winners proved by snapshot replay and emit OutcomeResolvedV4. Both must be payout-free. SDK 0.9.1 verifies these draws, and the public draw endpoints resolve through this path.
Sortition-by-Claim (v1.4.0)
Entrants register sha256(secret ‖ ticket_id ‖ draw_id) before a deadline. seal_sortition_draw fixes omega from three future slot hashes, 75, 90 and 105 slots after the draw was created; registration must close before the first of them. Each entrant claims by revealing the secret and wins when sha256(secret ‖ omega) < threshold.
- Cost. A claim uses about 8,200 CU, under the 10k budget.
- Coverage. 5 of 5 negative cases rejected.
- Distribution. 200 draws of 100 tickets with 10 expected winners: mean 9.7, variance 8.76, against a binomial mean of 10 and variance 9.
Winners claim for themselves, so the operator pays nothing per winner. Fits airdrops, allowlists and mass rewards.
Sortition economics (v1.4.1)
An optional fee layer, byte-compatible with 1.4.0 draws. fee_mode selects an entry_fee_lamports at registration and a close_skim_bps skim at close. close_split(rent, skim_bps) guarantees refund + skim == rent. Measured: paid register 18,096 CU, init economics 5,234 CU, close 9,643 CU, sweep of two tickets 18,459 CU; 9 of 9 negative cases rejected.
Conditions (fc7)
Scores committed prediction slips, each a set of legs with odds, against revealed match facts, and ranks entrants. The off-chain scorer at POST /api/conditions/score is byte-identical to on-chain resolve_conditions, which uses about 15,700 CU. The event ConditionsResolvedV1 carries winners_digest, facts_ref_root and winning_score_sum. VRE proves the ranking; payout stays with the partner.
Slot spins (SF1)
prepare_slot_spin binds the player, the bet and the committed game before the entropy exists. settle_slot_spin derives randomness from future slot hashes, replays the reels from the artifact, applies the max-win cap and pays the bound player from the vault. The public demo game runs on its own devnet program, 6R5n5uPTj1bg4xz6P3xCmVkyorTySbLQQZuENe8SSaKK; verify its spins with the SDK and that program ID.
Unicorn-G entropy (B8) · implemented, not deployed
An alternative entropy source: a VDF output verified on-chain with an embedded Groth16 verifier. It was measured on devnet in June 2026: seal with verification 247,476 CU, attest 61,466 CU, and a wrong proof rejected as InvalidUnicornGProof. Its instructions sit behind a build feature that the canonical program is not built with. Before mainnet it needs the circuit soundness fix and a trusted-setup ceremony.
HTTP API
Base URL https://verifiableoutcome.online. JSON in, JSON out. Draw endpoints sign with the server's operator key, so callers never hold one. A draw resolves through rng_v2 inside the request: the server commits the rules, waits for the future target slots, about 10 seconds, then settles and replays, so a draw call takes 12–45 seconds in total.
The canonical program binds only artifacts approved by the ProgramConfig admin, so a draw cannot be resolved there with your own wallet; the API runs it with the operator key. Sortition instructions need no admin and can be sent from any funded wallet (§08).
Limits and statuses
| Limit | Default |
|---|---|
| General API, per IP | 60 requests / 60 s |
| POST /api/replay, per IP | 12 requests / 60 s |
| POST /api/examples/draw, per IP | 1 draw / 15 s |
| POST /api/live-raffle | 1 draw / 10 s |
| POST /api/partner/draw, per key | 1 draw / 10 s |
| POST /api/examples/sortition/draw, per IP | 1 draw / 15 s |
Statuses: 200 success; 400 validation error; 401 missing or unknown key; 403 key without the needed permission; 404 unknown conditions round, or an account that does not exist (AccountNotInitialized); 409 a scale-shaped body sent to a draw endpoint, a duplicate snapshot chunk, or a program state conflict (SortitionRegistrationOpen, SortitionRegistrationClosed, SortitionClaimWindowClosed, SortitionAlreadyClaimed, AccountAlreadyInUse for a reused id); 413 participant table too large for a compact artifact; 422 the program rejected the input (SortitionPredicateNotMet for a losing ticket, InvalidSortitionSecret, other InvalidSortition*); 429 rate limit, counted from the start of the previous draw; 500 server-side failure; 503 partner config absent on the host; 504 devnet timeout, retry.
When the program rejects an instruction the body carries error_code (the program error name), error_number and a readable error: { "ok": false, "error": "Sortition predicate not met", "error_code": "SortitionPredicateNotMet", "error_number": 6045 }.
Public, no key
| Endpoint | Request | Response |
|---|---|---|
| POST /api/replay | signature, programId? | Verdict, see §04. |
| POST /api/timeline | signature, compiledArtifactHash, programId? | { ok, artifact_slot, resolution_slot, gap_slots } |
| GET /api/recent-resolutions | ?limit= | { ok, resolutions: [{ signature, outcome_id, outcome_ids, winners_count, artifact_format_version, standard_version, rng_version, resolution_formula, target, verification_result, verification_reason, runtime_id, resolve_id, compiled_artifact_hash, participants_count, snapshot_count, snapshot_hash, snapshot_uri, merkle_root, commit_slot, resolve_slot, updated_utc, source }] } |
| GET /api/blessed-signatures | { ok, data } | |
| GET /api/health | { ok, program_id, rpc, operator: { mode, program_id }, world_id: { enabled, environment, … }, blessed_signatures_count } | |
| GET /api/spec | { ok, summary, blessed } | |
| POST /api/conditions/score | fixtures, entrants | Scores and ranking, see below. |
| POST /api/examples/draw | Same body as partner draw. | Same response as partner draw. Runs on the server's demo key. |
| POST /api/examples/sortition/draw | Same body as partner sortition draw. | Creates a sortition draw on the demo key. |
| POST /api/examples/sortition/register | draw_id, ticket_id, commitment | Registers a ticket commitment. |
| POST /api/examples/sortition/seal | draw_id | Seals omega from future slot hashes. |
| POST /api/examples/sortition/claim | draw_id, ticket_id, secret | Reveals and claims a ticket. |
Scoring conditions without a key
Leg encoding: op 1 adds, for totals, and 2 subtracts, for 1x2; comparison 0 greater, 1 less, 2 equal; line_x2 is the line times two, totals only. For full-match goals stat_key 1 is side one and 2 is side two.
Live raffle with a wallet
| Endpoint | Request | Response |
|---|---|---|
| POST /api/live-raffle | address, requireWorldId?, worldId? | { ok, signature, outcome, outcome_ids, runtimeId, resolveId, artifactHash, programId, participantCount, world_id?, rng_version, entropy_source, prepare_signature, target_slots, resolved_slots, winner_claim_hash } |
| POST /api/world-id/rp-context | { ok, enabled, …, rp_context } |
With requireWorldId, the caller sends a World ID 4.0 proof; the server verifies it and reserves its nullifier, so one human enters once. The public host runs World ID in staging, through the simulator. Wallet pages read only the address through the Wallet Standard and never ask to sign.
Partner API
Every partner endpoint takes x-api-key: vresk_<32 hex>. Keys are issued on request in Telegram and stored server-side only. Draws additionally require draw_enabled on the key.
| Endpoint | Request | Response |
|---|---|---|
| POST /api/partner/draw | formula, participants, winners_count?, target?, label?, use_case? | { ok, signature, outcome_id, outcome_ids, replay_url, artifact_slot, resolution_slot, rng_version, entropy_source, prepare_signature, target_slots, resolved_slots, winner_claim_hash } |
| GET /api/partner/draw/:signature/proof | ?address= | Membership proof for a scale draw: included, winner, participant, merkle_root, leaf_hash, proof, snapshot_uri, publication_url. |
| POST /api/partner/snapshot/init | label?, use_case? | A snapshot session id, valid for 24 hours. |
| POST /api/partner/snapshot/chunk | session_id, chunk_index, participants | Up to 5,000 participants per chunk, 2 MB per body. |
| POST /api/partner/snapshot/finalize | session_id, formula, winners_count, target?, wait? | 202 with status_url; the draw runs in the background. wait: true answers 200 with the result instead. |
| GET /api/partner/snapshot/status | ?session_id= | Status open, finalizing, finalized or failed, with stage, result (signature, winners, snapshot_uri, snapshot_mirror_url, merkle_root, rng_v2 fields), or error and retryable. |
| POST /api/partner/sortition/draw | draw_id?, threshold, registration_duration_slots, claim_duration_slots, unclaimed_rule | Creates a sortition draw. |
| POST /api/partner/sortition/register | draw_id, ticket_id, commitment | Registers a ticket commitment. |
| POST /api/partner/sortition/seal | draw_id | Seals omega from future slot hashes. |
| POST /api/partner/sortition/claim | draw_id, ticket_id, secret | Reveals and claims a ticket. |
| POST /api/partner/conditions/commit | round_id, fixtures, entrants | { commitment_ref, commit_signature, committed_at_slot } |
| POST /api/partner/conditions/resolve | round_id, fixtures (with stats, event_stat_root) | { signature, verify_url, winners, ranking, winning_score_sum, artifact } |
| GET /api/resolutions | ?limit= | Recent resolutions on the canonical program. |
| GET /api/participant | ?address= | Draws where the address participated and whether it won. |
Partner draw fields
- formula. weighted_random, rank_desc, rank_asc, first_n or closest_to.
- participants. At least 2 objects with a unique id of up to 64 printable ASCII bytes, plus weight or score as the formula needs. The compact artifact holds about 100 entries; use the snapshot flow above for more.
- winners_count. 1 to 10, not more than the participants.
- target. Required for closest_to; a signed safe integer.
- use_case. raffle, airdrop or competition; informational.
- Scale-shaped body. mode: "scale", scale: true, public_proof: true or standard_version: "1.2" on this endpoint returns 409 with route: "scale_snapshot" and the snapshot endpoints.
- artifact_slot. The slot where these exact rules were first committed; identical rules reuse one artifact, so it can predate the draw.
SDK
verifiable-outcome-sdk is the TypeScript library for building artifacts and verifying outcomes. It is an ES module for Node 18 and later.
Verify from a terminal
Verify a transaction
Build an artifact
Exports
| Area | Exports |
|---|---|
| Verification | verifyOutcome |
| Artifacts | buildArtifact, buildArtifactV4, buildArtifactV5, buildArtifactV12, buildArtifactV121, parseArtifactV5, parseArtifactV12, parseArtifactV121 |
| Snapshots and Merkle | buildSnapshotHash, buildSnapshotManifest, buildWinnerClaimHash, buildMerkleLevels, buildMerkleProof, buildSnapshotMerkleProofFromFile, hashSnapshotLeafLine; since 0.9.0: canonicalSnapshotLine, verifySnapshotMerkleProof, computeSnapshotMerkleRootFromProof, fetchSnapshotCommitment, fetchCommittedSnapshot, snapshotSourceCandidates, SnapshotUnavailableError |
| Composite | buildCompositeRankDescArtifact, parseCompositeRankDescArtifact, replayCompositeRankDescArtifact |
| Sortition | createSortitionClient, sortitionCreateDrawInline, sortitionRegisterTicketInline, sortitionSealDrawInline, sortitionClaimTicketInline, sortitionCommitment, sortitionWins, sortitionThresholdFromProbability, build*SortitionInstruction helpers |
| Conditions and slot | Scoring, artifact encoders and replay helpers for fc7 and SF1. |
The operator resolve client and the rng_v2 prepare-and-settle client are not in the published package: draws run through the HTTP API. verifyOutcome and vre verify do not replay sortition transactions; the claim instruction enforces the sortition predicate on-chain.
Snapshot sources
snapshot_uri is opaque to the program. It is part of the hashed artifact, so a draw commits to where the list was published, and the file itself is checked against snapshot_hash. The verifier tries, in order:
- a local copy (--snapshot-file);
- the committed URI: https:// as is, irys-devnet://<id> through devnet.irys.xyz and gateway.irys.xyz, irys:// through gateway.irys.xyz, ar:// through arweave.net and turbo-gateway.com;
- mirrors by hash, <mirror>/sha256/<snapshot_hash>.jsonl: https://verifiableoutcome.online by default, override with --snapshot-mirror or VRE_SNAPSHOT_MIRRORS.
A source whose bytes do not hash to snapshot_hash is skipped. If no source serves the file, the result is ERR_SNAPSHOT_UNAVAILABLE. Moving to other storage needs no program upgrade: new draws commit a different URI, and older draws keep verifying through mirrors. Irys devnet drops uploads after about 60 days; the VRE mirror keeps every snapshot it published.
File format: one JSON object per line, {{"id","order","weight"}} for weighted_random, {{"id","order"}} for first_n, {{"id","order","score"}} otherwise, each ending in a newline, sorted by id bytes. Leaf = SHA-256(SHA-256(line)); node = SHA-256(left ‖ right), the last node is duplicated on odd levels; a proof is a list of {{ position, sibling }} from the leaf upward, where position is the side the sibling sits on. verifySnapshotMerkleProof checks a line and proof against the on-chain merkle_root.
Measured on devnet on 2026-09-15: 10,001 participants, finalized in 23 s, replayed to MATCH from the committed URI alone. On a local validator, 100,001 participants (8 MB) took 24 s.
Use Cases
| Use case | Mechanism | What is committed before the result |
|---|---|---|
| Raffle or NFT drop | weighted_random | Every ticket holder with a weight equal to their tickets. |
| Trading competition | rank_desc | Final scores; the top N follow deterministically. |
| Lowest-bid contest | rank_asc | Bids; no late entries or edits after commit. |
| Guess the value | closest_to | Guesses and the target value. |
| First-come airdrop | first_n | Registration order. |
| Pack break or loot table | weighted_random | Every slot and its weight, before the stream. |
| Mass airdrop or allowlist | Sortition | Ticket commitments; winners claim themselves. |
| Large leaderboard | Scale snapshot | A snapshot hash and Merkle root; each wallet proves inclusion. |
| Prediction slips | Conditions | Every slip with its legs and odds; facts are revealed later. |
| Slot game | SF1 | The game math, before each bet's entropy exists. |