Vettaidocs
Under the hood

Architecture

Two processes, one database, one chain. Which piece runs where, and which one can move money.

Vettai is two always-on processes built from one Docker image, sharing one Postgres database, plus a Next.js app on the edge and the Nimiq chain underneath.

The system, drawn

The two processes

world. The HTTP API and the WebSocket. It runs the rooms, the 20 Hz tick, the quest engine, the ladder endpoint and the claim API. It never holds a private key, and it refuses to boot if it finds TREASURY_PRIVATE_KEY in its environment at all.

treasury. Delivers queued claims as NIM payments, watches the treasury address for shop payments, reads stakes for the landlord quest, and pays the weekly ladder. It is the only process with the key.

One image runs both. The host picks by setting VETTAI_PROCESS=src/treasury/index.ts; unset, the image runs the world. Locally the treasury reads its key from a separate .env.treasury file layered on top of .env.

The database

Money amounts are integers in luna, never floats. One NIM is 100,000 luna.

TableWhat it holds
playersaddress (primary), public key, created and last seen, last IP hash, gear as JSON, landlord since
sessionstoken hash (primary), address, created, expires after 30 days
challengesnonce (primary), kind (login, claim or shop), subject, expiry, used at. Single use, ten minutes
questsid, address, UTC day, kind, target, progress, state, reward in luna, detail JSON, created and done. Unique on address, day and kind
claimsid, address, quest id, kind, amount in luna, state, memo, transaction hash (unique), block number, IP hash, attempts, held until, timestamps, error. At most one claim per quest
shop_ordersid, address, item, price in luna, memo (unique), state, transaction hash (unique), block number, created, paid, announced
received_paymentsevery incoming transaction the watcher inspected: hash (primary), sender, recipient, value, memo, block, block time, order id, outcome, seen at
ladder_periodsperiod (primary, for example 2026-W41), paid at, the claim ids
stats_dailyday (primary), players, kills, luna paid
watch_cursoraddress (primary), last block number, updated at

The module graph

The arrows only ever point downward. The play simulation knows nothing about the database, the domain knows nothing about HTTP, and the treasury is the only thing that reaches the signer.

Identity

Login never carries an address. The server hands out vettai-login:<nonce>:<expiry>, the client signs it with the wallet provider, and the server rebuilds the Nimiq signed-message hash, verifies Ed25519 against the public key, derives the address from that key, spends the nonce and returns a session token of the shape vt1.<random>, stored only as a hash.

Configuration

Every environment key is validated with zod at boot, in one file, and a missing or bad key fails loudly. Nothing reads an environment variable anywhere else. The full list is in Running locally and Deploying.