# Ledger wallet (Agent Skill)

> A customer wallet as an append-only ledger with a balance per currency: Stripe Checkout top-ups, orders paid from the wallet, by card or split between them, refunds to source and staff adjustments.

Page: https://timerise.ai/skills/ledger-wallet

Agent Skill `ledger-wallet`: https://github.com/timerise-ai/ledger-wallet. Category: Payments & compliance. Runs on Next.js; backend: Firestore or Postgres behind a store seam, one conformance suite for both.

Latest release: v0.1.4 (2026-09-28).

Install: npx skills add timerise-ai/ledger-wallet. Timerise builds it on a brief; the price comes from the brief.

Gyms, studios, clubs and venues sell prepaid credit. A customer tops up a balance and spends it on bookings, tickets or services. Some orders are paid from the balance, some by card, and some by both.

Storing a number is the easy half. The hard half is keeping it right while requests retry, webhooks arrive twice or never, and a card and a balance pay for the same order. A balance that can be spent twice, or a top-up credited twice, is money lost or a customer wronged.

This skill gives a coding agent the wallet the way we build it: every balance is the sum of an append-only ledger, every movement is keyed by what caused it so a retry changes nothing, and a mixed payment holds the balance part until the card pays. Refunds go back to where the money came from. Staff adjustments need a reason, and the history explains every balance.

Build a customer wallet as an append-only ledger with a balance per currency: stored credit topped up by Stripe Checkout, orders paid from the wallet, by card, or split between them, refunds that return each part to where it came from, and staff adjustments with a required reason. Use when: (1) customers prepay or top up a balance and spend it on bookings, tickets, orders or services, (2) a payment must combine a balance with a card, or a refund must go back to the balance, (3) balances exist in more than one currency, (4) an existing balance or credit module needs auditing, (5) the user mentions: wallet, stored credit, store credit, account balance, prepaid balance, top up, top-up, add funds, pay with balance, pay with wallet, split payment, hybrid payment, partial balance payment, refund to wallet, balance history, transaction history, multi-currency balance, ledger, double-entry, idempotent credit, idempotency key, reconcile balance, BalanceLedger, checkout.session.completed, async_payment_succeeded, "credit the customer", "admin balance adjustment". Carries a pure ledger core with refs that make every retry a replay, holds that keep a mixed payment's balance part reserved until the card pays, per-currency limits floored at Stripe's minimum charge, the currency list taken as the skill argument with USD by default, Firestore and Postgres stores verified by one conformance suite, and 97 tests. Next.js App Router; the store, auth and tenancy sit behind a seam. Not loyalty points, not gift cards, not a crypto wallet, not currency conversion, not marketplace payouts.

This skill was written by the engineer who has shipped this module. The earlier implementation it was audited against was the stored-credit balance of a multi-location venue-booking system. The templates hold the properties a wallet has to hold: a top-up is credited once, for what Stripe collected, however often the event arrives; a wallet payment commits with its order or not at all; a balance never goes below zero under concurrent spends; a hold settles exactly once; a refund returns what was paid, per source; a balance in one tenant is never spendable in another; the sum of entries equals the balance. The suites, 97 tests on Firestore and Postgres, state each one, and [`references/provenance.md`](references/provenance.md) has the record.

What it builds:
- Stripe Checkout top-ups
- Orders paid from the wallet
- By card or split between them
- A hold on the balance part until the card pays
- Refunds to source
- Staff adjustments with a reason
- Every movement keyed by a ref so a retry replays

When to use:
- Customers hold credit and spend it: prepaid visits, top-ups, store credit from refunds or goodwill.
- An order is paid from the wallet, by card, or by both, and refunds must follow the money back.
- Balances in several currencies, with the supported list chosen per app.
- Auditing an existing balance module: [provenance.md](references/provenance.md) has the audit ledger and the order of work.

