Per-key credit wallets
A credit wallet is a prepaid USD balance attached to a single gateway API key. Requests routed through that key are charged against its balance, and new requests are blocked (HTTP 402) once the balance runs out. This makes wallets a natural fit for reseller scenarios: provision one key per end-user, grant credit, and merido handles the accounting.
The wallet model
Each wallet tracks two values:
| Field | Meaning |
|---|---|
granted_usd | Total USD credited to the wallet across all top-ups and adjustments. Never resets. |
spent_usd | Lifetime charges drawn down from the wallet (the charge amount — see Charge / markup below). |
The available balance is always balance = granted − spent. Top-ups are additive: POST /api/credits/{key_id}/topup {amount_usd: 5} twice yields granted = 10.
Wallets are not time-windowed — they do not reset at midnight or the end of a billing cycle. A wallet runs until you refill it or delete it.
One-request overshoot. A wallet's balance can be exceeded by at most one in-flight request's actual charge. The engine checks the balance before admitting a request and settles the real charge after it completes, so under concurrent load the last request to pass the pre-flight check may push the wallet slightly negative. This is the same guarantee budgets give. If you need a tighter ceiling, set the limit a few cents below the amount you actually want to allow.
Onboarding a new user (two calls)
The typical reseller flow:
# 1 — Create a key scoped to the user (returns the key_id)
POST /api/keys
Content-Type: application/json
{
"label": "alice"
}# 2 — Grant the opening balance
POST /api/credits/{key_id}/topup
Content-Type: application/json
{
"amount_usd": 10,
"reason": "initial grant"
}The key is now live. Once Alice's balance reaches zero her requests receive HTTP 402 until you top up again.
API reference
All routes are under /api/credits*. Mutations require an admin session or a management token with the ManageBudgets capability (Owner / Admin / Billing roles). The two GET routes are org-scoped only — no additional RBAC gate.
List wallets
GET /api/creditsReturns { "data": [ wallet... ] } for every key in the org that has a wallet. Each object includes:
{
"key_id": 42,
"granted_usd": 25.00,
"spent_usd": 7.34,
"balance_usd": 17.66,
"low_balance_usd": 2.00,
"enabled": true,
"currency": "USD"
}spent_usd is read from the live in-memory counter (same one the data plane debits), so the balance reflects in-flight + settled spend immediately.
Get one wallet
GET /api/credits/{key_id}Same fields as the list item, plus a ledger array with the 50 most recent entries.
Top up
POST /api/credits/{key_id}/topup
Content-Type: application/json
Idempotency-Key: <optional unique string>
{
"amount_usd": 10.00,
"reason": "monthly refill" // optional
}Creates the wallet if it does not exist, then adds amount_usd to granted_usd. Returns { "balance_usd": <new balance> }.
Idempotency-Key header — supply a unique string (e.g. an invoice ID) to make retries safe. merido stores the key alongside the ledger entry: a second call carrying the same Idempotency-Key for the same key_id is a no-op that returns the current balance without granting a second time. Without an idempotency key, each call is treated as a distinct top-up.
Signed adjustment (refund / correction)
POST /api/credits/{key_id}/adjust
Content-Type: application/json
{
"amount_usd": -3.00, // negative = clawback; positive = manual correction
"reason": "overpayment clawback" // required
}Applies a signed delta to granted_usd. Positive adjustments add credit (equivalent to a top-up, but recorded as a refund entry). Negative adjustments reduce credit, subject to one constraint: the new granted total must not fall below the already-spent amount — you cannot claw back funds that have already been consumed. Returns { "balance_usd": <new balance> }.
Update settings
PUT /api/credits/{key_id}
Content-Type: application/json
{
"low_balance_usd": 2.00, // optional; null clears the alert
"enabled": true
}Sets a low-balance alert threshold (low_balance_usd) and enables or disables the wallet. A disabled wallet ("enabled": false) stops enforcing credit — requests through the key proceed as if there were no wallet. Setting enabled back to true re-activates enforcement.
The low-balance threshold does not block requests; it triggers internal alerting (visible in the dashboard wallet view).
Returns { "ok": true }.
Paginated ledger
GET /api/credits/{key_id}/ledger?limit=100&before=<entry_id>Returns the append-only audit ledger newest-first, keyset-paginated. limit is clamped to 1–500 (default 100). before is the id of the last entry you received; omit it for the first page.
Entry types:
entry_type | Meaning |
|---|---|
topup | Credit granted via /topup. |
refund | Positive adjustment via /adjust. |
adjust | Negative adjustment via /adjust. |
debit | Charge settled after a completed request. |
Delete wallet
DELETE /api/credits/{key_id}Removes the wallet. The underlying key is unaffected; it simply loses its balance gate. Returns { "ok": true }.
402 — insufficient credit
When a request is blocked because the balance cannot cover the estimated charge:
HTTP/1.1 402 Payment Required{
"error": "insufficient credit",
"scope": "key",
"key_id": 42,
"balance_usd": 0.08,
"required_usd": 0.24,
"currency": "USD"
}required_usd is the pre-flight estimate (it may differ slightly from the actual charge once the request completes). There is no Retry-After header — unlike a rate-limited budget, a depleted wallet cannot be waited out. The key must be topped up.
Charge, markup, and the sell price
By default every request's cost is passthrough: the wallet is decremented by what the upstream provider charges merido (the raw cost). For reseller deployments you usually want to decrement by a sell price — the amount you bill the end-user — not the raw cost.
Set a charge policy on a virtual model via the dashboard Virtual Models → Pricing tab, or via the API:
PUT /api/virtual-models/{vm_id}/pricing
Content-Type: application/json
{
"charge_mode": "markup",
"markup_factor": 1.25
}RBAC. The pricing endpoint (
PUT /api/virtual-models/{vm_id}/pricing) gates on theManagePolicycapability (Owner / Admin roles) — notManageBudgets. It governs routing/charge policy, not the wallet itself. So a Billing-role user can fund and adjust wallets (ManageBudgets) but cannot set the charge/markup policy; that requiresManagePolicy.
Three modes are available:
charge_mode | Behaviour |
|---|---|
passthrough (default) | charge = cost — the wallet decrements by the upstream cost. No margin. |
markup | charge = cost × markup_factor — e.g. 1.25 for a 25% margin. |
flat | charge = flat rate table — the wallet decrements by your declared per-token rates, independent of upstream cost. |
For flat mode, supply the per-million-token rates alongside charge_mode:
PUT /api/virtual-models/{vm_id}/pricing
Content-Type: application/json
{
"charge_mode": "flat",
"input_per_1m": 3.00,
"output_per_1m": 15.00
}Once a charge policy is set, every request routed through that virtual model decrements wallets (and budgets that track ChargeUsd) by the sell price, not the provider cost. The raw cost is still recorded separately for your own margin analysis.
Read the current policy with:
GET /api/virtual-models/{vm_id}/pricingRelated
- Virtual models & fallback — creating and managing virtual models.
- Guardrails & budgets — spending limits that complement credit wallets.
- Access control & teams — the
ManageBudgetscapability required for credit mutations. - API endpoints — full endpoint listing.