Money
Funding
How a sandbox tenant gets simulated money to spend. The lifecycle is modelled honestly: a chain confirmation is not a credit, and only a ledger write can make it one.
What exists today
Sandbox only — 403 sandbox_only in live mode
This endpoint models a balance held by Predicta, which a live operator does not have. It exists so an integration can be learned against simulated money.
In production you own your customers' cash ledger. Predicta returns the exact amounts to apply against it — operatorMoney, operatorExitMoney, settlementCredit — and never holds a player's funds. Do not build the live integration on this endpoint.
All three funding routes answer 403 sandbox_only for a live operator. There is no live deposit flow to build here: a live integration funds its own customers on its own books and applies the amounts Predicta returns on quotes, orders and settlements.
Test treasury only
Funding in this build targets a test treasury wallet and is not approved for customer custody. Nothing on this page should be read as an approval to take a member of the public's money. Custody, the per-user versus omnibus account structure, and the regulatory perimeter around them are settled per deployment, before launch, with people rather than with documentation.
The deposit lifecycle
created → awaiting_funds → detected → bridging → credited
↘ ↘ ↘
╰─────────┴─────────┴──→ failed| Status | What it means |
|---|---|
created | A route was validated and a deposit row persisted. |
awaiting_funds | An address has been issued. Nothing has arrived. |
detected | A transaction exists on the source chain. |
bridging | The transfer is progressing toward collateral. Includes the upstream’s own “completed”. |
credited | A ledger entry was written. This, and only this, means the player has the money. |
failed | Terminal. No credit was made. |
A deposit never moves backwards. Legal transitions are enforced in code, and credited and failed are both terminal.
A confirmation is not a credit
detected and bridging describe a blockchain. They say a transaction exists and is progressing, and they say nothing whatever about whose internal balance the money belongs to.
credited is the separate decision that it belongs to a particular player, and the only path to it also writes the ledger entry: in the same transaction, or not at all. That is why the upstream reporting its own terminal success does not by itself advance our status: it sets eligibleToCredit instead. A retry that re-reports success would otherwise pay a player twice.
The two guards against a double credit
- The source transaction hash is unique. One on-chain transaction funds one deposit, ever.
- The ledger entry's idempotency key is derived from the deposit id, so a second credit attempt collides with the ledger's own uniqueness index.
Supported assets
/api/v1/funding/supported-assetsAPI keysymbol, chainId or comma-separated symbols narrow the list.The list is fetched live on every call rather than cached. A cached copy would be a list of routes we believe exist, and the first delisting would hand a player an address that eats their funds. That is also why an upstream outage returns 502 bridge_unavailable rather than a stale list.
Verified against the live bridge
At the time of writing this endpoint returned 229 live routes across 13 networks, with USDC, USDT, ETH and BTC present. Nothing in that list is cached or hard-coded.
All three funding endpoints require funding:write
Including the two reads. There is no funding read scope today, so a key that can only look at deposits is not currently expressible: provision keys accordingly.
{
"retrievedAt": "2026-08-20T17:04:11.902Z",
"count": 1,
"assets": [
{
"symbol": "USDC",
"name": "USD Coin",
"chainId": "137",
"network": "Polygon",
"tokenAddress": "0x…",
"decimals": 6,
"minimumUsd": 5,
"addressKind": "evm"
}
],
"symbols": ["USDC"],
"depositsEnabled": true
}depositsEnabledisfalsewhen no treasury is configured, anddepositsDisabledBecausethen lists what is missing. Check it before showing a deposit button: the asset list is still returned either way.minimumUsdis the route's own minimum, not ours. Below it a transfer will not bridge, and the funds are stranded rather than refunded.addressKindis one ofevm,svm,btc,tron, and tells you which deposit address shape the route is funded from.decimalsis required to render an amount. Base units are integers; a display amount is not.
Opening a deposit
/api/v1/funding/deposit-addressAPI key201 on success.POST /api/v1/funding/deposit-address
Authorization: Bearer pk_test_...
X-Predicta-User: your-own-player-id
{ "asset": "USDC", "chainId": "137" }{
"depositId": "…",
"userId": "your-own-player-id",
"status": "created",
"asset": "USDC",
"chainId": "137",
"network": "Polygon",
"depositAddress": "0x…",
"tokenAddress": "0x…",
"decimals": 6,
"minimumUsd": 5,
"addressKind": "evm",
"createdAt": "2026-08-20T17:04:12.115Z",
"pollUrl": "/api/v1/funding/deposits/…"
}The route is checked against the live catalogue before anything is persisted, so a deposit row can never describe a pair that is not accepted. The row is then written before an address is handed out, because an address a player has already funded and we have no record of is the one failure with no recovery path.
- This is the one funding call that creates an identity. A deposit must name the user it will credit, so a first-time
X-Predicta-Useris registered here rather than rejected. chainIdis accepted as a string or a number and is normalised to a string. It is an identifier, not an integer to do arithmetic on.- Follow
pollUrlrather than building the path yourself.
Refusals
| Status | Code | Meaning |
|---|---|---|
400 | missing_user | No X-Predicta-User header. |
422 | route_not_supported | That asset and chain pair is not in the live catalogue. |
503 | funding_unavailable | No treasury destination is configured. Nothing can be funded. |
503 | no_address_for_chain | The route is valid but no address of the required kind was issued. |
502 | bridge_unavailable | The bridge could not be reached. Nothing was recorded: retry safely. |
Following a deposit
/api/v1/funding/deposits/{id}API key{
"depositId": "…",
"status": "bridging",
"providerStatus": "SUBMITTED",
"eligibleToCredit": false,
"asset": "USDC",
"chainId": "137",
"network": "Polygon",
"depositAddress": "0x…",
"minimumUsd": 5,
"sourceTxHash": "0x…",
"sourceAmountBaseUnit": "25000000",
"creditedAmount": null,
"ledgerEntryId": null,
"failureReason": null,
"timeline": {
"createdAt": "…", "awaitingFundsAt": "…", "detectedAt": "…",
"bridgingAt": "…", "creditedAt": null, "failedAt": null
},
"history": [
{ "from": "created", "to": "awaiting_funds", "at": "…" },
{ "from": "awaiting_funds", "to": "detected", "at": "…" }
]
}- The bridge is polled on every read unless you pass
?sync=false. A non-terminal deposit therefore refreshes itself; acreditedorfailedone never does. - If that poll fails, the call still returns
200with the stored lifecycle and asyncErrorfield. A bridge outage must not blank out a deposit a player is anxiously watching, and a502here would read as though the money were gone. eligibleToCreditmeans the transfer has arrived and a credit may now be made. It is not a credit. Onlystatus: "credited"with aledgerEntryIdis that.404 unknown_depositcovers both “no such deposit” and “not yours”, deliberately. A partner must not be able to probe for another's deposit ids by comparing error codes.- Every transition appends to
historyrather than overwriting a field, so a deposit's past is inspectable after the fact.
Show the player your status, not the upstream’s
providerStatus is recorded verbatim for diagnosis and is not a customer-facing string. Render status, and treat anything short of credited as money that is on its way rather than money that has arrived.

