Wallet
Consumable balances and economy ledger with atomic spend and lazy regen, as a Bijection component
git clone https://github.com/bijectionhq/wallet.git
cd wallet
git checkout 86623289d0d8346f17c35e43618569181392c135Bijection repository access is required. Request access.
Description
Consumable balances and economy ledger with atomic spend and lazy regen, as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/wallet. Based on vllnt/convex-wallet. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Consumable balances and economy ledger with atomic spend and lazy regen, as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@vllnt/convex-wallet stores consumable balances and a ledger of value changes. Currency names and subject references come from your application, so a balance can represent credits, energy, points, or another unit.
Configure currencies and ceilings, then grant, earn, spend, or transfer amounts. Spending checks available balance atomically. Idempotency keys protect supported grant and spend paths, and history records deltas and reasons. Optional regeneration calculates replenishment from server time without a periodic refill job.
The host authenticates callers, verifies purchases, and controls every credit and debit. This is not a payment processor, bank account, or subscription system. UI, pricing, reward eligibility, and external payment reconciliation stay in your app.
Maintained by bntvllnt at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- AI products: grant credits after a verified purchase and spend them on generation.
- Mobile games: maintain coins or energy, with optional time-based regeneration.
- Reward programs: credit participation points and debit them when the host approves a redemption.
- Team tools: manage workspace balances for exports or processing operations.
How it works
From the upstream package documentation.
Configure currencies and ceilings, then grant, earn, spend, or transfer amounts. Spending checks available balance atomically. Idempotency keys protect supported grant and spend paths, and history records deltas and reasons. Optional regeneration calculates replenishment from server time without a periodic refill job.
The host authenticates callers, verifies purchases, and controls every credit and debit. This is not a payment processor, bank account, or subscription system. UI, pricing, reward eligibility, and external payment reconciliation stay in your app.
Upstream package FAQ
Does @vllnt/convex-wallet prevent balances from going negative?
Yes. The `spend` method in @vllnt/convex-wallet runs atomically inside the Convex mutation transaction. If the subject's balance is insufficient, the call returns `{ ok: false, code: 'INSUFFICIENT' }` without committing any change. Concurrent spends are also safe because Convex mutations are serialized.
How does idempotent grant work in @vllnt/convex-wallet?
The `grant` method accepts an `idempotencyKey` string that is scoped per subject reference and currency. If the same key has already been applied, the call returns the current balance without crediting again. This is intended for IAP or Stripe flows where a webhook or client retry might call grant more than once for the same receipt.
How does lazy regen work and why does it not need a cron job?
When a currency is configured with a `regen` object, @vllnt/convex-wallet stores the timestamp of the last known balance update. On each `balance` or `balances` query, the component computes how many regen intervals have elapsed using the server clock and adds the accrued amount up to the configured cap. No scheduled function or cron is required because the calculation happens at read time.
What happens if I pass an invalid or zero amount to earn, spend, or transfer?
Every value-moving method in @vllnt/convex-wallet validates the amount and rejects non-finite or non-positive values with the error code `INVALID_AMOUNT`. This prevents silent bugs from floating-point edge cases or client-supplied inputs.
How do I use @vllnt/convex-wallet with React?
The package exports optional tree-shakeable React hooks from `@vllnt/convex-wallet/react`, including `useBalance` and `useBalances`. These hooks take the host application's own re-exported query references rather than importing the component's internal API directly, so the component remains sandboxed. React is an optional peer dependency and is not bundled if unused.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Wallet with Bijection.
Read: https://bijection.com/components/wallet/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-wallet, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-wallet
Consumable balances and economy ledger with atomic spend and lazy regen, as a Convex component — hold per-subject balances of any opaque currency (coins, energy, gems, credits).
const wallet = new Wallet(components.wallet, {
currencies: { energy: { regen: { amount: 1, intervalMs: 10_000, cap: 100 } } },
});
await wallet.spend(ctx, subjectRef, "coins", 5, "purchase"); // atomic, never negative
const energy = await wallet.balance(ctx, subjectRef, "energy"); // regen-aware
Features
- Atomic spend — debits ride the Convex mutation transaction; never goes negative, never double-spends.
- Abuse-guarded amounts — every value-moving call rejects non-finite or non-positive amounts (
INVALID_AMOUNT). - Typed ledger — every earn / spend / transfer records a signed
delta(the actual change),reason, and timestamp. - Idempotent grants & spend — credit/debit exactly once per
idempotencyKey, scoped per(subjectRef, currency). - Lazy time-regen — energy-style currencies regenerate on read from the server clock, no cron.
- Per-currency
maxceiling, multi-currency, transfers — with aSELF_TRANSFERguard. - Typed error codes, self-pruning ledger cron, opaque host refs.
Installation
pnpm add @vllnt/convex-wallet
Peer dependency: convex@^1.41.0.
Usage
// convex/convex.config.ts
import { defineApp } from "convex/server";
import wallet from "@vllnt/convex-wallet/convex.config";
const app = defineApp();
app.use(wallet);
export default app;
// convex/economy.ts — host owns auth + payment confirmation.
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { Wallet } from "@vllnt/convex-wallet";
// `energy` regenerates 1 unit / 10s up to 100; `gems` caps at 9999; `coins` is static.
const wallet = new Wallet(components.wallet, {
currencies: {
energy: { regen: { amount: 1, intervalMs: 10_000, cap: 100 } },
gems: { max: 9999 },
},
});
export const buy = mutation({
args: { userId: v.string(), cost: v.number() },
handler: (ctx, { userId, cost }) => wallet.spend(ctx, userId, "coins", cost, "purchase"),
});
// Call AFTER the host has verified the IAP — idempotent on the receipt id.
export const creditPurchase = mutation({
args: { userId: v.string(), coins: v.number(), receiptId: v.string() },
handler: (ctx, { userId, coins, receiptId }) =>
wallet.grant(ctx, userId, "coins", coins, "iap", receiptId),
});
export const energy = query({
args: { userId: v.string() },
handler: (ctx, { userId }) => wallet.balance(ctx, userId, "energy"),
});
API Reference
| Method | Kind | Result |
|---|---|---|
earn(ctx, subjectRef, currency, amount, reason, opts?) | mutation | { balance } (clamped to max) |
grant(ctx, subjectRef, currency, amount, reason, idempotencyKey) | mutation | { balance } (credited once per key) |
spend(ctx, subjectRef, currency, amount, reason, opts?) | mutation | { ok, balance, code? } |
transfer(ctx, fromRef, toRef, currency, amount, reason) | mutation | { ok, balance, code? } |
balance(ctx, subjectRef, currency) | query | number (regen-aware) |
balances(ctx, subjectRef) | query | { currency, amount }[] |
history(ctx, subjectRef, currency, limit?) | query | LedgerEntry[] (newest-first) |
Full reference: docs/API.md — including per-currency config, error codes (INVALID_AMOUNT, INVALID_REGEN, INSUFFICIENT, SELF_TRANSFER), and the regen model.
React
Optional, tree-shakeable hooks at @vllnt/convex-wallet/react; react is an optional peer dep. Pass the host's own re-exported query refs — the component never imports your api.
import { useBalance, useBalances } from "@vllnt/convex-wallet/react";
import { api } from "../convex/_generated/api";
const energy = useBalance(api.economy.energy, { subjectRef: userId, currency: "energy" });
const all = useBalances(api.economy.allBalances, { subjectRef: userId });
| Hook | Args | Result |
|---|---|---|
useBalance(balanceRef, { subjectRef, currency }) | host's balance query ref | number | undefined |
useBalances(balancesRef, { subjectRef }) | host's balances query ref | BalanceEntry[] | undefined |
Security
- Auth- and payment-agnostic — the host gates access and verifies any IAP / Stripe purchase before calling
grant. - Tables are sandboxed; spend and transfer are atomic, so balances never go negative and grants never double-credit.
- Amounts and regen configs are validated, and time is server-sourced — a forged or replayed clock can't drive extra regen.
See docs/API.md.
Testing
pnpm test # single run
pnpm test:coverage # enforced 100% on covered files
Tests run against the real component runtime via convex-test (@edge-runtime/vm), not mocks.
Contributing
See CONTRIBUTING.md.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.