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
- You name the unit and set how many units one US dollar buys.
- 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.
- 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:
| Setting | What it decides |
|---|---|
| Enabled | Whether wallets are debited and enforced at all. |
| Unit name | What your users see — credits, tokens, runs. Cosmetic, but it appears in the error message. |
| Units per USD | Your retail rate. 1,000 units per USD means one credit is a tenth of a cent of AI cost. |
| Starting grant | Units a wallet receives the first time odnoga sees that end-user id. Your free tier. |
| Block at zero | On: 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:
| Tool | Use |
|---|---|
wallets.settings.get | Read the unit, rate, grant and whether zero blocks. |
wallets.list | Every wallet with its balance; search by end-user id. |
wallets.adjust | Credit or debit one wallet, with a reason that lands in the ledger. |
wallets.entries | The 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.