Wallet & Ledger

Atomic wallet and economy ledger for Convex: per-subject multi-currency balances with idempotent grants, lazy time-regen, and spend that never goes negative.

Installation

npm install @vllnt/convex-wallet

About Wallet & Ledger

Wallet & Ledger brings consumable multi-currency balances to Convex — coins, gems, energy, credits — with atomic spend that never goes negative, idempotent grants, lazy time-regen without a cron, and a signed ledger entry on every transaction. Balances and history live in the component's own sandboxed tables (no blast radius into your schema), every credit path is host-gated so the host verifies IAPs and payments before calling `grant`, and `spend` and `transfer` are atomic inside Convex mutation transactions. Auth- and payment-provider-agnostic by construction, it runs identically on Convex Cloud and self-hosted convex-backend.

Benefits

Use cases

how to implement in-app purchase credits in Convex without double crediting

The @vllnt/convex-wallet component provides a `grant` method that accepts an idempotencyKey, typically a receipt ID, scoped per subject and currency. Calling `grant` multiple times with the same key credits the user exactly once, making it safe to retry IAP verification flows. The host is responsible for verifying the purchase before calling `grant`.

virtual currency wallet with atomic spend in Convex

The `spend` method in @vllnt/convex-wallet runs inside the Convex mutation transaction, so a balance can never go negative and concurrent spend attempts cannot double-debit. It returns `{ ok, balance, code }` so callers can handle `INSUFFICIENT` without try/catch around the entire mutation.

energy regeneration system in Convex without cron

Currencies configured with a `regen` object in the Wallet constructor regenerate automatically on read using the server clock, requiring no scheduled functions or cron jobs. The `balance` and `balances` queries return the regen-aware current value, and regen is capped at the configured `cap` value.

multi-currency ledger component for Convex game economy

The @vllnt/convex-wallet component supports arbitrary named currencies like coins, gems, and energy in a single Wallet instance. Each currency can have independent configuration for a max ceiling, regen rate, and regen cap. The `history` query returns a signed ledger of deltas with reasons for any currency per subject.

Frequently asked questions

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.

Links