Problem
-------
0ce4c07 pruned expired pending mint quotes purely locally at startup:
any quote past its bolt11 expiry with no recorded PAID/ISSUED
observation was failed without contacting its mint. That invariant is
only forward-looking: a quote can be paid before expiry while the
daemon is down, leaving no local observation behind. Failing such a
quote strands the paid funds at the mint: failed operations are skipped
by recoverPendingMintOperations(), so the claimable proofs are never
claimed.
Concrete case: receiveBolt11 invoice created, daemon stops, user pays
within expiry, daemon restarts after expiry: the old prune failed the
op without ever asking the mint.
Change
------
Replace failExpiredMintsLocally() with settleExpiredMintQuotes(),
which adds one bounded observation round before any local fail:
1. Select expired, unobserved pending mint quotes as before
(selectCleanupOperations, minAgeMs 0).
2. For each candidate, ask its mint for the quote state via
MintOperationService.observePendingOperation() (the same check the
mint sweep uses, reached through the existing structural cast)
under a shared 15s wall-clock budget
(EXPIRED_MINT_OBSERVATION_DEADLINE_MS).
3. Act on the answer from the mint:
- UNPAID ("waiting"): the expired quote can never be issued, so
failing it locally cannot strand funds; failPendingOperation().
- PAID/ISSUED ("ready"/"completed"): leave pending; the mint
recovery sweep (or the processor, via the emitted
mint-op:quote-state-changed event) finalizes it and claims the
proofs.
- unreachable/slow mint or unknown quote: leave pending so a later
startup can still recover it. Nothing is failed without a mint
confirmation.
Why a deadline
--------------
coco-core issues mint requests via bare fetch() with no timeout, so a
hung mint could otherwise stall this phase (and with it the recovery
promise that gates value-moving operations) for minutes. The shared
budget caps the whole round at 15s; the unobserved remainder stays
pending and is handled by the normal sweep (background, per-op
contained).
Why not keep the blind local fail
---------------------------------
The mint sweep treats UNPAID as "waiting" and never fails expired
quotes itself, so some form of pruning is still required to keep
recovery quick on wallets with many dead quotes. The observation round
keeps that property: confirmed-unpaid quotes are failed before the
sweep and never contacted again, while the unsafe case (paid before
expiry, never observed) now goes through normal recovery.
Side effects
------------
- Asking the mint also closes the narrower race from the old flow
(watcher records PAID between selection and fail): quotes are now
failed only when the mint currently reports UNPAID past expiry.
- Recovery phase strings are now "Settling/Settled expired mint
quotes"; settlement counts are logged to the startup stream.
- The explicit wallet cleanup command keeps its local-only semantics:
it is user-invoked, supports dry-run, and defaults to a 7-day
minimum age, giving ample observation opportunity beforehand.
Testing
-------
- New settleExpiredMintQuotes unit tests (6): mint-confirmed unpaid is
failed locally; PAID/ISSUED is left for recovery; unreachable mint
is left pending; hung mint is bounded by the shared deadline;
unexpired/observed quotes untouched.
- bun run lint (tsc --noEmit) passes.
- bun run build passes.
- Wallet/cleanup tests pass (56/56).
- Full bun test shows one pre-existing, unrelated failure
(mergeHermesConfig) that also fails on the parent commit.
routstrd
Routstr daemon - A CLI tool for managing routstr processes, similar to cocod (a Cashu wallet daemon).
Overview
routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It integrates with cocod for wallet management and uses the Routstr SDK to handle provider routing and model discovery.
Routstr for Teams
For team-based routing, see routstrd-auth.
Features
- Daemon Mode: Run routstrd as a background HTTP server
- Wallet Integration: Works with cocod for Cashu token management
- Provider Routing: Automatically discovers and routes requests to available providers
- Config Management: Stores configuration in
~/.routstrd/
Requirements
- Bun runtime
curl -fsSL https://bun.com/install | bash
Installation
Step 1: Install
Global with bun:
bun i -g routstrd
OR - From source:
git clone https://github.com/routstr/routstrd.git
cd routstrd
bun install
bun link
Step 2: Setup & Fund
routstrd onboard
routstrd receive <cashu> # receive a Cashu token
routstrd receive 2100 # to top up 2100 sats with lightning
Step 3: Integrate with Claude Code
routstrd clients add --claude-code # or --pi-agent / --opencode
Use Routstrd Skill
Tip: You can also install the routstrd skill so the agent can manage routstrd for you.
More Commands
Start Daemon
Start the background daemon:
routstrd start
With custom port:
routstrd start --port 9000
The daemon binds to 127.0.0.1 by default. To expose it on another interface:
routstrd start --host 0.0.0.0
Only expose the daemon behind appropriate network controls.
With specific provider:
routstrd start --provider https://your-provider.com
CLI Commands
Check daemon status:
routstrd status
Get wallet balance:
routstrd balance
Test connection:
routstrd ping
Stop the daemon:
routstrd stop
NPC (Lightning Address)
The in-process wallet registers the NPC (npubx.cash) plugin, which gives the daemon a persistent Lightning address backed by the wallet's Cashu mints. Payments to the address are imported into the wallet automatically (websocket push, plus manual sync on demand).
# Show your NPC Lightning address (username@npubx.cash, or npub fallback)
routstrd wallet npc address
# Claim a username (quote first, then confirm to pay the claim fee from the wallet)
routstrd wallet npc username myname
routstrd wallet npc username myname --confirm
# Manually sync paid NPC quotes into the wallet
routstrd wallet npc sync
Equivalent daemon endpoints: GET /wallet/npc/address,
POST /wallet/npc/username, POST /wallet/npc/sync.
Daemon API
The daemon exposes an HTTP server (default port 8008) with the following endpoints:
Health Check
GET /health
Route Request
POST /
Request body:
{
"model": "model-id",
"messages": [...],
"stream": false
}
Response:
{
"choices": [...],
"usage": {...}
}
Wallet storage
The in-process Cashu wallet stores its mnemonic and proof database in
~/.routstrd/wallet/. On first startup, an existing wallet in ~/.cocod/ is
migrated automatically after routstrd verifies that the legacy cocod daemon is
not running. Back up your mnemonic before upgrading.
Set ROUTSTRD_WALLET_DIR to override the canonical wallet directory. The
COCOD_DIR, COCOD_SOCKET, and COCOD_PID variables are retained only for
locating and excluding a legacy external cocod process.
Configuration
Configuration is stored in ~/.routstrd/config.json:
{
"port": 8008,
"host": "127.0.0.1",
"provider": null,
"cocodPath": null
}
Environment Variables
ROUTSTRD_DIR- Config directory (default:~/.routstrd)ROUTSTRD_SOCKET- Socket path (default:~/.routstrd/routstrd.sock)ROUTSTRD_PID- PID file path (default:~/.routstrd/routstrd.pid)
Development
Install dependencies:
bun install
Run CLI:
bun run start
Run daemon:
bun run start
Typecheck:
bun run lint
Project Structure
routstrd/
├── src/
│ ├── index.ts # Entry point with shebang
│ ├── cli.ts # Commander CLI commands
│ ├── cli-shared.ts # IPC utilities
│ ├── daemon.ts # HTTP server daemon
│ └── utils/
│ └── config.ts # Path configuration
├── package.json
└── tsconfig.json
License
MIT