Quickstart
Ten minutes, in sandbox, with no chain contacted and nothing billed. At the end you will have a real derived address, a webhook endpoint receiving signed events, and a confirmed deposit against that address.
Everything here is free and unmetered. Sandbox derives real addresses from a real extended public key rather than fabricating strings, so the formats, checksums and validation you exercise are the production ones. The only difference is that no chain is contacted.
1. Generate a mnemonic
Offline, on a machine you trust, with a tool you trust. Any BIP-39 generator produces a compatible 12- or 24-word phrase.
For this walkthrough use the published BIP-39 test vector, so you can check your derivation against a known answer:
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
That phrase is in every BIP-39 test suite on the internet. It is fine for a sandbox walkthrough and catastrophic anywhere near real funds. Generate your own before you touch a live key, and read Mnemonic management.
Write it to a file, readable by nothing else:
mkdir -p secrets
printf '%s' "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" > secrets/mnemonic.txt
chmod 0400 secrets/mnemonic.txt
2. Get a test API key
Sign in to the console, enrol 2FA if you have not, and mint a key under Settings → API keys. Minting requires a fresh TOTP code.
You want the test key. Its prefix is the only thing that decides which environment the Edge runs in:
| Prefix | Environment |
|---|---|
flk_test_* | Sandbox — simulated chains, unmetered |
flk_live_* | Live — real chains, real funds, metered |
The key is shown once. Store it in your secret manager now.
3. Run the Edge
docker run -d --name chainos-edge \
-p 127.0.0.1:8787:8787 \
-v "$PWD/secrets/mnemonic.txt:/run/secrets/mnemonic:ro" \
-v chainos_edge_data:/var/lib/chainos \
-e PASS_PHRASE_FILE=/run/secrets/mnemonic \
-e API_KEY="$CHAINOS_API_KEY" \
-e CHAINOS_CLOUD_URL=https://api.chainos.cloud \
ziklag/chainos-edge:1.0
Two things about that command are deliberate.
-p 127.0.0.1:8787:8787 binds to loopback, not to every interface. The Edge holds your
key material and its local API is authenticated by keys it issues itself, but there is no
reason for it to be reachable from the network.
-v chainos_edge_data:... is a named volume, not a tmpfs and not an emptyDir. The
Edge refuses to start if its data directory is on ephemeral storage, because the outbox
there would lose its contents on restart while still looking durable. See
Configuration.
Watch it come up:
docker logs -f chainos-edge
A first boot logs synced with Cloud and a line about derived Solana and Stellar pool
addresses. A restart logs resumed with Cloud and the pool line disappears — enrolment is
a one-off.
Then confirm it for yourself, at the Edge rather than at Cloud:
curl -s http://localhost:8787/v1/edge/health
There is also a status page at http://localhost:8787/ in a browser. It is served by the
Edge and works before the first sync, which is exactly when the console can tell you
nothing — Cloud has no extended public keys yet, so every chain there is red with "No
Edge has connected yet."
If the derivation is right, the standard test mnemonic produces these two addresses:
| Chain | Address |
|---|---|
| Bitcoin | bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu |
| Ethereum | 0x9858EfFD232B4033E47d90003D41EC34EcaEda94 |
Those are the published vectors. If yours differ, stop — derivation has regressed and nothing else matters.
4. Mint a local key for your application
Your banking application should never hold a Ziklag credential. It holds a zkl_* key
that your own Edge issued, and it only ever talks to localhost:8787.
curl -s -X POST http://localhost:8787/v1/edge/keys \
-H "Content-Type: application/json" \
-d '{ "label": "quickstart" }'
{
"success": true,
"data": {
"id": "ek_01J8XKD",
"label": "quickstart",
"key": "zkl_test_9f2c…",
"prefix": "zkl_test_***…f5d559"
}
}
The key field appears once. Export it:
export EDGE=http://localhost:8787/v1
export CHAINOS_EDGE_KEY=zkl_test_9f2c…
5. Issue a deposit address
curl -s -X POST $EDGE/addresses \
-H "X-API-Key: $CHAINOS_EDGE_KEY" \
-H "Content-Type: application/json" \
-d '{
"chain": "eth",
"userRef": "cust_88213",
"tag": "deposit",
"label": "Main NGN wallet"
}'
{
"success": true,
"data": {
"id": "adr_01J8XKE",
"chain": "eth",
"address": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
"derivationPath": "m/44'/60'/0'/0/4211",
"environment": "sandbox",
"userRef": "cust_88213",
"tag": "deposit",
"status": "active"
},
"meta": { "requestId": "01J8XK…", "timestamp": "2026-08-19T10:32:00Z" }
}
userRef is yours — your customer identifier, opaque to ChainOS, and the field that
comes back on every webhook so you can credit the right ledger account without a lookup
table.
derivationPath is absolute. It exists so you can recover funds without ChainOS if
you ever need to, and a path relative to an account extended public key you never receive
would defeat its only purpose.
6. Register a webhook
You need a URL that can receive a POST. For a local walkthrough, any tunnel or request bin works; in production, this is an endpoint on your own network and the Edge relays to it, so it needs no public ingress at all.
curl -s -X POST $EDGE/webhooks \
-H "X-API-Key: $CHAINOS_EDGE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-endpoint.example/webhooks/chainos",
"events": ["deposit_detected", "deposit_confirmed"]
}'
The response carries a secret, once. It is the HMAC key for every delivery to that
endpoint. Store it beside your other secrets and read
Webhooks before you write the handler — verifying against a
re-serialised object instead of the raw bytes is the single most common integration bug
and it fails intermittently, which makes it miserable to debug.
7. Simulate a deposit
curl -s -X POST $EDGE/sandbox/simulate-deposit \
-H "X-API-Key: $CHAINOS_EDGE_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
"chain": "eth",
"amount": "100.50",
"autoConfirm": true,
"confirmAfterSeconds": 5
}'
Two webhooks arrive: deposit_detected at one confirmation, then deposit_confirmed
five seconds later at Ethereum's threshold of twelve.
{
"event": "deposit_confirmed",
"id": "evt_01J8XK9",
"environment": "sandbox",
"confirmations": 12,
"threshold": 12,
"timestamp": "2026-08-19T10:32:05Z",
"data": {
"chain": "eth",
"txid": "0xsbx…",
"address": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
"amount": "100500000000000000000",
"amountFormatted": "100.50",
"decimals": 18,
"userRef": "cust_88213",
"tag": "deposit"
}
}
To advance confirmations by hand instead, omit autoConfirm and call
POST /v1/sandbox/simulate-confirmation with a target count. It fires every threshold
crossed on the way, which is how you test a Bitcoin integration's 1 / 2 / 3 ladder in one
call.
8. Start over whenever you like
curl -s -X DELETE $EDGE/sandbox/reset -H "X-API-Key: $CHAINOS_EDGE_KEY"
That deletes every sandbox address, transaction and delivery, and resets the derivation index. No ceremony and no confirmation — the destructive-wipe gating applies to live only.
Where next
- Deposits — the full inbound path, thresholds, and reorgs.
- Withdrawals — build, sign at the Edge, broadcast, and the three distinct insufficient-funds errors.
- Idempotency — at-least-once delivery is the contract; this is the obligation it puts on you.
- Your chain's page — each one has an operational quirk that will otherwise surprise you: XRP's reserve, Solana's rent, Stellar's trustlines, TRON's energy, Polygon's two USDCs.
- Going live — what changes when the prefix changes.