Everything a tool needs to read Zyft boards or verify a round on its own. No API key, no account: JSON over HTTPS and a Solana wallet for writes.
For developers
Start here
All game rules live in one pure module shared by the browser and the server (src/lib/arena/games.ts in the source). Given a seed, a rule variant and an input log, it returns the same score everywhere. Reading is open; writing needs a wallet signature over an exact message.
- Reads are public. Boards and live chain numbers are plain GET requests.
- Writes are signed. Each one carries a one-time nonce, the wallet address and an ed25519 signature over a message the server rebuilds.
- No custody, no transactions. Nothing in the API moves funds or touches a token account.
Public reads
GET /api/board?game=reflex|aim|echo[&room=<room id>][&address=<wallet>]
→ { mode, entries: Entry[], mine: Entry | null, card: Card | null }
GET /api/chain
→ { sol: { usd, change24h, source }, network: { slot, tps, userTps, epoch, ... },
token: {...} | null, market: {...} | null }mode is hosted, local or off; off means the deployment has no storage and boards are empty by design. A room id is <game>-<variant>-<seed>.
Ticket
POST /api/round?address=<wallet>&game=<id>&variant=standard|long|hard[&room=<room id>]
→ { seed, issuedAt, game, variant, room, mac, mode }- The MAC covers seed, issue time, wallet, game, variant and room. Change any of them and the post is refused.
- Open rounds get a fresh random seed; room rounds reuse the room seed. Open boards accept the standard variant only.
- A ticket is valid for 20 minutes and can be posted once. Each network can request 60 tickets per 10 minutes.
Input log
Every entry is [t, a, b] with integers only and t in milliseconds since the round started, strictly increasing. At most 600 entries.
| GAME | a | b | NOTES |
|---|---|---|---|
| reflex | round index | 0 tap, 1 miss | Exactly one entry per round. A miss is logged at signal + 2000 ms. The next round starts 1000 ms after the entry. |
| aim | x (0–1000) | y (0–600) | Clicks after 25s (standard) or closer than 60 ms are refused. |
| echo | tile 0–8, or −1 | level | −1 is a timeout, logged at pattern end + 15000 ms. The next level starts 800 ms after the solving tap. |
Replay rules
- The seed feeds a small deterministic generator (mulberry32 over an FNV-1a hash of a label and the seed). Waits, targets and patterns all come from it.
- The server refuses a post when the replay errors, the replayed score differs from the claimed one, the round is shorter than the game allows, or it claims more time than has passed since the ticket.
- Badges are recomputed from the replayed facts; a client cannot add one.
Signed writes
GET /api/nonce?address=<wallet> → { nonce, issuedAt, domain }
POST /api/board
{ action: "score" | "card" | "remove", address, payload, nonce, issuedAt, signature (base58) }The signed text is built from the domain, a plain-words summary, the wallet, Action, the SHA-256 of the canonical JSON payload, the nonce and the issue time. For a score the payload is { seed, issuedAt, mac, game, variant, room, score, inputs }; for a card { card: { tag, color, motto } }; for remove {}.
Limits
- Bodies are read with a hard cap of 48 kB; larger requests get 413.
- Every route is rate limited per network. Writes also count against per-wallet and site-wide daily caps.
- A nonce is only issued to a wallet that holds a small SOL balance, read live. Nothing is spent.
Things that bite
- Times must be integers and strictly increasing. Two inputs in the same millisecond break the log; add one millisecond to the later one.
- A ticket names its room. A room ticket cannot be posted to an open board, and the other way round.
- Signatures expire with their nonce after ten minutes and work once.