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:
- 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).
- 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
- TypeScript
- CLI
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 }
]
}'
const account = await sb.treasury.createAccount({
name: 'Acme Treasury',
owners: [{ address: signer.address }],
buckets: [
{ name: 'Operating', kind: 'OPERATING', targetBps: 6000 },
{ name: 'Reserve', kind: 'RESERVE', targetBps: 3000 },
{ name: 'Yield', kind: 'YIELD', targetBps: 1000, apyBps: 450 },
],
})
sb treasury setup --name "Acme Treasury" \
--bucket Operating:operating:6000 \
--bucket Reserve:reserve:3000 \
--bucket Yield:yield:1000: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
| Type | What it does |
|---|---|
SWEEP_EXCESS | Drain source above thresholdUsd into dest. |
TOP_UP | Refill dest back to targetUsd from source when it dips below the trigger (thresholdUsd, defaulting to targetUsd). |
REBALANCE_TO_TARGET | Move funds from the most-over-target bucket to the most-under-target one (per bucket targetBps), one transfer per fire. |
- curl
- TypeScript
- CLI
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" }'
await sb.treasury.createRule(account.accountId, {
type: 'SWEEP_EXCESS',
sourceBucketId: ops.bucketId,
destBucketId: reserve.bucketId,
thresholdUsd: '1000.00',
})
sb treasury rules add <accountId> --type sweep_excess \
--source <opsBucketId> --dest <reserveBucketId> --threshold 1000
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.
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
| Method | Path | Scope |
|---|---|---|
POST | /v1/treasury/accounts | treasury:write |
GET | /v1/treasury/accounts | treasury:read |
GET | /v1/treasury/accounts/{id} | treasury:read |
POST | /v1/treasury/accounts/{id}/rules | treasury:write |
GET | /v1/treasury/accounts/{id}/rules | treasury:read |
PATCH | /v1/treasury/rules/{ruleId} | treasury:write |
DELETE | /v1/treasury/rules/{ruleId} | treasury:write |
POST | /v1/treasury/accounts/{id}/sweeps | treasury:write |
GET | /v1/treasury/accounts/{id}/sweeps | treasury:read |
See the REST reference for full request/response schemas.