Skip to main content

Treasury

A treasury account is a set of buckets — Operating, Reserve, Yield — where each bucket is its own Safe multisig. You give every bucket a target share of the account's total USDC (in basis points), then attach rules that keep the buckets on target by moving USDC between them.

Two properties to understand before anything else:

  1. The platform never holds your keys. A rule that fires — or a manual sweep — only proposes a payment between bucket Safes. It executes when an owner signs it through the normal payments rails (a 1-of-1 sandbox Safe completes in a single sign call).
  2. Yield is simulated. Sandbox yield accrues daily at your configured APY and is always labeled simulated — it's display-only accounting and never moves funds. Real yield adapters come later behind the same shape.

Treasury is sandbox-only for now.

Set up an account

One Safe is created per bucket (202 — the CREATE2 addresses are usable immediately; deployment is async).

curl -X POST https://api.sandbox.safebank.ai/v1/treasury/accounts \
-H "X-SafeBank-Api-Key: $SAFEBANK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Treasury",
"owners": [{ "address": "0xYourOwnerAddress" }],
"buckets": [
{ "name": "Operating", "kind": "OPERATING", "targetBps": 6000 },
{ "name": "Reserve", "kind": "RESERVE", "targetBps": 3000 },
{ "name": "Yield", "kind": "YIELD", "targetBps": 1000, "apyBps": 450 }
]
}'

Fund a bucket like any wallet (sb wallets fund <walletId> --usdc 1000 in the sandbox), then watch it:

sb treasury status <accountId>

The detail read reports each bucket's live balance, its drift from target, and accrued simulated yield. A bucket whose balance can't be read right now (RPC hiccup, Safe still deploying) reports null — never zero — and drift is suppressed until every bucket is readable.

Rules

TypeWhat it does
SWEEP_EXCESSDrain source above thresholdUsd into dest.
TOP_UPRefill dest back to targetUsd from source when it dips below the trigger (thresholdUsd, defaulting to targetUsd).
REBALANCE_TO_TARGETMove funds from the most-over-target bucket to the most-under-target one (per bucket targetBps), one transfer per fire.
curl -X POST https://api.sandbox.safebank.ai/v1/treasury/accounts/$ACCT/rules \
-H "X-SafeBank-Api-Key: $SAFEBANK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "SWEEP_EXCESS", "sourceBucketId": "'$OPS'", "destBucketId": "'$RESERVE'", "thresholdUsd": "1000.00" }'

The rule runner evaluates enabled rules on an interval (min. spacing per rule via intervalSeconds, default 3600). When a rule's condition holds it proposes a sweep — an audited TreasurySweep row linked to a Payment. Nothing moves until an owner signs.

Balance reads fail safe

The runner treats an unreadable balance as skip this window, never as $0 — a dead RPC can't trigger a drain.

Sweeps: propose, then sign

# propose manually (or let a rule fire)
sb treasury sweep <accountId> --from <bucketId> --to <bucketId> --amount 250 --sign

# see everything, including rule-fired sweeps awaiting signature
sb treasury sweeps <accountId>

# sign a pending sweep's payment with your demo signer
sb treasury sign <accountId> <sweepId>

--sign completes the payment with your local demo signer — the same raw-secp256k1 signSafeTxHash flow as sb pay. In production your owners sign with their own wallets; the platform can't move treasury funds unilaterally.

Key endpoints

MethodPathScope
POST/v1/treasury/accountstreasury:write
GET/v1/treasury/accountstreasury:read
GET/v1/treasury/accounts/{id}treasury:read
POST/v1/treasury/accounts/{id}/rulestreasury:write
GET/v1/treasury/accounts/{id}/rulestreasury:read
PATCH/v1/treasury/rules/{ruleId}treasury:write
DELETE/v1/treasury/rules/{ruleId}treasury:write
POST/v1/treasury/accounts/{id}/sweepstreasury:write
GET/v1/treasury/accounts/{id}/sweepstreasury:read

See the REST reference for full request/response schemas.