Atomic wallet and economy ledger for Convex: per-subject multi-currency balances with idempotent grants, lazy time-regen, and spend that never goes negative.
npm install @vllnt/convex-walletWallet & 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.
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`.
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.
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.
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.
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.
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.
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.
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.
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.