Architecture

How sats is built: a portable core, a wallet library that every front end shares, the CLI and MCP server on top of it, and the one spend path that humans and agents share.

System shape

flowchart TD
    CLI["Human CLI (send, approve)"] --> Native["sats-wallet workflows"]
    MCP["MCP server (token, no keys)"] --> Requests["request::create"]
    Requests --> State["Store + watch-only wallet"]
    Native --> Exec["request::execute"]
    Native --> Core["sats-core"]
    Exec --> Core
    Exec --> State
    Exec --> Providers["Chain providers"]
    Native --> Providers

A human send unseals the seed for one command. An agent never signs: the MCP server files a request, and sats agent approve executes it in the human's process, for exactly that request. Both paths share preparation, the dust exclusion, fee estimation, and the sign-then-persist-then-broadcast tail.

sats-core owns deterministic wallet and authorization behavior. sats-wallet performs it against files, SQLite, and the network: storage, provider selection, preparation, and the request workflow. sats-cli owns the human: argument parsing, terminal output, passwords, and MCP stdio.

Crates

sats-core

The portable engine has no filesystem, network, clock, terminal, or async runtime. Callers pass in time and own the BDK wallet.

ModuleOwns
authzGrants, the verdict ladder (every proposal is ask or deny), reservations, refunds
requestThe request record and its state machine
intentThe canonical send intent and its digest
networkThe network policy: test networks only, mainnet refused
eventEvent kinds appended per request transition
tokenAgent tokens: mint, hash, constant-time verify
verifyRecomputing a PSBT's payments and fee from the wallet's descriptors
enginePSBT preparation and conservative UTXO exclusion
planPrepared spends and finalized transaction records
seedBIP-39 mnemonics; BIP-86 public and private descriptors
sealVersioned Argon2id/XChaCha20-Poly1305 envelopes
signerThe Signer trait and the local mnemonic signer
error, fmt, amountTyped errors, sat formatting, amount shorthand

sats-wallet

The native workflow every front end shares. It never talks to the human: no terminal output, prompts, or password reading. Warnings go through the log facade, sync progress through provider::Progress, and the signer arrives as a factory from a surface that has the human's authorization. The surface also chooses where the wallet lives: Store takes explicit config and data directories and never looks up a home directory.

ModuleOwns
configTOML configuration and network names
storePaths, atomic files, permissions, transactions, grants, requests, the event log
walletdThe SQLite-backed watch-only BDK wallet
providerPer-network provider, driver resolution, chain access, experimental Alkanes views
prepareThe preparation pipeline shared by human sends and approved requests
requestRequest create, dismiss, reconcile, and the executor (stage, commit)
spendThe shared tail: sign, persist, then broadcast; rebroadcast a saved transaction

sats-cli

The sats binary: the human CLI and the MCP server.

ModuleOwns
main, cliFlag parsing, network selection, dispatch. main.rs is a composition root only.
commandsHuman CLI workflows and their text and JSON output
keys, passwordUnsealing the seed; verifying the password without keeping anything
mcpThe MCP stdio server and tool schemas
uiTerminal presentation, the stderr warning sink, and the sync status line

sats-alkanes

Pure Alkanes encoding: alkane ids, LEB128 varints, cellpacks, the protostone/runestone envelope, bytecode hashing, and tolerant simulation views. Encodings follow the published alkanes-rs reference, frozen by unit vectors. Like sats-core, it does no I/O. Default builds don't include it: the experimental-alkanes feature adds sats alkanes inspect and simulate, and execution sits behind the development-only experimental-alkanes-execute feature.

sats-playground

sats-core compiled to WebAssembly for the website playground. Only the chain is simulated. Planning, UTXO exclusion, signing, sealing, and grant checks run the same core code as the native binary. A web page has no process boundary, so the playground demonstrates the grant model without the separation that enforces it natively. It is not a compatibility surface and must never gain filesystem, network, or store dependencies.

Persistent state

config.toml lives in the config directory and everything else in the data directory (see CLI). SATS_DIR puts both in one directory.

