Choose a money model
This is the decision that shapes your whole backend. IAMGame Wallet gives you three server-side ways to move value, and every game picks one as its core plus, usually, a second for the cash boundary. They all live on the same server SDK (@iamgame/wallet-sdk-server), all use your sk_ key, and all speak the same money rules: integer base units, idempotency keys, no floats.
The wallet is always the money source of truth. Your game owns the meaning — the odds, the rounds, who won — and joins its own records to the wallet on a shared reference id. Pick the model by asking one question: where does the friction of an on-chain transaction actually need to happen?
The three models at a glance
| Model | SDK surface | Use it for | On-chain when |
|---|---|---|---|
| Ledger | server.ledger.* | High-frequency per-player play (slots, crash, per-tick betting) | Once, at settle — the net result of the whole session |
| Custodial treasury | server.treasury.* | Real-money balances players top up and cash out | Every deposit and every withdraw (the cash boundary) |
| Pool-escrow custody | server.custody.* | Pari-mutuel pools — everyone stakes, winners split the pot | Per stake (lock) and once per pool (settle/refund) |
1. Ledger — speed
Signing an on-chain transaction for every game action is too slow and too expensive for real-time play. The ledger fixes that with a seamless-wallet model: open a session with one lock, then run bet / win / rollback off-chain at memory speed, and settle the net once. Nothing hits the chain between the lock and the settle.
- Best for: anything with a hot loop — slots, crash/multiplier, per-tick betting, arcade rounds.
- Trade-off: holds per-player money, not a shared pot. The pool math (odds, who won) is entirely your game’s job — you compute each winner’s payout and call
winagainst their bet. - Guarantee: funds locked in an open session are untouchable by withdraw or export — a player can’t pull money out from under an active bet.
Deep dive: Ledger.
2. Custodial treasury — the cash boundary
The treasury is the bridge between a player’s real on-chain wallet and your game treasury. deposit pulls real tokens in (an implicit top-up); withdraw sends them back out (an explicit cash-out). Each move is a real on-chain transfer, tracked to a confirmed signature and reconcilable by an idempotency ref. In between, per-minute play stays off-chain in your game’s own balances.
- Best for: real-money games where players hold a spendable balance that they load and cash out — the entry and exit points of the money.
- Trade-off: deposits and withdrawals are genuine on-chain transactions — they confirm on chain time, not instantly. You reconcile them with a
statusstate machine (none/orphan/dead/stuck/confirmed). - Guarantee: crash-safe and idempotent on
ref— a retry resolves the recorded signature against the chain instead of moving fresh money. Exactly one on-chain transfer perref.
Deep dive: Custodial treasury.
3. Pool-escrow custody — shared pots
When the money that’s at stake is a shared pool — everyone stakes into it, the outcome resolves, winners split it — use pool custody. Each bet’s stake is lockStaked into an on-chain escrow keyed by your pool id. When the outcome is known, one settlePool call atomically fans out the rake and every payout, guarded by a conservation check. If the pool is voided, refundPool returns every stake.
- Best for: pari-mutuel and prize-pool games — the pot is the product (sports pools, bracket games, jackpots).
- Anchored on-chain: each pool is a real Solana asset lock (a PDA the signer controls). Stakes are escrowed on-chain and settled atomically — the pot is guaranteed by the Solana runtime, not just a database row. Your game still runs at UI speed (no per-bet transaction); the money is persisted on-chain. This is the “fast off-chain play, on-chain certainty” model.
- Trade-off: heavier than the ledger — real escrow, per-stake locks. Not for a hot loop; for outcomes that resolve on a schedule.
- Guarantee: settle/refund independently re-check that
Σpayouts + rake == Σlockedand fail closed — the pool never moves a cent when the books don’t balance. Idempotent onbetId/poolId.
Deep dive: Pool-escrow custody.
How they compose
These aren’t either/or. A typical real-money game uses more than one, each at the layer where it belongs:
- Treasury at the edges — the player deposits real USDC in and withdraws it out.
- Ledger (or custody) in the middle — fast per-player play, or a shared pool, funded from the balance the treasury holds.
- Referrals on top of the rake — when a round takes house rake, you split a cut up the bettor’s network with
server.referrals.distribute. The game decides the shares; the wallet records the credits.
transactionUuid, ref, betId/poolId, idemKey). Use your own id — your bet id, your pool id — as that key. It doubles as the reconciliation join between your game records and the wallet’s money-truth. A retry with the same key never double-moves money.import { IAMGameWalletServer } from "@iamgame/wallet-sdk-server";
const wallet = new IAMGameWalletServer({
secretKey: process.env.IAMGAME_WALLET_SECRET_KEY!, // sk_ — BACKEND ONLY
baseUrl: "https://api-wallet.iamgame.com/v1",
});
// Composition sketch: treasury at the edge, ledger in the middle, referrals on the rake.
const userId = "usr_abc"; // resolve from wallet.verifySession(sessionToken)
const currency = 3; // ENUMGPCurrency: USDC
// 1) Cash boundary — top the player up (idempotent on your ref).
await wallet.treasury.deposit({ userId, currency, amount: 50_000_000n, ref: "topup:2026-07-13:usr_abc:1" });
// 2) Fast play — one lock, then bet/win off-chain, settle the net.
const s = await wallet.ledger.openSession({ userId, gameCode: "momentum", currency: "USDC", lockAmount: 50_000_000n });
await wallet.ledger.bet({ sessionToken: s.sessionToken, transactionUuid: "bet:r1", amount: 1_000_000n, round: "1" });
await wallet.ledger.win({ sessionToken: s.sessionToken, transactionUuid: "win:r1", referenceTransactionUuid: "bet:r1", amount: 1_800_000n, round: "1", roundClosed: true });
const settled = await wallet.ledger.settleSession(s.sessionToken);
// 3) On the house rake, pay a cut up the referral chain (game chooses the split).
const rake = 40_000n;
await wallet.referrals.distribute({
userId, totalAmount: rake * 200n / 10_000n, currency, // 2% of rake
levels: [{ level: 1, bps: 6000 }, { level: 2, bps: 3000 }, { level: 3, bps: 1000 }],
sourceRef: `round:${settled.sessionId}`,
idemKey: `refdist:${settled.sessionId}`, // retry-safe
});Decision guide
| If your game is… | Use… | Plus, for cash in/out… |
|---|---|---|
| A fast single-player loop (slots, crash, arcade) | Ledger | Treasury |
| Per-tick / real-time betting against the house | Ledger | Treasury |
| A shared prize pool everyone stakes into (pari-mutuel, brackets, jackpots) | Pool-escrow custody | Treasury |
| Just holding a real-money balance players load and cash out | Custodial treasury alone | — |
| Free-to-play / off-chain scores only | None — you don’t need to move real value | — |
Rule of thumb: ledger when speed is the constraint, custody when a shared pot is the product, treasury whenever real money enters or leaves. Most real-money games are ledger-or-custody in the middle with treasury at the edges.
What every model shares
- Server-to-server only. These surfaces use your
sk_key and must run on your backend. The browser can never callwin,settle, orwithdrawon itself — the client SDK has no money-moving surface by design. - Integer money. Every amount is a
bigintin the token’s smallest unit (lamports = 109, micro-USDC = 106). Never a float. - Idempotency. Every mutating call takes a key you own. Retries are safe; they replay, they don’t double-move.
- Scope & environment. Your key resolves the app + environment (test/devnet vs live/mainnet) and its wallet scope. A key can only move money for its own app’s users.
Where to go next
- Ledger — lock once, play fast, settle the net.
- Custodial treasury — deposits, withdrawals, and the reconciliation model.
- Pool-escrow custody — a full pari-mutuel pool lifecycle.
- API reference — the raw
/v1surface behind the SDK.