Skip to main content

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
Test vector only

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:

PrefixEnvironment
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:

ChainAddress
Bitcoinbc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu
Ethereum0x9858EfFD232B4033E47d90003D41EC34EcaEda94

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.