DocumentationAffiliate
Ask AI

Take the docs with you

Ask ChatGPTOpen a new chat ↗Ask ClaudeOpen a new chat ↗Ask on GeminiCopy prompt, then paste in Gemini ↗View llms.txtDocumentation index ↗

External chats receive links to public docs.

Affiliate

Only endpoints that intentionally differ from the standard API carry a chip; the chip links to the note that explains the difference.

Covers the five live routes under /api/dashboard/affiliate. Audience: signed-in dashboard users earning referral commission. Prerequisite for every command below: log in first and keep the session cookie. API keys (ar-…) are rejected on all affiliate routes with 401.

/api/dashboard/affiliate
bash
curl -c jar.txt https://atmorouter.dev/api/dashboard/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"..."}'

Two wallets — read this first

There are two balances and money never moves between them on its own:

WalletResponse fieldPurpose
Usage balancebalanceSpendable on API calls. Check it with GET /api/dashboard/balance → {"balance":12.5}.
Affiliate balanceavailable_amountWithdrawable only. Referral commission lands here, never in the usage balance.

The affiliate balance reaches the usage balance through exactly one path: an explicit payout with "method":"api_balance", recorded in the affiliate ledger. There is no automatic transfer, no matter how large either balance grows.

1. Check your affiliate balance

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/summary

The response carries your share link and both halves of the picture (identity values below are illustrative; shapes and policy fields are exact):

json
{
  "code": "K7Q2XA",
  "enabled": true,
  "rate": 0.1,
  "min_withdraw": 5,
  "withdraw_daily": 1,
  "referral_link": "https://atmorouter.dev/?ref=K7Q2XA",
  "rate_history": [{ "rate": 0.1, "effective_from": "2026-09-01T00:00:00Z" }],
  "wallet": { "available": 12.5, "withdrawn": 5.0, "total_earned": 17.5 },
  "stats": {
    "signups": 9, "spenders": 4, "referred_spend": 125.0,
    "matured_pending": 3.2, "locked_pending": 9.3
  },
  "pending_payout": { "id": 41, "amount": 5.0, "method": "usdt" }
}

How to read it:

FieldDescription
wallet.availableWhat you can withdraw right now.
wallet.withdrawn / wallet.total_earnedLifetime reporting totals, never used for payouts.
stats.matured_pending vs stats.locked_pendingCommission matures gradually as each referred user spends; locked_pending is still waiting on their spending. If your balance looks "stuck", this split is why.
pending_payoutPresent only while a request is awaiting processing. While it exists, new payout requests are rejected (see 409 below), so the dashboard should disable the withdraw form instead of letting the server refuse it.
rate, min_withdraw, withdraw_dailyThe live program policy, not hardcoded promises. The numbers above are the current defaults (10% rate, 5 minimum, 1 per day); an admin change is reflected here immediately. rate_history lists past rate periods (shape above) for the footnote: past earnings used the rate in effect when each invite's spend happened. A first entry dated 1970-01-01 is the backfill for pre-history spend — expected, not a corrupt timestamp.

2. See who you referred

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/referrals
# -> {"data":[{"id":7,"created_at":"...","email":"jo***@example.com",
#      "spent":40.0,"commission":4.0,"matured":1.8}]}

Newest first, capped at 200 entries. Emails are masked server-side to jo***@example.com form (first two local-part characters kept, or customer when no @ exists). spent counts only the referred user's requests that settled with status success or partial — failed requests are never billed, so they never earn commission.

3. Track your payouts

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/payouts
# -> {"data":[{"id":41,"amount":5.0,"method":"usdt","address":"...",
#      "network":"solana","status":"pending","note":"","tx_hash":"",
#      "requested_at":"...","processed_at":null}]}

Newest first, capped at 100 entries. status stays pending until a human operator confirms it (completed) or rejects it (rejected) — there is no intermediate state written by any current endpoint (readers also accept a processing value defensively, but nothing sets it). A populated tx_hash is your on-chain receipt for usdt withdrawals.

Method quirk: only POST creates a payout — any other method on this path (PUT, PATCH, …) returns the list above instead of 405.

4. Read the ledger (beta)

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/ledger
# -> {"data":[{"kind":"payout_usdt","amount":-5.0,"balance_after":7.5,
#      "created_at":"..."}]}

Newest first, capped at 100 entries. Every payout reserves from available_amount and writes a negative-amount entry (payout_usdt or payout_api_balance) with the balance after reservation — the audit trail that ties each withdrawal to the wallet movement. The ledger also records maturation credits (commission_matured), clawbacks (commission_clawed), and reject returns (payout_rejected_return), so a payout entry here always has a matching ledger entry.

Beta: response shape may change. The ledger endpoint is in beta. It is safe to poll today, but treat the exact response shape as subject to change.

5. Request a payout

Two methods, usdt or api_balance (surrounding spaces trimmed, case folded; anything else is a 400). Amounts round to 6 decimals and must be at least the live min_withdraw (currently 5; the server message reads "minimum withdrawal is 5 USDC").

Withdraw to a Solana wallet (manual transfer by an operator follows):

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/payouts \
  -H "Content-Type: application/json" \
  -d '{"amount":12.5,"method":"usdt","address":"<32-to-44-char-solana-address>"}'
# -> {"status":"requested","id":41,"amount":12.5}

Move earnings into API-call credit instead (no address needed, no daily limit):

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/affiliate/payouts \
  -H "Content-Type: application/json" \
  -d '{"amount":12.5,"method":"api_balance"}'
# -> {"status":"requested","id":42,"amount":12.5}

Check the address twice before sending. The server validates length only (32–44 characters) — it does not verify the address is real or that you typed it correctly, and the operator transfer is irreversible. network defaults to solana.

