Guides
Only endpoints that intentionally differ from the standard API carry a chip; the chip links to the note that explains the difference.
Three end-to-end paths that connect the reference sections into one sitting each. Every step below runs as written: dashboard calls reuse the session jar from POST /api/dashboard/login, inference calls use a Bearer ar- key. Secrets stay redacted — the full secret is shown exactly once at creation, so every example below writes ar-xxxxxxxxxxxx where your secret goes. Each guide ends with a done-criterion and a pointer for the failure case.
Contract-level changes — behavior fixes, new errors, anything that changes what a status code means — are logged newest-first in docs/CHANGELOG.md, with the shipped commit and the action to take, if any. When a guide step surprises you, check the changelog first: the behavior may have moved since the guide was written.
/v1/chat/completions/api/dashboard/deposit/epusdt/create/api/dashboard/topup/qris/api/dashboard/keys/api/dashboard/affiliatear- key (or x-api-key); the dashboard uses a session cookie instead. An ar- key never acts as a session, and a session cookie never authenticates inference.Guide 1: top up to your first settled call
From a zero balance to one settled inference call with reconciled cost, in about fifteen minutes. Prerequisite: a verified account with a session jar (see Account & security). Expect 402 before money lands — empty balance, unaffordable input, or a hold the balance cannot cover all answer 402, never retry them (full table under Billing).
| Step | Do this |
|---|---|
| 1. Key | Create one key. The response shows the full secret once — save it now; listing later returns a 12-character preview only. |
| 2. Balance | Confirm the starting point. A zero balance means the first inference attempt answers 402. |
| 3. Deposit | Open one order on either rail. Epusdt spans $1–$100,000; QRIS starts at the live-settings floor (default 10,000 IDR) with a 2,000,000 IDR ceiling. An unconfigured rail answers 503 before field checks — read deposit/info first. |
| 4. Pay + poll | Pay the newest invoice only, then poll its status. Epusdt status re-verifies a pending order against the provider inline — status 2 settles to credited. QRIS status only reports the stored state; only the signed webhook credits. On any repeat delivery, check status instead of paying again. |
| 5. Call | Send one chat completion with the new key. The gateway reserves a hold first — estimated from prompt size plus the max_tokens cap, stamped at affordability for uncapped requests, with a small per-request minimum — then forwards. |
| 6. Reconcile | Read the X-Atmorouter-Cost header, then compare it against GET /api/dashboard/usage (newest 200) and official list prices in usage-stats. Settle refunds an over-reserved hold and charges an under-reserved extra in full — the balance may go slightly negative. |
# 1. Key (201 — full secret once)
curl -b jar.txt https://atmorouter.dev/api/dashboard/keys \
-H "Content-Type: application/json" -d '{"name":"guide-01"}'
# -> {"id":"...","key":"ar-...","message":"key created"}
# 2. Balance (expect 0.00 on a fresh account)
curl -b jar.txt https://atmorouter.dev/api/dashboard/balance
# -> {"balance":0}
# 3a. Deposit over Epusdt ($1-$100,000) ...
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposit/epusdt/create \
-H "Content-Type: application/json" -d '{"amount_usd":10,"token":"USDT","network":"solana"}'
# -> 201 {"order_id":"...","trade_id":"...","payment_url":"..."} — pay this invoice
# ... or 3b. over QRIS (floor from live settings, ceiling 2,000,000 IDR)
curl -b jar.txt https://atmorouter.dev/api/dashboard/topup/qris \
-H "Content-Type: application/json" -d '{"amount_idr":50000}'
# -> 201 {"order_code":"AR-...","pay_url":"...","expires_in":7200} — pay within 2 hours
# 4. Poll the order (Epusdt may settle inline; QRIS only reports)
curl -b jar.txt "https://atmorouter.dev/api/dashboard/deposit/epusdt/status?order_id=<order_id>"
# -> {"order_id":"...","status":"credited"}
curl -b jar.txt "https://atmorouter.dev/api/dashboard/topup/status?id=<id>"
# -> {"id":...,"status":"credited"}
# 5. First call (Bearer ar- key, never the session cookie)
curl https://atmorouter.dev/v1/chat/completions \
-H "Authorization: Bearer ar-xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"model":"cbcn/glm-5.3","messages":[{"role":"user","content":"Hello!"}],"max_tokens":32}'
# -> 200; note the X-Atmorouter-Cost response header — that is the exact charge
# 6. Reconcile (header vs usage log)
curl -b jar.txt https://atmorouter.dev/api/dashboard/usage
# -> {"data":[{"model":"cbcn/glm-5.3",...,"cost":<same-as-header>,...}]}Done when the X-Atmorouter-Cost header equals the newest usage entry cost for the same model. If the call answers 402, top up instead of retrying; if it answers 401, the key is missing, revoked, or lacks the ar- prefix. The six-stage deposit lifecycle behind step 4 lives under Billing.
Guide 2: rotate a key without downtime
There is no rotation endpoint and no grace window — revoke-and-recreate is the whole primitive (see C-no-rotate under API keys). Downtime-free rotation is manual overlap: the replacement serves traffic before the old key dies.
# 1. Create the replacement FIRST (both keys are valid at once)
curl -b jar.txt https://atmorouter.dev/api/dashboard/keys \
-H "Content-Type: application/json" -d '{"name":"prod-app-v2"}'
# -> 201 {"id":"<new-id>","key":"ar-...","message":"key created"} — save the secret now
# 2. Prove the new key validates — without spending. Auth runs before body
# parsing, so a valid key with a bad body answers 400,
# while any bad key answers 401 first.
curl https://atmorouter.dev/v1/chat/completions -H "Authorization: Bearer ar-...new..." -H "Content-Type: application/json" -d '{}'
# -> 400 {"error":{"message":"model is required",...}} — key validated, zero billed
# 3. Shift traffic to the new secret (only now that it proves valid)
# 4. Delete the old key (immediate hard delete, scoped to your account)
curl -b jar.txt -X DELETE https://atmorouter.dev/api/dashboard/keys/<old-id>
# -> {"status":"ok"}
# 5. Prove the old key is dead (same shape, no spend — auth fails first)
curl https://atmorouter.dev/v1/chat/completions -H "Authorization: Bearer ar-...old..." -H "Content-Type: application/json" -d '{}'
# -> 401 {"error":{"message":"invalid API key","type":"invalid_api_key"}}| If you see | It means |
|---|---|
| 400 {"error":"invalid key id"} | The DELETE id is malformed. Copy the id from the list output, not from memory. |
| 404 {"error":"key not found"} | Well-formed id, but unknown or owned by someone else — scoped deletes answer identically so ids cannot be probed. |
| 401 on the NEW key | The secret was copied wrong, or the account is not active-and-verified. Re-create rather than guessing. |
Done when the old secret 401s, the new secret 400s on the empty-body probe (key validated, body rejected), and the key list shows only the new id. Two related traps: the list and export surfaces never return full secrets (12-character preview only), and a password change revokes every key on the account at once — after a reset, re-issue instead of rotating.
DELETE commits, so overlap first, delete last. There are no read-only or config-scoped key kinds — every user key is a full inference key.Guide 3: from referral link to spendable credit
Turn a referral link into API-call credit. The mental model first: two wallets, no automatic transfer — referral commission lands in your affiliate balance (available_amount), never in your usage balance (balance). Exactly one path bridges them: an explicit payout with method api_balance (full wallet map under Affiliate).
# 1. Get your link and the live policy (rate/minimum/daily are live, not promised)
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/summary
# -> {"code":"K7Q2XA","referral_link":"https://atmorouter.dev/?ref=K7Q2XA",
# "rate":0.1,"min_withdraw":5,"withdraw_daily":1, ...}
# 2. The invitee signs up via ?ref=CODE, deposits, and SPENDS.
# Each deposit opens commission worth deposit x rate; it unlocks as spend
# happens (matured = smaller of total and spend-since-deposit x rate).
# No spend, no matured money — locked_pending explains a balance that
# looks "stuck".
# 3. Watch maturity split in the same summary
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/summary
# -> {...,"stats":{"matured_pending":3.2,"locked_pending":9.3,...}}
# 4. Move matured earnings into call credit (no address, no daily limit)
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/payouts \
-H "Content-Type: application/json" -d '{"amount":5,"method":"api_balance"}'
# -> {"status":"requested","id":...,"amount":5}
# 5. After the operator confirms, the usage balance grows
curl -b jar.txt https://atmorouter.dev/api/dashboard/balance
# -> {"balance":<up-by-5>} — the only moment affiliate money becomes call credit| If you see | It means |
|---|---|
| 400 {"error":"minimum withdrawal is 5 USDC"} | Below the live min_withdraw policy — the number is policy-live; re-read the summary instead of hardcoding 5. |
| 409 {"error":"you already have a withdrawal awaiting processing"} | One payout in flight per user, any method. Wait for the operator to complete or reject it first. |
| 429 {"error":"wallet withdrawals are limited to 1 per day"} | usdt daily quota per UTC day — rejected requests excluded (a same-day retry after a reject is allowed), and only usdt is limited; api_balance never is. |
Done when GET /api/dashboard/balance grows by the payout amount and the ledger shows the matching payout_api_balance entry (GET /api/dashboard/affiliate/ledger). Prefer api_balance for credit: the usdt method instead pays a 32–44-character Solana address by manual operator transfer (length-checked only, never verified real), which is irreversible. Validation order, limits, and the post-request lifecycle live under Affiliate §§6–8.
If a guide and a reference disagree
The reference section wins — guides narrate, references state the exact behavior. Report the mismatch on Discord with the guide name and step number so the reference can be corrected.