Per-user credits (wallets)

Give every user of your SaaS a balance in your unit — credits, tokens, points — that odnoga debits automatically as they use AI, and that stops them when it runs out. No metering code, no cron job, no reconciliation.

This is the second way to monetise on odnoga. It composes with subscriptions: sell a plan that grants 500 credits a month and let overage be a top-up, or run credits alone with no subscription at all.

How it works

  1. You name the unit and set how many units one US dollar buys.
  2. Every request that carries an end-user id is converted at that rate from its real AI cost and debited from that user's wallet.
  3. When the balance reaches zero the request is refused with 402 end_user_exhausted — before any vendor is called, so an empty wallet costs you nothing.

You are still billed by odnoga for what your users actually consume. The wallet is your retail layer on top: the margin between what you charge for a credit and what the AI costs is yours.

Turning it on

Billing → End-user wallets, per workspace:

SettingWhat it decides
EnabledWhether wallets are debited and enforced at all.
Unit nameWhat your users see — credits, tokens, runs. Cosmetic, but it appears in the error message.
Units per USDYour retail rate. 1,000 units per USD means one credit is a tenth of a cent of AI cost.
Starting grantUnits a wallet receives the first time odnoga sees that end-user id. Your free tier.
Block at zeroOn: an empty wallet returns 402. Off: the balance goes negative and nothing is refused — metering only.

Set units_per_usd deliberately. It is the exchange rate between your pricing and your cost, and changing it later does not restate balances already issued.

Topping up

Credit or debit a wallet whenever your own billing says so — after a Stripe payment succeeds, on a monthly plan refresh, or as a goodwill gesture:

// after your own payment webhook confirms the purchase
await fetch(`${ODNOGA_API}/airouter-mcp/…`, …)   // or the MCP tool below

In practice most teams do this from an agent or a back-office screen rather than in code. Over MCP:

ToolUse
wallets.settings.getRead the unit, rate, grant and whether zero blocks.
wallets.listEvery wallet with its balance; search by end-user id.
wallets.adjustCredit or debit one wallet, with a reason that lands in the ledger.
wallets.entriesThe ledger for one wallet — every debit, with the request behind it.

wallets.adjust takes the external end-user id: the exact string your requests send in x-odnoga-end-user. A wallet is created on first credit, so a typo funds a wallet nobody will ever spend from. Copy the id from wallets.list rather than typing it.

Handling the refusal

402 end_user_exhausted is a normal, expected outcome — not an incident. Catch it and show your own upgrade path:

const r = await openai.chat.completions.create({ … });
// on 402 with code end_user_exhausted:
//   → render your "you're out of credits" screen, link to your checkout

The message names the unit you configured, so it reads as your product rather than as an odnoga error.

What wallets do not do

  • They do not charge anyone. Money is collected by your Stripe account through end-user billing, or by whatever you already use. A wallet is a balance, not a payment method.
  • They do not apply to traffic with no end-user id. An unattributed request is billed to the workspace and passes through, whatever any wallet says — which is one more reason to send the header on every call.
  • They are per workspace. The same end user in two workspaces has two wallets.