Sandbox and live
There are two environments. Which one you are in is decided by the prefix of the API key, at both the Edge and Cloud, and it is immutable for the lifetime of the process.
| Key prefix | Issued by | Environment |
|---|---|---|
flk_test_… | the console | sandbox |
flk_live_… | the console | live |
zkl_test_… | your Edge | sandbox |
zkl_live_… | your Edge | live |
There is no toggle, no header and no query parameter. An Edge started with a test key
cannot issue live keys and cannot produce live data, and the two are separated by an
environment column on every tenant table rather than by convention.
Running both means running two containers with two API keys and two data directories. This is deliberate: it makes it impossible to cross sandbox and live data by misconfiguration.
What differs
| Live | Sandbox | |
|---|---|---|
| Address derivation | Real, from your registered extended public key | Real, from your sandbox extended public key. Valid addresses, never funded |
| Chain RPC | Yes | None. Zero outbound chain calls |
| Balances | On-chain | Derived from the ledger: simulated incoming less outgoing |
| Deposits | Chain events | POST /v1/sandbox/simulate-deposit |
| Confirmations | Real block progression | POST /v1/sandbox/simulate-confirmation, or autoConfirm |
| Webhooks | Real, signed | Real, signed, byte-identical format |
| Signing | Real | Real — exercises the full Edge path |
| Billing | Metered | Never metered, at all |
A sandbox balance is a ledger read, not a mock and not a chain read. That distinction matters because the alternative is what the endpoint used to do: ask a provider about an address that exists on no chain, and report the truthful zero that comes back while the transaction list showed the deposits. The balance and the transactions now agree, and the zero-outbound-calls row above holds for balance queries too.
The two rows in bold are the point. Sandbox derives genuine addresses from a genuine extended public key and signs with genuine keys, so your integration tests exercise real address formats, real checksums, real validation and the real Edge signing path. The only thing that does not happen is a chain being contacted.
That is deliberately different from a mock server. A fabricated address string passes your validation and fails on the first live deposit; a real derived address that happens to have no funds does not.
Simulating activity
# One deposit, auto-confirming after five seconds
curl -X POST $EDGE/sandbox/simulate-deposit \
-H "X-API-Key: $CHAINOS_EDGE_KEY" \
-H "Content-Type: application/json" \
-d '{ "address": "0x3fC9 …7FAD",
"chain": "polygon",
"amount": "100.50",
"autoConfirm": true,
"confirmAfterSeconds": 5 }'
Deposits are of the chain's native asset, so amount is denominated in it — POL
above, not USDT. Token deposits are not simulated, and a request carrying
tokenContract is rejected rather than quietly treated as a native one.
chain disambiguates, and this example needs it. Ethereum, BSC, Polygon, Avalanche and
Base share coin type 60 and therefore one extended public key, so the first address
issued on each of the five is the same string — one address, five registry rows. The
address alone cannot say which chain a deposit arrived on, so omitting chain for one of
them is a 400 listing the candidates rather than a guess. On Bitcoin, TRON, Solana, XRP
and Stellar no collision is possible and chain can be omitted.
The response echoes the chain and symbol it resolved, which is worth asserting on in
a test: it is the one part of the outcome the request does not already state.
confirmAfterSeconds defaults to 15 and has a minimum of 1. Set it low in CI; a suite
that waits fifteen seconds per fixture wastes more time than it looks like it does.
# Advance a transaction to a target confirmation count by hand
curl -X POST $EDGE/sandbox/simulate-confirmation \
-H "X-API-Key: $CHAINOS_EDGE_KEY" \
-H "Content-Type: application/json" \
-d '{ "transactionId": "tx_sbx_01J8XKB", "confirmations": 12 }'
This fires every threshold crossed on the way, which is how you test Bitcoin's
1 / 2 / 3 ladder in a single call. Omit confirmations to advance by one.
# Status, and the reset
curl -s $EDGE/sandbox/status -H "X-API-Key: $CHAINOS_EDGE_KEY"
curl -s -X DELETE $EDGE/sandbox/reset -H "X-API-Key: $CHAINOS_EDGE_KEY"
reset deletes every sandbox address, transaction and webhook delivery, and resets
next_index on the sandbox extended public keys. One call, no confirmation prompt — the
destructive-wipe gating applies to live only, because there is nothing here to lose.
Should sandbox use the same mnemonic?
No. Use a separate mnemonic for sandbox.
Cloud actively refuses the same one in both: the identity key derived from a mnemonic is
the same in either environment, and seeing one identity claim both is
409 CROSS_ENVIRONMENT_IDENTITY. The check exists because sharing a mnemonic across
environments means a sandbox Edge — usually the one running in CI, on a laptop, in a
container with looser handling — holds the keys to live funds.
Generate a throwaway phrase for sandbox and keep the live one in your secret manager. The BIP-39 test vector is fine for sandbox; see Quickstart.
Going live
A checklist, in the order the failures actually happen:
- A live mnemonic exists, is backed up, and the backup has been restored once as a test. An untested backup is not a backup. See Mnemonic management.
- A live
flk_live_*key is minted and in your secret manager. It is shown once. - A second Edge container, with its own data directory and the live key. Do not repoint the sandbox one.
EDGE_DATA_DIRis on persistent storage. Not tmpfs, notemptyDir. The Edge refuses to start otherwise, which is the intended behaviour and catches this before it matters.- All seven readiness indicators are green in the console, or the ones you need are.
A red chain refuses address generation with
CHAIN_NOT_READYrather than issuing an address nobody holds a key for. See Edge liveness. - Your webhook handler verifies the signature against raw bytes and is idempotent on
event.id. Both. See Webhooks and Idempotency. - Your handler survives a duplicate delivery without double-crediting. Test it by replaying a delivery from the console rather than assuming.
- You have read your chains' pages. Each has one operational quirk that costs money to discover live: XRP's reserve, Solana's rent, Stellar's trustlines, TRON's energy, Polygon's two USDCs.
- Alerting on your own webhook handler's failure rate and dead-letter depth. Ziklag alerts on delivery from our side; only you can see a handler that accepts a delivery and then drops it. See Monitoring.
What billing does with the distinction
Sandbox activity is never metered — no address counts, no settled transactions, no overage. Nothing you do in sandbox appears on an invoice, which is why the simulate endpoints have no rate ceiling worth mentioning and why load-testing your integration there is encouraged. See What is billed.