Skip to content

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:

FieldMeaning
granted_usdTotal USD credited to the wallet across all top-ups and adjustments. Never resets.
spent_usdLifetime 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:

http
# 1 — Create a key scoped to the user (returns the key_id)
POST /api/keys
Content-Type: application/json

{
  "label": "alice"
}
http
# 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

http
GET /api/credits

Returns { "data": [ wallet... ] } for every key in the org that has a wallet. Each object includes:

json
{
  "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

http
GET /api/credits/{key_id}

Same fields as the list item, plus a ledger array with the 50 most recent entries.

Top up

http
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)

http
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

http
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

http
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_typeMeaning
topupCredit granted via /topup.
refundPositive adjustment via /adjust.
adjustNegative adjustment via /adjust.
debitCharge settled after a completed request.

Delete wallet

http
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
HTTP/1.1 402 Payment Required
json
{
  "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:

http
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 the ManagePolicy capability (Owner / Admin roles) — not ManageBudgets. 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 requires ManagePolicy.

Three modes are available:

charge_modeBehaviour
passthrough (default)charge = cost — the wallet decrements by the upstream cost. No margin.
markupcharge = cost × markup_factor — e.g. 1.25 for a 25% margin.
flatcharge = 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:

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

http
GET /api/virtual-models/{vm_id}/pricing

MIT / Apache-2.0 licensed.