6. Limits and errors

The server checks, in order: bad body → program enabled → method → amount → minimum → address → wallet lock (a missing wallet stops here) → one in-flight request → daily limit → sufficient balance. The first failure wins. Limit values below reflect current defaults; the summary endpoint always carries the live policy.

SituationStatusBody
Not signed in (or ar- API key)401{"error":"not authenticated"}
Unparseable POST body (4 KB cap)400{"error":"invalid request"}
Program disabled by admin403{"error":"affiliate program is disabled"}
Unknown sub-path (after auth)404{"error":"not found"}
Method not usdt/api_balance400{"error":"invalid payout method"}
Amount ≤ 0, NaN, or infinite400{"error":"invalid amount"}
Below live minimum400{"error":"minimum withdrawal is 5 USDC"}
usdt address shorter than 32 or longer than 44 chars400{"error":"a valid Solana wallet address is required"}
A payout is already pending/processing409{"error":"you already have a withdrawal awaiting processing"} — one in flight per user, any method.
usdt daily quota reached (UTC day)429{"error":"wallet withdrawals are limited to 1 per day"} — usdt only, rejected requests excluded from the count; api_balance is never rate-limited.
Reserved balance fell short400{"error":"insufficient affiliate balance"}

Three rules worth remembering:

RuleDescription
1. One at a time (409)A second request while one is in flight is rejected, even for a different amount or method. Wait for the operator to complete or reject the pending one — or check pending_payout in the summary first.
2. One usdt withdrawal per UTC day (429)The day boundary is UTC, the same day the operator sees in the admin queue. api_balance transfers are internal, cost nothing to process, and are never rate-limited.
3. Concurrent requests cannot double-spendThe wallet balance is locked first and the reserve is a single atomic update that fails unless the balance still covers the amount — so two parallel requests cannot both pass. If yours loses the race, you get insufficient affiliate balance, not a partial withdrawal.

7. What happens after {"status":"requested"}

Your request is parked as pending. Nothing else moves until a human operator acts — the operator queue is server-side only (there is no public operator endpoint: /api/admin answers 404 on the public gateway):

OutcomeWhat happens
api_balance confirmCredits the usage balance — this is the only moment affiliate money becomes API-call credit.
usdt confirmOff-platform manual transfer by an operator; the tx_hash is then recorded on your payout request.
RejectReturns the reserved amount to available_amount and writes a payout_rejected_return ledger entry, freeing you to request again.

There is no email-confirmation step on this path: nothing on the payout route sends mail, and no confirmation record is ever read back. Request, then wait. Settlement time depends on operator review, so pending lasting hours is normal.

8. Commission maturity and payout-safety notes

How the money behind this page behaves: the maturity rule, when rate changes take effect, and the payout-safety edges the live service enforces. Nothing here moves money — behavior fixes, if any, ship in a separate lane.

RuleWhat happens
1. Commission matures only from spendEach referred deposit opens a commission worth deposit x rate at deposit time; it unlocks as the referred user spends: matured is the smaller of total and spend-since-deposit x rate. Oldest deposits consume spend first from one shared pool (FIFO, oldest deposits first). Unspent deposits pay nothing.
2. Rate changes apply the moment they take effectSpend prices at the rate in effect when it happened (periods oldest-first; new deposits use the live policy rate). Each usage entry is attributed by its own timestamp, so a same-day change prices the morning at the old rate and the afternoon at the new one. rate_history lists past periods; the 1970-01-01 line is the backfill for pre-history spend, not a corrupt timestamp.
3. Fully-matured earnings show as ready, not stuckmatured_pending / locked_pending count pending plus fully-matured-but-unpaid commissions; a fully-matured commission flips to matured and still shows as ready here as well as in wallet.available. Payouts reserve from available_amount, so a zero split with positive available means request a payout — nothing is stuck.
4. Commission opens shortly after the deposit, never twice — retries close the gapAccrual opens a commission shortly after the deposit on both rails with fail-open guards, so a just-credited deposit may show its commission seconds later — wait and refresh, do not re-deposit. If the process stops between credit and the follow-up write, the commission is not lost: the write retries inline 3 times with 100 ms backoff, a pending intent is persisted durably, and a per-minute pass replays open intents and picks up completed rail deposits older than 2 minutes that still have no commission entry (batch cap 200). Replays only ever open pending, zero-matured commissions, so a retry can never mint spendable money — maturity still unlocks exclusively when the referred user spends. Both rails claim pending-to-credited exactly once and the commission write is idempotent per source deposit.
5. Refunds claw matured money back; a reject returns only what the claw left behindA refund on spent deposits reduces available_amount / total_earned with a floor at zero and writes a commission_clawed ledger entry. A payout already parked as pending is resolved by the operator by hand — so expect a delay and matching ledger entries, not an automatic adjustment. The returned amount is capped, not full: it can never exceed the still-unpaid matured amount left after the claw, so a refund that landed mid-flight keeps its clawed part out of your wallet instead of minting it back. A capped return is marked clawback-reconciled on the payout note with its payout_rejected_return ledger entry; when the claw took everything there is no ledger entry and the note is the only pointer. Only an api_balance confirmation credits your usage balance.
6. A rejected USDT payout frees today's quota for a retryThe daily count excludes rejected requests: a rejected usdt request returns its reservation to the wallet and does not consume withdraw_daily = 1, so a second request the same UTC day is allowed — just retry. Completed requests still consume quota (confirm-then-retry answers 429 {"error":"wallet withdrawals are limited to 1 per day"}). api_balance moves are never rate-limited, only one payout may be in flight at a time (409), and the minimum stays policy-live, currently 5 ({"error":"minimum withdrawal is 5 USDC"}).