Treasury and self-custody
Onboard your own entity, receive into your own accounts, and hold the result in wallets only your Recovery Key controls.
You are your own customer, so the funds the custody model protects are yours.
The shape
Build order
| Step | Call | Notes |
|---|---|---|
| 1. Onboard your entity | POST /api/customers with kind: "business" | Your operating company, verified through KYB like any other business customer |
| 2. Register your keys | Dashboard, Wallets | Public halves only, once per environment, and never a private key |
| 3. Activate | Dashboard, Wallets | Signed by the cold Recovery Key. Happens once, ever |
| 4. Create wallets | POST /api/customers/{customer_id}/wallets | One per chain, or several per chain if you want separated balances |
| 5. Receive | POST /api/customers/{customer_id}/virtual_accounts | Destination is a wallet you hold, or an address you already control |
| 6. Move | POST /api/wallets/{wallet_id}/operations, then authorize | Verified and signed in your KMS |
Read the custody model before you generate a key
Wallet addresses derive from your Recovery Key. It is your permanent identity, it cannot be rotated, and losing it means no new Primary Signing Key can ever be installed. For what each of your two keys does and where it belongs, see Wallets.
Generate the Recovery Key offline, in the device it will live in, and back it up like a root of trust. Read Custody Architecture first, and treat that generation as a change-controlled event with more than one person present.
What the model guarantees
If our API, our database, our infrastructure keys, or any third party we rely on were fully compromised, an attacker still could not move a token out of one of these wallets. Moving funds requires a fresh signature from your Primary Signing Key, over bytes your own verifier has already rebuilt and checked.
The threat model table states what each compromise scenario can and cannot do, and you can check every claim in it against a public chain or with code you run.
Separating balances
Each POST /api/customers/{customer_id}/wallets call mints a new wallet with a new
address, including on a chain you already have one on. Use that to separate
concerns rather than tracking sub-balances yourself:
- an operating wallet that funds day-to-day payouts,
- a reserve wallet nothing automated can reach,
- a per-counterparty wallet, so an address identifies who paid.
partner_reference_id is your handle on each one, and list endpoints
filter by it. name is a display label and is deliberately not unique.
Operational discipline
- Alert on every verifier refusal. A refusal means something did not match, which makes it the highest-signal alert you have.
- Watch
wallet_transaction.created(withkind: "deposit"). You want to know within seconds when funds arrive at a treasury address from an unexpected source. - Use
read_onlyAPI keys for reporting jobs and anything that reads balances. A read-only key cannot create an operation. - Reconcile balances from the chain.
GET /api/wallets/{wallet_id}andGET /api/wallets/total_balancesread balances from the chain, so they match what an explorer shows.GET /api/wallets/{wallet_id}/historyis the activity log beneath them.