Treasury
A double-entry virtual economy engine: developer-defined currencies, wallets for users, orgs, or arbitrary entities, atomic multi-leg transfers, holds for escrow, faucet/sink supply control, and economy analytics. It knows amounts and accounts, never why - the "why" lives in the calling service. Every example on this page was executed against a live server.
When to reach for it: game currencies with quest payouts and shop sinks, marketplace escrow, creator tips split atomically across wallets, API-credit allowances with holds during long jobs, internal scrip.
When not to: no real money exists here - Treasury moves integers between wallets and never touches payment processors. Usage metering and spend budgets belong to Meter; points programs with campaigns and reward catalogs belong to Loyalty.
Concepts
- Domains partition economies: everything lives in exactly one
{domain}path segment, and nothing crosses domains. - Currencies are developer-defined and admin-created: name, symbol,
display decimals, an overdraft policy (
DENYby default, orALLOW_TO_LIMIT/UNLIMITED), optional supply controls. Amounts are always integers. - Wallets hold one currency for one principal (
auth_user,org, orentity). Balance splits intobalance,held, andavailable(balance minus held); debits checkavailable. - Transfers are atomic sets of debit and credit legs that must net to zero per currency - all legs commit or none do.
- Holds reserve funds without spending them; capture consumes them, release or expiry frees them.
- Faucets and sinks are the only way value enters or leaves a currency: a faucet wallet mints by going negative, a sink burns what it receives. Both are admin-created and feed the analytics.
- Idempotency keys are mandatory on transfers and settles: a retry
with the same key replays the stored result (
idempotent_replay: true, HTTP 200 instead of 201) instead of moving money twice.
Currencies and wallets
Creating a currency requires a platform admin token (or the treasury
role):
snug treasury currencies create -d docs4-econ -i cur_gold -n Gold -s G \
--decimals 0 --max-supply 1000000 --faucets-require-tag
snug treasury currencies list -d docs4-econ # any authenticated userAny user can create a wallet for their own auth_user principal; org
and entity principals, and the faucet/sink kinds, are admin-only (both
verified to fail with 403 insufficient_permissions otherwise):
snug treasury wallets create -d docs4-econ -c cur_gold -p auth_user:docs-wave
snug treasury wallets create -d docs4-econ -c cur_gold -p entity:mint -k faucet # adminWallet ids are deterministic in (domain, principal, currency, kind):
creating the same wallet twice returns the existing one, balance intact.
wallets list defaults to the caller's own principal; reading or listing
another principal's wallets is 403 insufficient_permissions. Admins can
also freeze a wallet (wallets freeze --frozen true): debits from it
then fail with 409 wallet_frozen even for its owner (verified) until it
is unfrozen.
Transfers
A transfer is N debit legs plus M credit legs. Wallets start at 0 - value first enters through a faucet (Supply and analytics). After minting 5000 gold, one user pays another:
BUYER=wal_e625a4f3362f057632597e8fb5e52c2f # ids from wallets create
SELLER=wal_51eb705df009f3d78fc1c749bb2490e2
snug treasury transfer -d docs4-econ -k docs4-sale-1 \
--debit $BUYER:1200 --credit $SELLER:1200 \
--tag marketplace_sale --ref handshake:deal_042Verified on both ledgers: a DEBIT 1200 in the buyer's history and a
CREDIT 1200 in the seller's, pointing at the same transfer_id. A
non-admin may only debit wallets they control (403 otherwise) but may
credit any wallet.
Over HTTP the same operation is POST /api/v1/treasury/{domain}/transfers:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"idempotency_key":"docs4-http-1","legs":[
{"wallet_id":"wal_e625...c2f","direction":"DEBIT","amount":25},
{"wallet_id":"wal_51eb...0e2","direction":"CREDIT","amount":25}],
"tags":["marketplace_sale"]}' \
http://localhost:4000/api/v1/treasury/docs4-econ/transfers{
"status": 201,
"msg": "Created",
"data": {
"id": "txn_bec13dcfc7414166935019b8f8ba5627",
"legs": [
{ "wallet_id": "wal_e625...c2f", "direction": "DEBIT", "amount": 25 },
{ "wallet_id": "wal_51eb...0e2", "direction": "CREDIT", "amount": 25 }
],
"tags": ["marketplace_sale"],
"status": "COMMITTED",
"created_at": "2026-08-28T12:50:26.901868Z",
"idempotent_replay": false
}
}Refusals, each reproduced live:
- Legs that do not net to zero per currency:
400 unbalanced_transfer. - A zero or negative leg amount:
400validation error (legs[0].amount: lower than 1) - there are no zero-value transfers. - Debiting past
availableunder the defaultDENYoverdraft policy:409 insufficient_funds, with the available balance in the message. - Repeating an idempotency key is not a refusal: the original committed
transfer comes back with
idempotent_replay: trueand the same id.
Holds and settle
A hold reserves funds without spending them - the escrow primitive behind bids, checkout, and long-running jobs:
snug treasury holds place -d docs4-econ -w $BUYER -a 1000 --ref auction:bid_007
snug treasury wallets get -d docs4-econ --wallet-id $BUYER
# balance: 3800, held: 1000, available: 2800Held funds still count in balance but not available, so a transfer
that would dip into them fails with 409 insufficient_funds. Three ways
out of a hold, all verified:
- Release (
holds release) frees the reservation; the hold becomesRELEASED, and acting on it again is409 hold_not_active. - Capture (
holds capture) consumes the funds: the wallet is debited and the value leaves circulation as a burn (ledger taghold_capture). Capture may be partial - capturing 700 of a 1000 hold debits 700 and releases the remaining 300. - Expiry: a hold placed with
--expires-inauto-releases asEXPIRED. The sweeper runs about once a minute, so a hold can outliveexpires_atby up to that long (verified: a 2-second hold was still capturable at 5 seconds, gone at 75).
settle captures several holds and applies extra legs - which must
balance among themselves - as one atomic, idempotent transfer; the
response is the committed transfer plus a captured_holds array:
snug treasury settle -d docs4-econ -k docs4-settle-1 \
--hold hold_8f10...a2c --hold hold_c483...7f6 \
--debit $BUYER:50 --credit wal_0a3d...013:50 --tag escrow_settleWallet history
Every leg lands in the wallet's ledger with a running balance_after.
Entries return oldest first; page with next_cursor:
snug --output json treasury wallets history -d docs4-econ \
--wallet-id $BUYER --limit 1{
"entries": [
{
"transfer_id": "txn_cbccc515ed914ba2849cced6ad119690",
"direction": "CREDIT",
"amount": 5000,
"balance_after": 5000,
"tags": ["quest_reward"],
"created_at": "2026-08-28T12:47:10.033618Z"
}
],
"next_cursor": "1787921230033-0",
"wallet_id": "wal_e625...c2f"
}Pass --cursor 1787921230033-0 to continue (verified: the next page
picks up at the following entry); a null next_cursor is the end.
Supply, faucets, and analytics
How value is minted and burned, supply caps, and the admin analytics (supply counters, per-tag flow reports, top holders) have their own page: Supply and analytics.
Limits and configuration
Transfers take up to 16 legs and 16 tags, history pages cap at 200
entries with the per-wallet stream trimmed to the newest 100000, and
idempotency keys replay for 24 hours by default - all tunable via the
TREASURY_* variables in the
Treasury CONFIG reference.
Reference
- Treasury API - every endpoint, callable
- Related: Meter for usage metering and budgets, Loyalty for points programs with rewards, Auction and Guild for services that pair naturally with wallets and holds