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