# Documents › **Deterministic Counters** # Deterministic counters (persist, inspect, bump) Deterministic outputs use per-keyset counters. The wallet reserves them atomically and emits a single event you can use to persist the "next" value in your storage. API at a glance: - `wallet.counters.peekNext(id)` – returns the current "next" for a keyset - `wallet.counters.advanceToAtLeast(id, n)` – bump forward if behind; no-op if already ahead (restore/sync) - `wallet.on.countersReserved(cb)` – subscribe to reservations (see [WalletEvents](./wallet_events/wallet_events.md) for subscription patterns) ** Optional:** - Depends on CounterSource: These methods will throw if the CounterSource does not support them. - `wallet.counters.snapshot()` – inspect current overall state - `wallet.counters.setNext(id, n)` – hard-set for migrations/tests ```ts // 1) Seed once at app start if you have previously saved "next" per keyset const wallet = new Wallet(mintUrl, { unit: 'sat', bip39seed, keysetId: preferredKeysetId, // e.g. '0111111' counterInit: loadCountersFromDb(), // e.g. { '0111111': 128 } }); await wallet.loadMint(); // Alternative to using counterInit for individual keyset allocation await wallet.counters.advanceToAtLeast('0111111', 128); // 2) Subscribe once, persist future reservations wallet.on.countersReserved(({ keysetId, start, count, next }) => { // next is start + count (i.e: next available) saveNextToDb(keysetId, next); // do an atomic upsert per keysetId }); // 3) Inspect current state, what will be reserved next const nextCounter = await wallet.counters.peekNext('0111111'); // 128 // 4) After a restore or cross device sync, bump the cursors forward // (see usage/restore_proofs.md for the full restore flow) const { lastCounters } = await wallet.restoreAll(); for (const [keysetId, last] of Object.entries(lastCounters)) { await wallet.counters.advanceToAtLeast(keysetId, last + 1); await saveNextToDb(keysetId, last + 1); } // 5) Parallel keysets without mutation const wA = wallet; // bound to '0111111' const wB = wallet.withKeyset('0122222'); // bound to '0122222', same CounterSource await wB.counters.advanceToAtLeast('0122222', 10); await wA.counters.snapshot(); // { '0111111': 137, '0122222': 10 } await wB.counters.snapshot(); // { '0111111': 137, '0122222': 10 } wA.keysetId; // '0111111' wB.keysetId; // '0122222' // 6) Switch wallet default keyset and bump counter await wallet.counters.snapshot(); // { '0111111': 137, '0122222': 10 } wallet.keysetId; // '0111111' wallet.bindKeyset('0133333'); // bound to '0133333', same CounterSource wallet.keysetId; // '0133333' await wallet.counters.advanceToAtLeast('0133333', 456); // Counters persist per keyset, so rebinding does not reset the old one await wallet.counters.snapshot(); // { '0111111': 137, '0122222': 10, '0133333': 456 } await wA.counters.snapshot(); // { '0111111': 137, '0122222': 10, '0133333': 456 } await wB.counters.snapshot(); // { '0111111': 137, '0122222': 10, '0133333': 456 } ``` > **Note** The wallet does not await your callback. > If saveNextToDb (or similar) is async, handle errors to avoid unhandled rejections > For more on lifecycle management, see [WalletEvents](./wallet_events/wallet_events.md) --- ## Shared CounterSource across wallet instances By default each `new Wallet(...)` creates its own internal counter source. If your app creates multiple wallet instances for the same seed (e.g. short-lived wallets per operation), each instance gets an independent copy seeded from `counterInit` — and concurrent operations can reserve **overlapping counter ranges**, causing "outputs have already been signed" errors. Use `createEphemeralCounterSource()` to create a single shared source and pass it to every wallet via the `counterSource` option: ```ts import { Wallet, createEphemeralCounterSource } from '@cashu/cashu-ts'; // Create once at app start, seeded from your persisted counters const counters = createEphemeralCounterSource(loadCountersFromDb()); // Every wallet instance shares the same source — no overlapping reservations const walletA = new Wallet(mintA, { unit: 'sat', bip39seed, counterSource: counters }); const walletB = new Wallet(mintB, { unit: 'sat', bip39seed, counterSource: counters }); ``` ### Persisting counter state The ephemeral source is memory-only — counters do not survive page reloads. Use `wallet.on.countersReserved` to persist after every operation: ```ts function wireCounterPersistence(wallet: Wallet) { wallet.on.countersReserved(({ keysetId, next }) => { saveNextToDb(keysetId, next); // your atomic save function }); } wireCounterPersistence(walletA); wireCounterPersistence(walletB); ``` Because the source is shared, the global event on any wallet instance reflects the true cursor — there is no need for per-operation `onCountersReserved` callbacks in your builder chains. ### counterSource vs counterInit | Option | When to use | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | `counterInit` | Single wallet instance, or you don't need cross-wallet coordination. Seeds a wallet-local ephemeral source. | | `counterSource` | Multiple wallet instances for the same seed, or you need persistence/custom storage. Takes precedence over `counterInit`. | ### Custom CounterSource implementations `createEphemeralCounterSource` returns the built-in in-memory implementation, which survives restarts when paired with the `countersReserved` persistence above. For multi-wallet use inside a single app instance, it is usually enough. Implement `CounterSource` yourself when the cursor must live in your storage: when several tabs or processes reserve from one DB, or when a crash between reserving and saving must not risk reusing a counter. Each method is then one atomic transaction: ```ts import type { CounterSource, CounterRange } from '@cashu/cashu-ts'; class IndexedDbCounterSource implements CounterSource { async reserve(keysetId: string, n: number): Promise { // atomic read-and-increment in your DB } async reserveAt(keysetId: string, start: number, count: number): Promise { // one transaction: throw if start < next, else SET next = start + count } async advanceToAtLeast(keysetId: string, minNext: number): Promise { // conditional update: SET next = max(next, minNext) } // Optional: snapshot(), setNext() } ``` `reserveAt` claims a caller-chosen range instead of taking the next free one, and throws when `start` is already below the cursor. Manual deterministic counters go through it, so a counter that another operation has already been given is reported rather than quietly reused. Do the check and the update in one transaction: splitting them leaves a window for a concurrent `reserve` to hand out counters inside the range.