Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Wallet

Consumable balances and economy ledger with atomic spend and lazy regen, as a Bijection component

Bijection source
git clone https://github.com/bijectionhq/wallet.git
cd wallet
git checkout 86623289d0d8346f17c35e43618569181392c135

Bijection 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.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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 max ceiling, multi-currency, transfers — with a SELF_TRANSFER guard.
  • 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

MethodKindResult
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)querynumber (regen-aware)
balances(ctx, subjectRef)query{ currency, amount }[]
history(ctx, subjectRef, currency, limit?)queryLedgerEntry[] (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 });
HookArgsResult
useBalance(balanceRef, { subjectRef, currency })host's balance query refnumber | undefined
useBalances(balancesRef, { subjectRef })host's balances query refBalanceEntry[] | 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.

Keywords