Deposits
Register an address for a customer account, and the engine credits what arrives at it — exactly once, even if the chain is re-scanned, the process restarts, or the block that carried it is later orphaned.
Current status
npm run e2e:local-chain exercises all of it. It has not been run on a public network yet, and only account-model (EVM) networks are supported.Getting an address
One address per customer account, per network. The engine derives it from your operator seed, records the derivation index and starts watching it. You never supply an address: one the engine was only told about is one it might not be able to move money out of.
const issued = await payments.deposits.issueAddress({ accountRef: 'account_4471', // your account identifier chainId: 'eth_devnet' // from GET /v1/assets }); // → 201 { addressId, address, chainId, accountRef, assets: [...] } // Ask again for the same account: 200, the same address.
| Field | Notes |
|---|---|
addressId | Your identifier for this registration. Reusing one is how you make registration idempotent. |
chainId | Must be a chain the gateway is configured for. |
address | The on-chain address. Refused if it is already watched for a different account. |
accountRef | Your customer account. Appears on every credited transfer and is what GET /v1/deposits filters by. |
derivationIndex | Decimal string. Unique per chain — two accounts cannot share an index. |
400, not an overwrite. Without that constraint, a deposit would be credited to whichever account happened to be looked up first — a bug that is invisible until the wrong customer withdraws.Lifecycle of a deposit
The uniqueness constraint is the interesting one. Detection is idempotent at the database level rather than by remembering what has been processed, so a watcher that crashes and re-scans a range costs nothing but time.
Re-orgs
A confirmed deposit whose block is later orphaned becomes REORGED and a deposit.reorged event fires. The row is not deleted: your reconciliation needs to see that it existed, and a deleted row is indistinguishable from one that never happened.
Confirmation thresholds are configured per chain. Higher thresholds make a re-org less likely and deposits slower; that trade-off is yours.
Reading deposits
const deposits = await payments.deposits.list('account_4471'); deposits.transfers.forEach(t => { t.amountMinor; // "250000000" — a string t.blockHeight; // "19482013" — also a string t.status; // OBSERVED | CONFIRMING | CONFIRMED | REORGED });
Reads are scoped to the caller in the query itself, not filtered afterwards. An endpoint that fetched every operator's rows and then discarded the others has already read them, which is a different security property.
An account with no registered address answers 404 rather than an empty list — the two mean different things, and confusing them hides an onboarding bug.
Transfers to addresses you never registered
Money does arrive at addresses nobody expected — a customer pastes an old address, a sweep goes to the wrong tier, someone donates. Those are recorded as unknown transfers rather than discarded, because a discarded transfer is money that exists on-chain and nowhere in your books, and reconciliation will find the gap without being able to explain it.