StatePathContents
Configurationconfig.toml (owner-only)Default network, fee targets, providers and their credentials
Seedseed.sealedPassword-sealed mnemonic, shared by every network
Wallet<network>/wallet.sqlitePublic descriptors and BDK chain state
Transactions<network>/transactions/<txid>.jsonRaw signed hex, broadcast status, payment metadata, origin
Grants<network>/grants/<agent>.jsonMode, limits, accounting, token hash. No key material.
Requests<network>/agent-requests/<agent>/<id>.jsonIntent digest, idempotency key, state, grant_id
Request locks<network>/agent-requests/<agent>/<id>.lockOS file lock held while a process executes or reconciles
Event log<network>/events/log.jsonlAppend-only, one JSON line per transition

Sensitive files are written atomically and owner-only. See Security.

Human send

sequenceDiagram
    participant H as Human
    participant C as CLI
    participant P as Providers
    participant E as Core
    participant S as Store
    H->>C: sats send address amount
    C->>P: sync, fee estimate
    C->>E: prepare PSBT
    C-->>H: amount, fee, confirmation
    C->>E: sign and finalize locally
    C->>S: save pending raw transaction
    C->>P: broadcast transaction
    C->>S: mark transaction broadcast

Preparation validates the address before any network I/O and refuses to plan on stale state. The PSBT stays in memory. Raw signed hex is saved before broadcast, so a failed broadcast leaves an exact retry.

Agent request

Filing happens in the MCP server and touches only the store:

sequenceDiagram
    participant A as Agent
    participant M as MCP server
    participant S as Store
    participant H as Human (CLI)
    A->>M: request_send(address, amount_sat, idempotency_key)
    M->>S: verify token, canonical intent, ladder (fee unknown)
    S-->>M: pending_approval (or denied)
    M-->>A: request_id + status
    H->>H: sats agent approve selects the request for review
    A->>M: check_request until it settles

Execution happens in the human's process, once, with the password:

sequenceDiagram
    participant H as Human
    participant C as sats agent approve
    participant P as Providers
    participant E as Core
    participant S as Store
    H->>C: sats agent approve (or an explicit id)
    C->>S: claim the request, re-check the grant (fee unknown)
    C->>P: sync, fee estimate
    C->>E: prepare PSBT; derive what it pays; ladder with the real fee
    C-->>H: wallet, network, recipient, amount, fee, total; confirmation and password
    C->>S: under the grant lock: check the grant instance, reserve budget, then record signing
    C->>E: construct the signer, sign, finalize
    C->>S: save the attributed transaction (before broadcast)
    C->>P: broadcast
    C->>S: record sent, or broadcast_pending

The reservation and the signing record are durable before the signer is constructed, which is why request::execute::commit takes the signer as a factory. What happens on failure at each step, and how a crash is reconciled, is specified in Security.

Providers

Each network has exactly one provider (mempool.space, Subfrost, or an Esplora server) for sync, fees, and broadcast. In experimental builds, Alkanes views come from the same provider, which must be Subfrost. Drivers are audited enums. Resolution is pure configuration work with no network I/O. Operations check the network when they run. See Providers.

Change map

ChangeOwnerProve it with
Coin selection, preparation, transaction recordssats-core::engine, planCore unit tests plus CLI and MCP paths
Grant rule or accountingsats-core::authzDecision edge cases, persistence order, MCP denial tests
What a PSBT is taken to paysats-core::verifyAdversarial tests: forged hints, extra outputs, foreign inputs
Request creation, execution, reconciliationrequestExecutor tests with the signer probe; tests/mcp.rs
CLI command or flagcli, commands, mainCLI integration test; docs/cli.md
MCP tool or schemamcp::serverMCP integration test; docs/mcp.md
Provider driver or choiceprovider, configMocked driver; network mismatch, unservable choice, failure tests
Persisted statestore, walletd, core modelRead and atomic-write tests
Keys or signingseed, seal, signer, keysSecurity unit tests and end-to-end signing

Read AGENTS.md before making any of these changes.