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

Deposit lifecycle liveEndpoints sandbox onlyScope funding:writeCustomer custody not approved

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

text
created → awaiting_funds → detected → bridging → credited
                              ↘         ↘         ↘
                               ╰─────────┴─────────┴──→ failed
StatusWhat it means
createdA route was validated and a deposit row persisted.
awaiting_fundsAn address has been issued. Nothing has arrived.
detectedA transaction exists on the source chain.
bridgingThe transfer is progressing toward collateral. Includes the upstream’s own “completed”.
creditedA ledger entry was written. This, and only this, means the player has the money.
failedTerminal. 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

GET/api/v1/funding/supported-assetsAPI key
The asset and chain pairs that can be funded right now. Optional symbol, 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.

200 application/json
{
  "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
}
  • depositsEnabled is false when no treasury is configured, and depositsDisabledBecause then lists what is missing. Check it before showing a deposit button: the asset list is still returned either way.
  • minimumUsd is the route's own minimum, not ours. Below it a transfer will not bridge, and the funds are stranded rather than refunded.
  • addressKind is one of evm, svm, btc, tron, and tells you which deposit address shape the route is funded from.
  • decimals is required to render an amount. Base units are integers; a display amount is not.

Opening a deposit

POST/api/v1/funding/deposit-addressAPI key
Validate a route and issue an address for one player. 201 on success.
request
POST /api/v1/funding/deposit-address
Authorization: Bearer pk_test_...
X-Predicta-User: your-own-player-id

{ "asset": "USDC", "chainId": "137" }
201 application/json
{
  "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-User is registered here rather than rejected.
  • chainId is 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 pollUrl rather than building the path yourself.

Refusals

StatusCodeMeaning
400missing_userNo X-Predicta-User header.
422route_not_supportedThat asset and chain pair is not in the live catalogue.
503funding_unavailableNo treasury destination is configured. Nothing can be funded.
503no_address_for_chainThe route is valid but no address of the required kind was issued.
502bridge_unavailableThe bridge could not be reached. Nothing was recorded: retry safely.

Following a deposit

GET/api/v1/funding/deposits/{id}API key
Current status, the upstream's verbatim status, the full timeline and every transition so far.
200 application/json
{
  "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; a credited or failed one never does.
  • If that poll fails, the call still returns 200 with the stored lifecycle and a syncError field. A bridge outage must not blank out a deposit a player is anxiously watching, and a 502 here would read as though the money were gone.
  • eligibleToCredit means the transfer has arrived and a credit may now be made. It is not a credit. Only status: "credited" with a ledgerEntryId is that.
  • 404 unknown_deposit covers 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 history rather 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.