When not to use:
- **Event registration with deposits**: the sibling `bookable-events` skill; it takes this wallet through its `BalanceLedger` port ([integration.md](references/integration.md)).
- **Payouts, marketplace balances, connected accounts**: the sibling `stripe-connect-subscriptions` skill.
- **Loyalty points, gift cards, vouchers**: separate products with their own expiry and liability rules.
- **Currency conversion or FX wallets**: this wallet never converts; each currency is its own balance.
- **Walk-up terminals**: the sibling `booking-kiosk` skill owns the unattended flow.

Non-negotiables:
- Every balance change is an entry, posted through the wallet. Seeds, imports and corrections included, because a balance written anywhere else can no longer be explained or reconciled. The reconciliation test holds that a write outside the ledger is detected.
- Tenant and customer come from the session or the webhook endpoint, never the request body. A body field lets anyone move money in someone else's wallet or another tenant's. Tests hold that refs are scoped per tenant, that another tenant's session is not credited, and that another tenant's customer is a 404.
- Money moves once, with what it pays for. A wallet payment commits in the order's transaction; a mixed payment holds and captures only when the card pays, because a debit before the order exists has nothing to recover it from. The order tests on Firestore and Postgres hold that a failed order moves no money.
- Credit only what Stripe collected. The route that opens Checkout credits nothing; the webhook credits `amount_total` once, on a paid session, because the redirect is not proof of payment. Tests hold that a resent event and an unsettled async payment change nothing.
- Refund what was paid, to where it came from. The amounts the order recorded when it was paid, per source, never its current total, because a total can change after payment and an unpaid order has nothing to refund. The refund tests hold both parts, their idempotency and the unpaid case.

Not this:
- Event registration, tickets, no-show deposits: The sibling [`bookable-events`](https://github.com/timerise-ai/bookable-events) skill, which takes this wallet as its `BalanceLedger`
- Marketplace payouts, connected accounts, subscriptions: The sibling [`stripe-connect-subscriptions`](https://github.com/timerise-ai/stripe-connect-subscriptions) skill
- Walk-up touchscreen booking: The sibling [`booking-kiosk`](https://github.com/timerise-ai/booking-kiosk) skill
- Loyalty points, gift cards, vouchers: A loyalty or voucher system with its own expiry and liability rules
- Currency conversion, FX wallets: A treasury or FX provider; this wallet never converts

Booking features we build with it:
- Deposits and online payments: /features/deposits-and-payments
- Memberships and passes: /features/memberships-and-passes

What to tell your agent:
- Give customers a wallet on Postgres: top up with Stripe Checkout, pay orders from the balance, by card or split between both, and send refunds back where the money came from.
- Sell prepaid credit packs, keep balances in PLN and EUR on Firestore, and show each customer the full history of every movement.
- Let staff issue store credit as a refund or a goodwill gesture, with the reason recorded on every entry.

Agent eval results (newest per agent and prompt):
- Gemini CLI (gemini-3.8-flash) on v0.1.4, 2026-09-28: pass (Postgres)
- Codex CLI (gpt-6-astra) on v0.1.4, 2026-09-28: pass (Postgres)
- Claude Code (claude-opus-5-5) on v0.1.4, 2026-09-28: pass (Postgres)

Recent releases:
- v0.1.4 (2026-09-28): Fix release, from scoring the prompt-1 agent eval runs against 0.1.3. Apps built from 0.1.3 should copy in lib/wallet/split.ts, lib/wallet/webhook.ts and components/wallet/WalletPanel.tsx, and raise splitAttempt to attempt + 1 in onSplitFailed instead of adding 1.
- v0.1.3 (2026-09-28): Fix release, from scoring the prompt-1 agent eval runs against 0.1.2. Apps built from 0.1.2 should copy in lib/wallet/split.ts, lib/wallet/wallet.ts and lib/wallet/postgres-store.ts, and keep a splitAttempt on each order.
- v0.1.2 (2026-09-28): Documentation only; the skill content is unchanged from 0.1.1.
