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

ModelSDK surfaceUse it forOn-chain when
Ledgerserver.ledger.*High-frequency per-player play (slots, crash, per-tick betting)Once, at settle — the net result of the whole session
Custodial treasuryserver.treasury.*Real-money balances players top up and cash outEvery deposit and every withdraw (the cash boundary)
Pool-escrow custodyserver.custody.*Pari-mutuel pools — everyone stakes, winners split the potPer 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 win against 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 status state 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 per ref.

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 == Σlocked and fail closed — the pool never moves a cent when the books don’t balance. Idempotent on betId / 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.
One reference id ties it together
Every model is idempotent on a key you choose (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.
TS
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)LedgerTreasury
Per-tick / real-time betting against the houseLedgerTreasury
A shared prize pool everyone stakes into (pari-mutuel, brackets, jackpots)Pool-escrow custodyTreasury
Just holding a real-money balance players load and cash outCustodial treasury alone
Free-to-play / off-chain scores onlyNone — 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 call win, settle, or withdraw on itself — the client SDK has no money-moving surface by design.
  • Integer money. Every amount is a bigint in 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