Last updated: 2026-09-18 · commit
e64b9df42
How to add credit, read your balance and usage, cap what a key can spend,
redeem an invite code, and act on a 402. Why the coordinator behaves this
way — reservations, settlement, the ledger, provider payouts — is explained in
architecture/billing.md; every constant and
route is tabulated in reference/pricing-model.md.
- A Darkbloom account (console sign-in through Privy) and an API key
(
sk-db-…) — see authentication.md. Requests below marked Privy need the console session token, not an API key. - Deposits and payouts require the operator's Stripe configuration; if
POST /v1/billing/stripe/create-sessionreturns503 billing_error, Stripe is not configured on that coordinator. - Rates are per token, prepaid, with no subscription.
GET /v1/pricing(no auth) returns the platform price for each model and the fallback rates used when a model has none;GET /v1/modelsrepeats them in itspricingblock.
curl -X POST https://api.darkbloom.dev/v1/billing/stripe/create-session \
-H "Authorization: Bearer sk-db-..." \
-H "Content-Type: application/json" \
-d '{"amount_usd": "10.00"}'amount_usd is a string and must be at least 0.50, the Stripe deposit
minimum in reference/pricing-model.md → Constants.
Optional fields:
email (prefilled on the Checkout page) and referral_code (see step 6). The
response carries the Stripe page to open plus the coordinator's own session id:
{
"session_id": "3f0e...",
"stripe_session": "cs_live_...",
"url": "https://checkout.stripe.com/c/pay/cs_live_...",
"amount_usd": "10.00",
"amount_micro_usd": 10000000
}Open url and pay. The coordinator does not credit on redirect; it credits
when Stripe delivers checkout.session.completed to its webhook, usually
within seconds. The credit lands as a stripe_deposit ledger entry on your
spendable balance; deposits are never withdrawable
(coordinator/api/billing_handlers.go handleStripeWebhook).
In the console, Buy Credits on /billing reaches the same endpoint through
the same-origin relay /api/payments/stripe/checkout, which forwards your Privy
session (Authorization header or privy-token cookie) — not your API key,
which the browser also sends and the relay ignores
(console-ui/src/app/api/payments/stripe/checkout/route.ts).
curl "https://api.darkbloom.dev/v1/billing/stripe/session?id=3f0e..." \
-H "Authorization: Bearer sk-db-..."status moves from pending to completed when the webhook has been
processed (handleStripeSessionStatus).
curl https://api.darkbloom.dev/v1/payments/balance -H "Authorization: Bearer sk-db-..."
curl https://api.darkbloom.dev/v1/payments/usage -H "Authorization: Bearer sk-db-..."{"balance_micro_usd": 10000000, "balance_usd": "10.000000",
"withdrawable_micro_usd": 0, "withdrawable_usd": "0.000000"}balance_micro_usd is what requests can spend, in
micro-USD.
withdrawable_micro_usd is the part you earned (serving inference, referral
rewards) and can pay out through Stripe Connect; deposits and invite credits
never count toward it, so a pure consumer sees 0. GET /v1/payments/usage lists settled
requests with job_id, model, prompt_tokens, completion_tokens,
cost_micro_usd, timestamp (coordinator/api/consumer.go handleBalance,
handleUsage). Console users get the same figures from GET /v1/me/summary
(Privy). Usage is a recent-history view, not a complete billing export;
the process retains the newest entries up to the usage history limit.
Dashboard earnings windows include every row in each window, without the old
5,000-row truncation. Concurrent tabs share one aggregate per account and may
lag by the per-account cache interval
(coordinator/api/me_summary_cache.go, mySummaryWindowsCacheTTL).
Each request is charged
prompt_tokens × input_price + completion_tokens × output_price at the
platform price, floored at the per-request minimum, on the token counts the
provider reports. Before dispatch the coordinator reserves the worst case —
your estimated prompt plus the full output bound (your max_tokens, or a
model default when you set none; the exact rule and default are in
pricing-model.md → Formulas) — and
refunds the difference after the response. A provider with a custom price above the platform price is paid
for from an extra reservation taken at dispatch. Requests that route to your
own machine (self-route) settle at zero. The
formulas and constants are in reference/pricing-model.md →
Formulas; what happens to cached
tokens and to the platform fee is stated in
architecture/billing.md → Invariants.
Because the reservation uses the output bound, a request can be refused for
insufficient balance even though its settled cost would have fit. Set
max_tokens to what you need.
curl -X POST https://api.darkbloom.dev/v1/keys \
-H "Authorization: Bearer <privy-access-token>" \
-H "Content-Type: application/json" \
-d '{"name": "ci", "limit_usd": 25, "limit_reset": "monthly"}'limit_usd is a USD number >= 0; limit_reset is none (lifetime cap),
daily, weekly, or monthly, aligned to UTC midnight, Monday, and the 1st
(coordinator/store/apikey.go KeySpendWindowStart). Change either later with
PATCH /v1/keys/{id}. The cap is checked against the key's settled usage in
the window before each request's reservation; it is a soft sub-cap under your
account balance, so several in-flight requests can together overshoot it by up
to their reservations (coordinator/api/apikey_handlers.go checkKeySpendCap).
GET /v1/keys shows usage_usd, limit_usd, and remaining_usd per key.
Register a code of your own (3–20 letters, digits, or hyphens, stored
uppercased — the rule is in reference/pricing-model.md → Constants;
Privy):
curl -X POST https://api.darkbloom.dev/v1/referral/register \
-H "Authorization: Bearer <privy-access-token>" \
-H "Content-Type: application/json" -d '{"code": "MYCODE"}'A referred user attaches your code once, either by
POST /v1/referral/apply {"code": "MYCODE"} (Privy) or by passing
referral_code on their first Checkout session (step 1); an account can have
one referrer and cannot refer itself. From then on you earn a fixed share of
the platform fee taken on that user's requests, credited as withdrawable
referral_reward entries. The share and the fee it applies to are in
reference/pricing-model.md → Formulas
and architecture/billing.md → Consumer referral;
read those before promising anyone an income. GET /v1/referral/stats
returns code, total_referred, total_rewards_micro_usd;
GET /v1/referral/info returns code, share_percent, referred_by.
curl -X POST https://api.darkbloom.dev/v1/invite/redeem \
-H "Authorization: Bearer sk-db-..." \
-H "Content-Type: application/json" -d '{"code": "INV-1a2b3c4d"}'Invite codes are created by Darkbloom staff and carry a fixed amount. A
successful redemption returns credited_usd and balance_usd; the credit is
spendable but not withdrawable, and each account can redeem a given code once
(coordinator/api/invite_handlers.go handleRedeemInviteCode).
There is no self-service switch. If you route traffic on behalf of many end
users (a gateway or marketplace), ask Darkbloom to mark your account as a
service account. It changes three things: the per-request minimum charge is
dropped, requests are always billed at the platform price regardless of a
provider's custom price, and the request rate limit moves to the service tier.
Details: architecture/billing.md → Service accounts.
Choose your country of residence in bank setup and use a bank account in that country. Stripe collects bank details and required identification. Available destinations are shown in the country selector; a country being listed still requires successful verification of your account and bank.
For international bank withdrawals, enter a USD amount and select Review withdrawal. Review the estimated local deposit, destination, withdrawal fee and expected timing, then select Confirm withdrawal. Reviewing does not deduct earnings. An expired estimate must be refreshed. If a response is interrupted, Check withdrawal resolves the existing withdrawal before allowing another. The same browser remembers that confirmation when you reload or reopen the page, including when your remaining balance is zero.
If history shows Needs review, contact support with the withdrawal ID. Its funds remain reserved until the outcome is established; do not submit another payment for that withdrawal.
In history, Sent to bank means the transfer left Stripe; it can take additional time for your bank to credit it. Returned to balance means the transfer was returned and your withdrawable earnings were restored. Your bank can charge additional fees. Existing Connect withdrawals keep their current payout schedule. See the pricing reference.
The form shows the bank's published deposit minimum and maximum in local currency. If an amount is outside those limits, adjust the USD withdrawal and review again; Stripe confirms the exchange rate. When new withdrawals are paused, an unsubmitted confirmation is released. Already-submitted withdrawals can still be checked.
Choose Unlink Stripe account and start over to remove the destination currently shown. For Global Payouts this removes only Darkbloom's recipient mapping; an older Connect destination may then reappear. For Connect it removes the stored Connect mapping. This does not close either Stripe account or cancel submitted withdrawals.
GET /v1/billing/stripe/session?id=…shows"status": "completed"andGET /v1/payments/balancehas risen byamount_micro_usd.- After a chat completion,
GET /v1/payments/usagelists the request with itscost_micro_usd, and the balance has dropped by exactly that amount (the unused part of the reservation is refunded in the same settlement). GET /v1/keysshowsusage_usdgrowing on the key you used andremaining_usdshrinking toward0.
| Symptom | Cause | Fix |
|---|---|---|
| Paid on Stripe, balance unchanged | Webhook not delivered yet, or the coordinator's webhook secret is wrong | Poll the session status; if it stays pending for minutes, contact the operator with stripe_session |
402 with error.type insufficient_funds |
Balance is below the worst-case reservation | Deposit, or lower max_tokens; see step 4 |
402 with error.type insufficient_quota |
Per-key cap reached for the window | PATCH /v1/keys/{id} with a higher limit_usd, wait for the window to reset, or use another key |
402 with error.type provider_error |
The only provider that could serve the model has a custom price above the platform price and your balance could not cover the extra reservation | Deposit; the request was not charged |
400 invalid_request_error on create-session about amount_usd |
Deposit below the minimum | Send amount_usd at or above the minimum, as a string (step 1) |
400 "invalid referral code" on create-session |
referral_code is not a registered code |
Drop the field or fix the code |
400 referral_error "account already has a referrer" / "cannot refer yourself" |
One referrer per account; self-referral rejected | — |
400 "invite code … is inactive / has expired / has reached max uses" or "account has already redeemed code" |
Code exhausted or reused | Ask for a new code |
404 referral_error "not a registered referrer" on GET /v1/referral/info |
You have not registered a code | Step 6 |
401 auth_error on POST /v1/keys, /v1/referral/register, /v1/referral/apply |
Called with an API key | Use the Privy access token |
429 on create-session, key mutations, referral or invite calls |
The financial rate limiter | Back off for Retry-After |
| Balance dropped by more than the response should cost, then recovered | Reservation debited at admission, refund at settlement | Expected; read balance after the response completes |
503 billing_error |
Stripe or the referral service is not configured on this coordinator | Operator issue |
Mechanism for each error, including the exact functions, is in
architecture/billing.md → Failure modes.
architecture/billing.md— reservation, settlement, ledger, Stripe, referral, base rewardsreference/pricing-model.md— constants, formulas, routes, environment variablesauthentication.md— creating, rotating, and scoping API keysmodels.md—GET /v1/modelsand itspricingblock../provider/self-route.md— routing to your own machine, which settles free../reference/api-contracts.md— error envelope and status codes
Sign in and click the model offer’s Claim tokens button before its deadline. Logging in alone does not claim a grant. Eligibility depends on your account’s signup date; a limited number of grants are allocated to the first eligible accounts to claim. Tokens belong to your account, work with all its API keys, and never expire. The chat view shows the selected model's remaining allowance. Other models use their ordinary billing.
Input and output tokens both consume the allowance. Tiny requests consume their actual token usage; there is no extra token deduction for a sponsored payout minimum. When it runs out, requests use your paid balance. If a request crosses the boundary, only its uncovered tokens are billed, subject to the normal request minimum. A 402 free_tokens_exhausted means no free tokens are available and paid credit cannot cover the request. Add credit, or wait if another request has reserved the remaining free tokens. A 402 promotion_balance_required means the maximum requested size cannot fit the remaining free tokens plus paid balance; reduce max_tokens or add credit. A request may reserve more than it ultimately uses; unused tokens return at settlement.
With an interactive Privy session, GET /v1/me/token-promotions lists grants; POST /v1/me/token-promotions/claim with {"model_id":"..."} claims the selected offer safely. The response also lists available offers, remaining claim slots and eligibility status. API keys may use an existing grant but cannot issue one. See pricing-model.md.