Guide and API
This is the integration guide for the platform behind the three demos on this site. It covers the order the ledger enforces, how a tenant authenticates, the production limits, and a working call for every step of a prize draw, an instant win and a spin to win game. Every command on this page was run as written on 5 September 2026 against the live platform and the Aptos mainnet, and the responses shown are the real ones with long values shortened.
1. What you integrate with
Your system never talks to the blockchain directly and never holds a key or any tokens. It sends fingerprints (hashes) of its own records to the platform, and the platform writes them to the Aptos ledger inside transactions it builds, signs and pays for. What comes back is the transaction that recorded each fingerprint, its position in that transaction, and the seed the ledger minted when a step consumed randomness.
| Piece | What it does | Where it listens |
|---|---|---|
| Coordinator | The tenant API. One endpoint, POST /submit. Validates, batches, hands the work to the engine, returns the result synchronously or by signed webhook. | 127.0.0.1:8090 on the box that runs it. |
| Engine | Holds the worker accounts, builds and signs the transactions, pre-checks a ticket against the ledger before spending gas, and sweeps the storage a sealed draw no longer needs. Also serves the spin endpoints POST /spin/game and POST /spin. | 127.0.0.1:8085 on the same box. Loopback by design: a key-holding process is never public. |
| Contract | golden_egg_vp1b at 0xd1c1…f929 on Aptos mainnet, published as permanent (upgrade policy 2). One entry function per product per step; every entry emits one event that anyone can read from any public node. | Aptos mainnet. |
Neither service is exposed to the internet by this site. A production tenant reaches the coordinator over a private network or through a TLS front the deployment adds, and the examples below use the addresses as seen from the box. The three demos on this site are ordinary tenants of exactly this API with their own key; their thin browser endpoints are listed in the appendix.
2. The lifecycle the ledger enforces
All three products follow the same four steps. The ledger refuses anything out of order, and the refusal is a named abort on the public record, not a policy in our software.
- Rules. You hash the rules document and commit the hash. That hash becomes the draw's identity (
draw_id) and nothing about the draw can change afterwards. A prize draw or instant win declares how many tickets exist and, optionally, a closing time; a spin game declares how many outcomes there are and how many of them are prizes. - Entries. A ticket carries a number from 1 to the declared supply and each number can be taken once. A spin carries only its fingerprint; the ledger mints a seed in the same transaction and derives the outcome from it. An instant-win ticket batch mints one seed, and each ticket's own value is derived from that seed and its position, off ledger.
- Seal. Closes the entry set for good, never before the declared closing time. The seal can declare an earliest draw time. Within seconds of the seal the platform refunds the storage the entries reserved.
- Draw. Only after the seal, and only after any draw time the seal declared. It mints the seed your published rule turns into a winner. The platform never chooses the winner.
The functions, their arguments and their events
| Step | Prize draw | Instant win | Spin to win |
|---|---|---|---|
| Rules | commit_pd_rules(rules_hash, max_tickets, duration_seconds) PD_RULES_COMMIT {commit_hash, max_tickets, duration_seconds, batch_position} | commit_iw_rules(rules_hash, max_tickets, duration_seconds) IW_RULES_COMMIT, same fields | commit_stw_rules(game_hash, possible_outcomes, num_prizes, duration_seconds, expected_spins) STW_RULES_COMMIT {commit_hash, possible_outcomes, num_prizes, duration_seconds, batch_position} |
| Entries | commit_pd_tickets(draw_id, hashes, ticket_numbers) PD_TICKET_COMMIT {draw_id, commit_hash, ticket_number, batch_position} per ticket | commit_iw_tickets(draw_id, hashes, ticket_numbers) IW_TICKET_COMMIT, same fields plus aptos_unpredictable_seed | commit_stw_spins(game_id, hashes) STW_SPIN_COMMIT {draw_id, commit_hash, outcome, batch_position, aptos_unpredictable_seed} per spin |
| Seal | commit_pd_seal(draw_id, seal_hash, draw_not_before) PD_SEAL_COMMIT {draw_id, commit_hash, draw_not_before, batch_position} | commit_iw_seal, IW_SEAL_COMMIT | commit_stw_seal, STW_SEAL_COMMIT |
| Draw | commit_pd_draw(draw_id, draw_hash) PD_DRAW_COMMIT {draw_id, commit_hash, batch_position, aptos_unpredictable_seed} | commit_iw_draw, IW_DRAW_COMMIT | commit_stw_draw, STW_DRAW_COMMIT |
| Sweep | After a seal the sealing worker calls reclaim_pages (ticket bitmaps) or reclaim_idem (spin pages), emitting PAGES_RECLAIMED {draw_id, page_kind, reclaimed_page_indexes, reclaimed_by}. Automatic; no call of yours. | ||
One transaction is one product and one step. The function name on the explorer says which, before any event is opened. A seed is minted only where it is consumed: instant-win tickets, spins and draws. Rules and seals never carry one, and a prize-draw ticket batch mints nothing.
The refusals, and where they happen
Two layers refuse. The engine pre-checks a ticket against the ledger before spending any gas, so a taken number, a sealed draw or an unknown draw comes back as a per-entry status with no transaction. Everything else is enforced by the contract itself and surfaces as a named abort.
| What was attempted | Prize draw | Instant win | Spin to win | Pre-check status |
|---|---|---|---|---|
| An entry for a draw that does not exist | E_PD_DRAW_NOT_FOUND (100) | E_IW_DRAW_NOT_FOUND (120) | E_STW_GAME_NOT_FOUND (140) | rules_not_committed |
| The wrong product for that draw | E_DRAW_IS_NOT_PRIZE_DRAW (101) | E_DRAW_IS_NOT_INSTANT_WIN (121) | E_DRAW_IS_NOT_SPIN_GAME (141) | on chain |
| A ticket number already taken, or a spin fingerprint already used | E_PD_TICKET_NUMBER_TAKEN (108) | E_IW_TICKET_NUMBER_TAKEN (128) | E_STW_SPIN_ALREADY_COMMITTED (148) | ticket_taken; spins on chain |
| A ticket number outside 1..max_tickets | E_PD_TICKET_NUMBER_OUT_OF_RANGE (107) | E_IW_TICKET_NUMBER_OUT_OF_RANGE (127) | on chain | |
| An entry after the seal | E_PD_ENTRIES_SEALED (105) | E_IW_ENTRIES_SEALED (125) | E_STW_SPINS_SEALED (145) | draw_sealed; spins on chain |
| An entry after the closing time | E_PD_ENTRY_WINDOW_CLOSED (106) | E_IW_ENTRY_WINDOW_CLOSED (126) | E_STW_SPIN_WINDOW_CLOSED (146) | on chain |
| A second seal, or a second draw | E_PD_ALREADY_SEALED (103), E_PD_ALREADY_DRAWN (104) | E_IW_ALREADY_SEALED (123), E_IW_ALREADY_DRAWN (124) | E_STW_ALREADY_SEALED (143), E_STW_ALREADY_DRAWN (144) | on chain |
| A seal before the closing time | E_PD_SEAL_TOO_EARLY (109) | E_IW_SEAL_TOO_EARLY (129) | E_STW_SEAL_TOO_EARLY (149) | on chain |
| A draw before the seal, or before the declared draw time | E_PD_DRAW_BEFORE_SEAL (110), E_PD_DRAW_TOO_EARLY (111) | E_IW_DRAW_BEFORE_SEAL (130), E_IW_DRAW_TOO_EARLY (131) | E_STW_DRAW_BEFORE_SEAL (150), E_STW_DRAW_TOO_EARLY (151) | on chain |
| A rules, seal or draw hash the ledger has already recorded | E_HASH_ALREADY_COMMITTED (9): the original transaction is the record; the reply points at it as replayed | on chain | ||
Generic aborts 1 to 17 cover malformed input: an empty batch (1), a hash that is not 32 bytes (2), more than 1,000 entries in one transaction (3), max_tickets outside 1..1,000,000,000 (13), duration_seconds above 16,777,215 (14), possible_outcomes out of range (15), num_prizes above possible_outcomes (16), expected_spins of zero (17). The coordinator rejects all of these before they reach the chain.
3. Authentication and limits
Every tenant call carries an API key we issue. The key belongs to a client record with a scope (commit:write for all the calls on this page), a sustained rate and burst, an optional monthly quota, an optional IP allowlist, an optional expiry, and a revocation switch. Keys can be rotated without downtime because a client may hold several.
# either header form works
-H 'Authorization: Bearer $API_KEY'
-H 'X-API-Key: $API_KEY'
Signed requests. A key can also carry a signing secret. Send X-Key-Id (the key's public id), X-Timestamp (unix seconds, within five minutes of the platform clock), X-Nonce (unique per request; a reuse inside the window is refused) and X-Signature. The signature is hex HMAC-SHA256, keyed with the signing secret, over the string timestamp\nnonce\nMETHOD\npath\n followed by the raw request body. A request carrying X-Signature is verified as a signed request; one without it is treated as a bearer key.
| Limit | Value | Note |
|---|---|---|
| Entries per prize request | 500 | One request is one transaction, so its entries share a transaction version and have contiguous positions. The contract's own cap is 1,000. A request that would need more than about 183 new storage pages is split into ordered chunks for you and merged back; each entry's own txn_version is authoritative. |
| Spins per request | 1,000 hard cap; keep batches around 200 | Larger spin batches approach the VM execution limit. |
| Request body | 1 MiB | |
| Synchronous wait | 30 seconds by default | Then 504; the batch may still commit. Use a callback for anything you cannot wait on. |
| max_tickets | 1 to 1,000,000,000 | Ticket numbers run 1..max_tickets. |
| duration_seconds | 0 to 16,777,215 (about 194 days) | 0 means no timed close; the draw closes when sealed. |
| possible_outcomes, num_prizes | 1 to 1,000,000,000; num_prizes at most possible_outcomes | Outcomes 1..num_prizes are the prizes. |
| expected_spins | at least 1 | Sizes the game's spin index; a game can take more spins than declared. |
| Hashes | 32 bytes as 64 hex characters, optional 0x | SHA3-256 is what the platform and the demos use. |
| Rate limit | per client | 429 with Retry-After: 1; a monthly quota returns 429 with Retry-After: 3600. |
4. The request and the result
POST /submit with "prize_draw": true selects the prize pipeline. The entries are objects; each names its step (commit_type: rules, ticket, seal or draw), its product (draw_type: prize_draw, instant_win, or spin_to_win on seal and draw only), and the fields that step owns. Unknown fields are rejected, which catches typos. Spin games and spins use the engine's two endpoints instead, shown in section 8.
{
"prize_draw": true,
"callback_url": "https://you.example/hook", // omit for a synchronous reply
"entries": [
{"hash": "…", "commit_type": "rules", "draw_type": "prize_draw", "max_tickets": 100, "duration_seconds": 0},
{"hash": "…", "commit_type": "ticket", "draw_type": "prize_draw", "draw_id": "…", "ticket_id": 7},
{"hash": "…", "commit_type": "seal", "draw_type": "prize_draw", "draw_id": "…", "draw_not_before": 0},
{"hash": "…", "commit_type": "draw", "draw_type": "prize_draw", "draw_id": "…"}
]
}
A request may mix steps and draws; the platform groups the entries by product, step and draw, commits one transaction per group in rules, tickets, seal, draw order, and reports each entry against its own transaction. A rules entry needs no draw_id (its hash is the draw id). A ticket needs draw_id and ticket_id. A seal may declare draw_not_before as unix seconds.
The result
{
"request_id": "req_574ba081205c31c1",
"status": "committed", // committed | partial | failed
"prize_draw": true,
"txn_version": "7096573681", // the request's transaction (the last chunk, if split)
"timestamp": "1788648717", // ledger time, unix seconds
"master_seed": "0x", // empty when the step mints no seed
"verify_url": "https://explorer.aptoslabs.com/txn/7096573681?network=mainnet",
"results": [
{"hash": "ce58d904…", "position": 0, "txn_version": "7096573681", "master_seed": "0x",
"commit_type": "ticket", "draw_id": "d019df73…", "ticket_id": "7"},
{"hash": "63595b83…", "position": 1, "txn_version": "7096573681", "master_seed": "0x",
"commit_type": "ticket", "draw_id": "d019df73…", "ticket_id": "8"}
],
"coordinator_ts": 1788648718
}
Each entry reports its own txn_version, position and, when the step minted one, master_seed. An entry that could not be recorded has a status and an error instead of a transaction, and deliberately no txn_version: a refused ticket can never be mistaken for a committed one. A rules, seal or draw hash the ledger already holds comes back as replayed, pointing at the original transaction. A re-sent ticket is reported as ticket_taken, and its original event is the record.
| HTTP | Meaning |
|---|---|
| 200 | The reply is the result. committed: every entry landed. partial: the committed entries are on the ledger and the rest carry per-entry statuses. failed with results: nothing landed, and each entry says why (a pre-check refusal, no gas spent). |
| 202 | Accepted for a callback: {"request_id": …, "accepted": 1, "mode": "callback", "status": "queued"}. |
| 400 | The request itself is malformed: a bad hash, an unknown field, a missing ticket_id, a step field on the wrong entry. |
| 401 / 429 | No or bad key; rate limit or quota (Retry-After is set). |
| 502 | The whole request was rejected: it did not fit the configured module (for example no draw_type), or the transaction aborted on chain. The body's error names the abort. |
| 503 | At capacity; retry shortly. |
| 504 | The synchronous wait ran out or you disconnected. The batch may still commit; reconcile from the ledger. |
Callbacks
With a callback_url the reply is an immediate 202 and the result object above is POSTed to your URL, with retries and backoff (five attempts by default) until you answer 2xx. Redirects are not followed and private addresses are refused unless the deployment allows them. Each delivery carries:
User-Agent: golden-egg-coordinator/1
X-Coordinator-Event: commit
X-Coordinator-Delivery: dl_fc78d9a5d8cdbb0d
X-Coordinator-Signature: t=1788648724,v1=8956f65cf71c368fb153b1c83df5390197bb082ab3b877570035f1d840c276b2
Verify v1 as hex HMAC-SHA256 of "<t>." + raw body under the callback secret we share with you, compare in constant time, reject deliveries whose t is more than 300 seconds old, and dedupe on the request_id inside the signed body. The delivery header is not signed and is not a dedupe key.
5. What to hash
The platform only ever sees 32-byte fingerprints, so what you hash is your choice and your evidence. These are the conventions the demos use; anyone can re-derive every one of them from public data.
| Commitment | Preimage | Why |
|---|---|---|
| Rules, game | SHA3-256 of the rules document bytes | The hash is the draw id; publishing the document lets anyone check it matches. |
| Ticket, play, spin | SHA3-256 of your entry record (for example draw id, entrant reference, a nonce) | Must be new to the ledger; the same fingerprint twice is refused or replayed. Keep the preimage so the entrant can be shown their own commitment. |
| Seal | SHA3-256(draw_id bytes ‖ 0x02) | Derivable from the draw id alone, so the seal carries state, not secrets. |
| Draw | SHA3-256(draw_id bytes ‖ 0x01) | Same reasoning. |
# the helpers used by every example below
COORD=http://127.0.0.1:8090 # the coordinator, as reached from the box
ENGINE=http://127.0.0.1:8085 # the engine (spin endpoints), loopback only
NODE=https://fullnode.mainnet.aptoslabs.com/v1
ADDR=0xd1c16712a303087b4e3211284613063c8e7c4f6d9796cffe3071e63eff02f929
MOD=golden_egg_vp1b
A="Authorization: Bearer $API_KEY"; H='Content-Type: application/json'
sha3() { openssl dgst -sha3-256 -r | cut -c1-64; }
rnd() { openssl rand -hex 32; } # stands in for the hash of one of your records
6. Prize draw, step by step
Commit the rules. The response's draw_id equals the hash you sent.
DOC="Summer raffle 2026. One prize. Tickets 1 to 100. Winner = the sealed ticket at index SHA3-256(seed) mod N. Ref: 0904c4"
RULES=$(printf '%s' "$DOC" | sha3) # d019df73e4e979e7658a942aca80c95430f292e9327f4fe38e073625da128aa7
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$RULES'","commit_type":"rules","draw_type":"prize_draw","max_tickets":100,"duration_seconds":0}]}'
# HTTP 200
{"request_id":"req_a6c55468111c2f41","status":"committed","prize_draw":true,"txn_version":"7096573628",
"timestamp":"1788648717","master_seed":"0x","verify_url":"https://explorer.aptoslabs.com/txn/7096573628?network=mainnet",
"results":[{"hash":"d019df73…","position":0,"txn_version":"7096573628","commit_type":"rules","draw_id":"d019df73…","ticket_id":"0"}]}
# On the ledger: commit_pd_rules(0xd019df73…, 100, 0) and one PD_RULES_COMMIT event
Sell tickets. Up to 500 in one request. Numbers are the entrant's choice within 1..max_tickets.
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"prize_draw","draw_id":"'$RULES'","ticket_id":7},
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"prize_draw","draw_id":"'$RULES'","ticket_id":8}]}'
# HTTP 200: status committed, txn_version 7096573681, positions 0 and 1 (shown in full in section 4)
# On the ledger: commit_pd_tickets(draw_id, [h7, h8], [7, 8]) and two PD_TICKET_COMMIT events:
# {"draw_id":"0xd019df73…","commit_hash":"0xce58d904…","ticket_number":"7","batch_position":"0"}
# {"draw_id":"0xd019df73…","commit_hash":"0x63595b83…","ticket_number":"8","batch_position":"1"}
A taken number is refused per entry; the rest still land. The engine sees the bit already set and never spends gas on it.
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"prize_draw","draw_id":"'$RULES'","ticket_id":7},
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"prize_draw","draw_id":"'$RULES'","ticket_id":9}]}'
# HTTP 200
{"request_id":"req_bd8bb1a77d6e0d03","status":"partial","txn_version":"7096573794", …
"results":[
{"hash":"93d9e847…","commit_type":"ticket","draw_id":"d019df73…","ticket_id":"7","status":"ticket_taken",
"error":"ticket 7 of draw d019df73… is already committed (its bitmap bit is set); if this was a retry, the original commit's event is the canonical record — no new seed can be minted for this ticket"},
{"hash":"107f98a6…","position":0,"txn_version":"7096573794","commit_type":"ticket","draw_id":"d019df73…","ticket_id":"9"}]}
Seal. draw_not_before of 0 allows the draw immediately after the seal. Any registered worker may seal; the closing time binds everyone.
SEAL=$(printf '%s02' "$RULES" | xxd -r -p | sha3) # SHA3-256(draw_id bytes || 0x02)
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$SEAL'","commit_type":"seal","draw_type":"prize_draw","draw_id":"'$RULES'","draw_not_before":0}]}'
# HTTP 200: status committed, txn_version 7096573929
# On the ledger: commit_pd_seal(draw_id, seal_hash, 0), PD_SEAL_COMMIT; then, unprompted, the sealing worker's
# reclaim_pages at version 7096573969 with PAGES_RECLAIMED {"page_kind":"ticket_bitmap","reclaimed_page_indexes":["0"]}
A ticket after the seal.
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"prize_draw","draw_id":"'$RULES'","ticket_id":10}]}'
# HTTP 200
{"request_id":"req_dc2cd027964ec706","status":"failed","results":[{"hash":"90800298…","commit_type":"ticket",
"draw_id":"d019df73…","ticket_id":"10","status":"draw_sealed",
"error":"draw d019df73… is sealed — its entrant set is closed; no ticket can be added"}]}
Draw. The reply carries the minted seed. Your published rule turns the seed and the sealed entry set into the winner; the ledger records the seed, not the winner.
DRAW=$(printf '%s01' "$RULES" | xxd -r -p | sha3) # SHA3-256(draw_id bytes || 0x01)
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$DRAW'","commit_type":"draw","draw_type":"prize_draw","draw_id":"'$RULES'"}]}'
# HTTP 200
{"request_id":"req_53308fce2885db05","status":"committed","txn_version":"7096574066","timestamp":"1788648720",
"master_seed":"0xac0e2fdcc9cc6756ef28a88adabc7b0e7ea0a4225061e187dc2b1f3d4becc4ac",
"verify_url":"https://explorer.aptoslabs.com/txn/7096574066?network=mainnet",
"results":[{"hash":"1506a5c2…","position":0,"txn_version":"7096574066","master_seed":"0xac0e2fdc…","commit_type":"draw","draw_id":"d019df73…","ticket_id":"0"}]}
# On the ledger: commit_pd_draw(draw_id, draw_hash) and PD_DRAW_COMMIT {…,"aptos_unpredictable_seed":"0xac0e2fdc…"}
7. Instant win
The same four calls with "draw_type": "instant_win". The difference is that a ticket batch mints one seed, and each ticket's own value is SHA3-256(master_seed ‖ position as 8 big-endian bytes). Your published rule says what that value wins; the ledger records the seed beside every ticket in the batch.
IW=$(printf '%s' "Instant win 2026. 50 plays. A play wins when the first byte of SHA3-256(seed || be8(position)) is below 64. Ref: …" | sha3)
# the run behind this page hashed its own document: 78b34c947f31aeedd3974ed537655f5fcd5d7d70a75be29cfbfffcff423b2480
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$IW'","commit_type":"rules","draw_type":"instant_win","max_tickets":50,"duration_seconds":0}]}'
# HTTP 200: committed, txn_version 7096574235 (commit_iw_rules)
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"instant_win","draw_id":"'$IW'","ticket_id":1},
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"instant_win","draw_id":"'$IW'","ticket_id":2}]}'
# HTTP 200
{"request_id":"req_85a695e138254aac","status":"committed","txn_version":"7096574296","timestamp":"1788648722",
"master_seed":"0x0a9ab849b1d95fd195eafd0bbe72aa8bbf819e97bdcfce8ec22f5ba9a7d58e6a",
"results":[{"hash":"ebbb51e5…","position":0,"txn_version":"7096574296","master_seed":"0x0a9ab849…","commit_type":"ticket","draw_id":"78b34c94…","ticket_id":"1"},
{"hash":"7f5a977c…","position":1,"txn_version":"7096574296","master_seed":"0x0a9ab849…","commit_type":"ticket","draw_id":"78b34c94…","ticket_id":"2"}]}
# On the ledger: commit_iw_tickets(draw_id, [p1, p2], [1, 2]); each IW_TICKET_COMMIT event carries the same seed.
# the value of the ticket at position 1
printf '%s0000000000000001' 0a9ab849b1d95fd195eafd0bbe72aa8bbf819e97bdcfce8ec22f5ba9a7d58e6a | xxd -r -p | sha3
# 42960eb2b73d290b… (first byte 0x42 = 66, so under the rule above this play does not win)
The same call, asynchronously. Add a callback_url: the reply is a 202 and the result arrives signed, here two seconds later.
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"callback_url":"http://127.0.0.1:8777/hook","entries":[
{"hash":"'$(rnd)'","commit_type":"ticket","draw_type":"instant_win","draw_id":"'$IW'","ticket_id":3}]}'
# HTTP 202
{"request_id":"req_cfd914b343a5d6df","accepted":1,"write_once":false,"prize_draw":true,"mode":"callback","status":"queued"}
# POST /hook, received by the listener
# X-Coordinator-Event: commit X-Coordinator-Delivery: dl_fc78d9a5d8cdbb0d
# X-Coordinator-Signature: t=1788648724,v1=8956f65cf71c368fb153b1c83df5390197bb082ab3b877570035f1d840c276b2
{"request_id":"req_cfd914b343a5d6df","status":"committed","txn_version":"7096574457","timestamp":"1788648724",
"master_seed":"0x993733f955e602abb99fc1cfa25c3e1f3f868edfb548d571b387c7beeea13a01",
"results":[{"hash":"17fb3e42…","position":0,"txn_version":"7096574457","master_seed":"0x993733f9…","commit_type":"ticket","draw_id":"78b34c94…","ticket_id":"3"}]}
Seal and draw exactly as for a prize draw, with "draw_type": "instant_win". In the run behind this page the seal landed at version 7096574574 and the draw at 7096574618 with its own seed, for a grand prize on top of the instant results.
8. Spin to win
A spin game is declared and spun through the engine's two endpoints, with the same key. Its seal and its optional draw go through /submit with "draw_type": "spin_to_win". The engine does not pre-check spins: a duplicate fingerprint or a spin after the seal is refused by the contract, so it costs a failed transaction and comes back as an HTTP error naming the abort.
Commit the game. Outcomes 1..num_prizes are the prizes. expected_spins sizes the game's spin index.
GAME=$(printf '%s' "Wheel 2026. 12 possible outcomes, 3 of them prizes (outcomes 1 to 3). Ref: …" | sha3)
# the run behind this page hashed its own document: 0f1810a126e21211e6b71e1069b7401d690f78b874391dfdb7624a651a977e56
curl -s -X POST $ENGINE/spin/game -H "$A" -H "$H" -d '{"game_hash":"'$GAME'","possible_outcomes":12,"num_prizes":3,"duration_seconds":0,"expected_spins":64}'
# HTTP 200
{"txn_version":"7096574667","timestamp":"1788648726","verify_url":"https://explorer.aptoslabs.com/txn/7096574667/events?network=mainnet",
"game_id":"0x0f1810a126e21211e6b71e1069b7401d690f78b874391dfdb7624a651a977e56","spin_count":0,"duration_ms":0}
# On the ledger: commit_stw_rules(game_hash, 12, 3, 0, 64) and STW_RULES_COMMIT
Spin. One request, many spins, one transaction, one seed. The outcome of the spin at position i is (SHA3-256(seed ‖ be8(i)) as a big-endian integer, mod possible_outcomes) + 1; the contract computes it and emits it, and anyone recomputes it.
curl -s -X POST $ENGINE/spin -H "$A" -H "$H" -d '{"game_id":"'$GAME'","hashes":["'$(rnd)'","'$(rnd)'"]}'
# HTTP 200
{"txn_version":"7096574719","timestamp":"1788648726","master_seed":"0x77d4fbdbdb0447bde410a0237328d2c2ae7586f864ad7a7f333a6ef9c9bd47fb",
"verify_url":"https://explorer.aptoslabs.com/txn/7096574719/events?network=mainnet","game_id":"0x0f1810a1…","spin_count":2,"duration_ms":668,
"results":[{"hash":"0x1dc3b364…","position":0,"outcome":11},{"hash":"0x120729a6…","position":1,"outcome":10}]}
# On the ledger: commit_stw_spins(game_id, [s1, s2]); STW_SPIN_COMMIT {…,"outcome":"11","batch_position":"0","aptos_unpredictable_seed":"0x77d4fbdb…"} and one for position 1
# recompute position 0 (python3): (int(sha3_256(seed + (0).to_bytes(8,'big')).hexdigest(),16) % 12) + 1 -> 11
The same fingerprint twice.
curl -s -X POST $ENGINE/spin -H "$A" -H "$H" -d '{"game_id":"'$GAME'","hashes":["1dc3b36482098c2906ef057b593a052670ce45900dba5281806404298ca4f9e2"]}'
# HTTP 409
{"error":"spin already committed on-chain (E_SPIN_ALREADY_COMMITTED): batch not committed (tx: 0x56b81b05…): Move abort in 0xd1c1…f929::golden_egg_vp1b: E_STW_SPIN_ALREADY_COMMITTED(0x94): "}
Seal the game, then draw a grand prize over the sealed spins.
GSEAL=$(printf '%s02' "$GAME" | xxd -r -p | sha3); GDRAW=$(printf '%s01' "$GAME" | xxd -r -p | sha3)
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$GSEAL'","commit_type":"seal","draw_type":"spin_to_win","draw_id":"'$GAME'","draw_not_before":0}]}'
# HTTP 200: committed, txn_version 7096574862 (commit_stw_seal, STW_SEAL_COMMIT); the worker then sweeps the spin index (reclaim_idem, PAGES_RECLAIMED spin_idem)
curl -s -X POST $ENGINE/spin -H "$A" -H "$H" -d '{"game_id":"'$GAME'","hashes":["'$(rnd)'"]}'
# HTTP 500
{"error":"transaction aborted E_STW_SPINS_SEALED (code 145): Move abort in 0xd1c1…f929::golden_egg_vp1b: E_STW_SPINS_SEALED(0x91): (tx: 0x155d4b56…)"}
curl -s -X POST $COORD/submit -H "$A" -H "$H" -d '{"prize_draw":true,"entries":[
{"hash":"'$GDRAW'","commit_type":"draw","draw_type":"spin_to_win","draw_id":"'$GAME'"}]}'
# HTTP 200: committed, txn_version 7096574935, master_seed 0xea0aa667… (commit_stw_draw, STW_DRAW_COMMIT)
9. Reading the record back
Everything the platform reports can be read straight from a public node, without us. Two places to look: the transaction a result points at, and the contract's view functions. Numbers typed u64 travel as strings in the node's JSON; u8 arguments, such as a step code, are bare numbers.
# the transaction behind a result: the function it called, its arguments, and its events
curl -s $NODE/transactions/by_version/7096573681 | jq '{function: .payload.function, arguments: .payload.arguments, events: [.events[] | select(.type|test("golden_egg_vp1b")) | {type, data}]}'
# a draw's state: [rules committed, sealed, max_tickets, product (1 prize draw, 2 instant win, 3 spin game)]
curl -s -X POST $NODE/view -H "$H" -d '{"function":"'$ADDR'::'$MOD'::draw_stats","type_arguments":[],"arguments":["0x'$RULES'"]}'
# [true,true,"100",1]
# its declaration: [exists, product, max_tickets, duration_seconds, opened_at]
curl -s -X POST $NODE/view -H "$H" -d '{"function":"'$ADDR'::'$MOD'::get_draw","type_arguments":[],"arguments":["0x'$RULES'"]}'
# [true,1,"100","0","1788648717"]
# where a step lives: [exists, hash, worker, worker sequence number, position]. Step codes: 1 rules, 2 ticket, 3 draw, 5 seal.
curl -s -X POST $NODE/view -H "$H" -d '{"function":"'$ADDR'::'$MOD'::get_step_pointer","type_arguments":[],"arguments":["0x'$RULES'",5,"0"]}'
# [true,"0x6346afbd…","0xdbc5e67e…","778","0"]
# -> the seal is that worker's transaction 778:
curl -s "$NODE/accounts/0xdbc5e67e4121b2d4c0eb7a0ceb85f0c768621a4852e7cb853482b4499b729200/transactions?start=778&limit=2"
# a game's locked odds: [possible_outcomes, num_prizes, duration_seconds, opened_at, expected_spins]
curl -s -X POST $NODE/view -H "$H" -d '{"function":"'$ADDR'::'$MOD'::get_game","type_arguments":[],"arguments":["0x'$GAME'"]}'
# ["12","3","0","1788648726","64"]
# the earliest draw time a seal declared: [sealed, unix seconds]
curl -s -X POST $NODE/view -H "$H" -d '{"function":"'$ADDR'::'$MOD'::draw_not_before","type_arguments":[],"arguments":["0x'$RULES'"]}'
# [true,"0"]
Occupancy views are for open draws. is_ticket_taken, are_tickets_taken and is_spin_committed read the live bitmap and spin index. Once a draw is sealed that storage is refunded, so they read false for every number, as they did for tickets 7, 8 and 9 above right after the seal. The permanent record of who holds what is the events, and the sealed set is exactly the ticket events between the rules transaction and the seal transaction.
# every ticket event of a draw, from the module's event stream on a public node, or from your own results:
# PD_TICKET_COMMIT events carry {draw_id, commit_hash, ticket_number, batch_position}; the draw's seed is on its PD_DRAW_COMMIT event.
# the code that accepted every record is published as permanent: upgrade_policy 2 in the address's package registry
curl -s $NODE/accounts/$ADDR/resource/0x1::code::PackageRegistry | jq '.data.packages[] | select(.name=="goldeneggvp1b") | {name, upgrade_policy}'
Appendix: the demo's own endpoints
The dashboards on this site are a thin front end over a small JSON API that calls the platform with the demo's own key. Anyone can use it without a key, within the demo's limits: one write at a time per session, 1,200 calls per hour per address, eight draws per product per session, 250 tickets per draw and 25 per purchase, 20 spins per game, and sessions expire after four hours of inactivity. Base URL https://draws.integrimark.co.uk; every call after the first carries the session in an X-Demo-Session header. Each demo call maps onto one of the platform calls above.
curl -s -X POST https://draws.integrimark.co.uk/api/session # {"session":"7f3a…",…}
S="X-Demo-Session: $SESSION"; D=https://draws.integrimark.co.uk
# prize draw or instant win: chapter is "prize_draw" or "instant_win"; supply is 1..250; closing_seconds 0 = until sealed
curl -s -X POST $D/api/draw -H "$H" -H "$S" -d '{"chapter":"prize_draw","rules_text":"Summer raffle. Tickets 1 to 25. Ref: a1b2c3","supply":25,"closing_seconds":0}'
curl -s -X POST $D/api/ticket -H "$H" -H "$S" -d '{"chapter":"prize_draw","draw_id":"'$DRAW_ID'","tickets":[{"hash":"'$(rnd)'","number":1},{"hash":"'$(rnd)'","number":2}]}'
curl -s -X POST $D/api/seal -H "$H" -H "$S" -d '{"chapter":"prize_draw","draw_id":"'$DRAW_ID'"}'
curl -s -X POST $D/api/close -H "$H" -H "$S" -d '{"chapter":"prize_draw","draw_id":"'$DRAW_ID'"}' # the draw; needs the seal and two entries
curl -s -X POST $D/api/duplicate -H "$H" -H "$S" -d '{"hash":"'$(rnd)'"}' # re-buys a taken number on the newest prize draw
curl -s -X POST $D/api/late -H "$H" -H "$S" -d '{"hash":"'$(rnd)'"}' # a ticket on the newest sealed prize draw
# spin to win: the document must contain "Possible outcomes:" and "Winning outcomes:" lines
curl -s -X POST $D/api/game -H "$H" -H "$S" -d '{"rules_text":"SPIN GAME\nPossible outcomes: 20\nWinning outcomes: 5\nReference: g7h8i9"}'
curl -s -X POST $D/api/spin -H "$H" -H "$S" -d '{"hash":"'$(rnd)'","draw_id":"'$GAME_ID'"}' # one spin per call
curl -s -X POST $D/api/seal -H "$H" -H "$S" -d '{"chapter":"spin_to_win","draw_id":"'$GAME_ID'"}'
curl -s -X POST $D/api/close -H "$H" -H "$S" -d '{"chapter":"spin_to_win","draw_id":"'$GAME_ID'"}'
curl -s $D/api/state -H "$S" # every receipt and draw in the session
curl -s $D/api/meta # the contract address and module the demo is wired to
Every receipt carries the transaction version and an explorer link, and the dashboard's More info re-checks each one in your browser against fullnode.mainnet.aptoslabs.com. Our servers take no part in that check.