`routstrd update` gave the whole release download the same 30 s budget as the connection: `AbortSignal.timeout(FETCH_TIMEOUT_MS)` was attached to the fetch, then the 38 MB archive was read with `arrayBuffer()` under that same signal. Anything slower than ~1.3 MB/s failed with "The operation timed out." — including the 540 KB/s / 70 s connection this was reproduced on. Split the deadline in two: - connect + response headers: 30 s, unchanged. The releases API and SHA256SUMS are a few KB, and a hang there is a real failure worth surfacing quickly. - release archive body: 300 s. That is 38 MB / 300 s ≈ 127 KB/s ≈ 1 Mbit/s, about 4x headroom over the reproduction, while still bounding the silent failure (the command prints no progress) at five minutes. `fetchOrThrow` now owns an AbortController and reschedules the deadline once headers arrive, taking a `consume` callback so the body read is covered by the transfer budget. Aborts report the phase and URL instead of a bare "The operation timed out." Also bound `getLatestNpmVersion`, which called `fetch` with no timeout at all on the same `routstrd update` path; it already returns null on error, so a stalled registry now degrades to "latest version unknown" instead of hanging. Adds regression tests: a body slower than the connect budget must still install, and a body overrunning the transfer budget must fail with the phase-aware message.
routstrd
Routstr daemon - A CLI tool for managing routstr processes, with a built-in Cashu wallet.
Overview
routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It carries an in-process Cashu wallet (coco) for payments 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: In-process Cashu wallet, with Lightning, NWC, and NPC support
- Provider Routing: Automatically discovers and routes requests to available providers
- Config Management: Stores configuration in
~/.routstrd/
Requirements
The standalone release does not require Bun, Node.js, or npm. Installing from npm or running from source requires the Bun runtime.
Installation
Step 1: Install
Standalone binary (recommended):
Installs the standalone executable for Linux or macOS (x64 or arm64) into
$HOME/.local/bin. No Bun, Node.js, or npm required.
curl -fsSL https://github.com/Routstr/routstrd/releases/latest/download/install.sh | sh
Pin a version, change the install directory, or print the resolved asset without installing anything:
curl -fsSL https://github.com/Routstr/routstrd/releases/latest/download/install.sh \
| sh -s -- --version 0.4.9 --dir /usr/local/bin
The installer downloads the release archive, verifies it against the release
SHA256SUMS, and only replaces an existing routstrd once the checksum matches
and the extracted binary reports the expected version.
Manual install
Download the archive for your operating system and architecture from the latest GitHub Release. Release archives are available for Linux and macOS on x64 and arm64.
grep "routstrd-v0.4.9-linux-x64.tar.gz" SHA256SUMS | shasum -a 256 -c -
tar -xzf routstrd-v0.4.9-linux-x64.tar.gz
mkdir -p "$HOME/.local/bin"
install -m 755 routstrd "$HOME/.local/bin/routstrd"
Substitute the version, platform, and architecture for the archive you
downloaded, and ensure $HOME/.local/bin is on PATH.
Installing the standalone binary is preferred over the npm package: the npm package runs through the Bun runtime, while the standalone executable has no runtime dependency.
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
SKILL.md is the complete per-command reference — every command,
subcommand, and flag. The sections below cover the common ones.
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
Refresh models and client integrations on demand:
routstrd clients --manual-refresh # same as `routstrd refresh`
Turn the daemon's scheduled refresh on or off (no restart needed):
routstrd clients --disable-automatic-refresh
routstrd clients --enable-automatic-refresh
Stop the daemon:
routstrd stop
See which providers/models the router is currently skipping (cooldowns):
routstrd cooldowns
routstrd cooldowns --json
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
Cooldowns
GET /cooldowns
Providers and models the router is currently skipping. Each entry reports its
scope (provider blocks every model on that provider, model blocks one),
when the cooldown started, and when it lifts. Expired entries are filtered out,
and the cooldown window comes from the SDK (cooldownDurationMs).
Automatic Refresh Settings
POST /settings/auto-refresh
Request body:
{ "enabled": false }
Enables or disables the scheduled refresh job. Persisted to the daemon's
config.json as autoRefresh.enabled and picked up on the next tick, so no
daemon restart is required.
Route Request
POST /v1/chat/completions
Any unmatched POST path is proxied to the selected provider with the incoming
path preserved, so POST /v1/messages (Anthropic Messages API) and
POST /v1/responses (OpenAI Responses API) work in their own formats too.
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.
If both ~/.routstrd/wallet and ~/.cocod contain different wallets, startup
refuses to migrate rather than picking a mnemonic for you. Run
routstrd wallet doctor to compare the two wallets (mnemonic fingerprints,
timestamps, and balances) and see which one to keep.
Configuration
Configuration is stored in ~/.routstrd/config.json:
{
"port": 8008,
"host": "127.0.0.1",
"provider": null,
"autoModelPath": false,
"autoRefresh": { "enabled": true }
}
autoRefresh.enabled (default true) controls the daemon's scheduled refresh
job, which re-fetches Nostr events, routstr21 models, and client integrations
every 21 minutes. Set it to false (or run
routstrd clients --disable-automatic-refresh) to turn the schedule off and
refresh manually with routstrd clients --manual-refresh. autoRefresh.intervalMs
overrides the 21-minute interval.
autoModelPath defaults to false. Set it to true to let the SDK automatically
choose and pin an advertised model path for deepseek-v4.1-flash requests.
Explicit x-routstr-model-path request headers work independently of this
setting and take precedence. Restart the daemon after changing autoModelPath.
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)ROUTSTRD_WALLET_DIR- Wallet data directory (default:~/.routstrd/wallet)COCOD_DIR- Legacy external cocod directory, used only for migration and exclusion (default:~/.cocod)
Development
Install dependencies:
bun install
Run the CLI from source:
bun src/index.ts <command>
Run the daemon:
bun run start
Run the tests:
bun test
Build a standalone executable for the current platform:
bun run build:binary
./dist/routstrd --version
Standalone installations update directly from GitHub Releases with
routstrd update. npm installations continue to update through Bun. PM2 is an
optional external dependency used only by routstrd service; normal daemon
operation does not require it.
When an update finds a process on the configured daemon port, it only stops that process if the wallet PID file confirms a live daemon owned by the same routstrd configuration. Otherwise the update remains installed, but automatic restart is refused so an unrelated daemon is not interrupted.
Existing PM2 registrations created by routstrd 0.4.x continue to work through a compatibility daemon entrypoint. Recreate the registration to use the unified CLI entrypoint and remove its legacy path dependency:
routstrd service uninstall
routstrd service install
pm2 save
Typecheck:
bun run lint
Manual chat-completions smoke test
With a funded daemon running, create or reuse a client API key and pass one or more current model IDs to the smoke script:
routstrd clients add --name smoke-test
ROUTSTRD_API_KEY=<api-key> bun run smoke <model> [model ...]
Set ROUTSTRD_BASE_URL to test a daemon at a different address. The script
makes live provider requests that may spend wallet funds, so it is intentionally
not part of bun test.
Publishing a standalone release
- Set a new
package.jsonversion and commit it. The release tag must be the same version prefixed withv, and the tag must not already exist. - Push the tag. The release workflow runs lint and tests, builds Linux and
macOS executables for x64 and arm64, smoke-tests them, verifies the archives
through
install.shitself, and publishes the archives withSHA256SUMSandinstall.sh. - Verify all four archives and
install.shappear in the GitHub Release and validate each checksum before announcing it. - In disposable environments for each platform, test
--version,--help, foreground startup failure, and backgroundstart,status, andstopwithout Bun onPATH. - Test
routstrd service installand restart behavior with PM2 in a disposable environment. Never run release/update lifecycle tests against a production daemon. When isolation is needed, use both a separateROUTSTRD_DIRand a non-production port in that configuration.
Project Structure
routstrd/
├── src/
│ ├── index.ts # CLI entry point with shebang
│ ├── cli.ts # Commander CLI commands
│ ├── daemon.ts # Compatibility daemon entrypoint (legacy PM2 registrations)
│ ├── start-daemon.ts # Daemon process launcher
│ ├── daemon/ # HTTP server, wallet, provider routing
│ ├── integrations/ # Client integrations (Claude Code, pi, OpenCode, ...)
│ ├── tui/ # Interactive usage monitor (`routstrd monitor`)
│ └── utils/ # Config, paths, daemon client, update checker
├── tests/ # Integration tests (unit tests sit beside their source)
├── scripts/smoke/ # Manual end-to-end smoke test
├── docs/plans/ # Design/migration plans not yet executed
├── package.json
└── tsconfig.json
License
MIT