Tempo CLI is a multi-crate workspace providing a command-line HTTP client with built-in MPP payment support, wallet identity management, and a release signing tool. The top-level tempo launcher lives in the main tempo repo (tempo/crates/ext/).
tempo-wallet (wallet identity/custody + sessions/services/transfer)
└── tempo-common (shared library)
tempo-request (HTTP client + payment)
└── tempo-common (shared library)
tempo-sign (release signing, standalone)
tempo-test (shared test infrastructure, dev-only)
tempo-common is the shared foundation. tempo-wallet and tempo-request are independent binaries that both depend on it. tempo-sign is a standalone build tool. tempo-test provides mock servers, fixture builders, and assertion helpers used by integration tests across crates.
Dependency flows top-down; lower layers never import from higher ones.
src/
├── cli/ — shared CLI infrastructure
│ ├── args.rs — GlobalArgs, parse_cli
│ ├── context.rs — Context struct (Config, NetworkId, Keystore, Analytics, OutputFormat, Verbosity)
│ ├── exit_codes.rs — process exit codes (ExitCode enum)
│ ├── format.rs — value formatting helpers (amounts, durations, timestamps)
│ ├── output.rs — OutputFormat, structured output helpers
│ ├── runner.rs — CLI lifecycle (run_cli, run_main)
│ ├── runtime.rs — tracing setup, color mode, error rendering
│ ├── terminal.rs — terminal output helpers (hyperlinks, field formatting, sanitization)
│ ├── tracking.rs — analytics tracking (track_command, track_result)
│ └── verbosity.rs — verbosity configuration
├── keys/ — key storage, signing, authorization
│ ├── authorization.rs — on-chain key authorization proofs
│ ├── io.rs — key file I/O (read/write keys.toml)
│ ├── keystore.rs — Keystore struct, key selection logic
│ ├── model.rs — key data model (KeyEntry, WalletType, KeyType)
│ └── signer.rs — signer resolution (EOA vs keychain)
├── payment/ — payment error classification and session management
│ ├── classify.rs — payment error classification and extraction
│ └── session/ — channel persistence, queries, close, tx signing
│ ├── channel.rs — on-chain channel queries (balance, state, grace period)
│ ├── close/
│ │ ├── cooperative.rs — cooperative (off-chain) channel close
│ │ └── onchain.rs — payer-initiated on-chain requestClose → withdraw
│ ├── store/
│ │ ├── model.rs — domain model (ChannelRecord, ChannelStatus, PendingClose)
│ │ └── storage.rs — SQLite persistence (open, insert, update, query)
│ └── tx.rs — Tempo transaction submission
├── analytics.rs — opt-out telemetry (PostHog)
├── config.rs — configuration file handling
├── error.rs — error types (ConfigError, TempoError, PaymentError, etc.)
├── lib.rs — module declarations and tempo_home()
├── network.rs — chain definitions (Tempo, Moderato), explorer config, RPC
└── security.rs — security utilities (safe logging, sanitization, redaction)
Wallet identity and custody operations: login, key management, sessions, services, and transfers.
src/
├── main.rs — entry point
├── args.rs — Cli struct (flattens GlobalArgs), Commands, SessionCommands, ServicesCommands
├── app.rs — build Context, dispatch commands, track analytics
├── analytics.rs — wallet-specific analytics events and payloads
├── prompt.rs — interactive prompt helpers
├── wallet/
│ ├── types.rs — wallet account types (balances, spending limits)
│ ├── query.rs — on-chain wallet queries
│ └── render.rs — wallet info rendering
└── commands/
├── login.rs — passkey authentication flow
├── logout.rs — disconnect wallet
├── whoami.rs — wallet status, balances, keys
├── keys.rs — key listing with balance and spending limit queries
├── transfer.rs — TIP-20 token transfers (amount, token, recipient, fee estimation)
├── auth.rs — shared browser/authentication utilities
├── debug.rs — debug info collection for support tickets
├── completions.rs — shell completions
├── fund/ — fund command (browser-based faucet/bridge flow)
├── sessions/ — session management
│ ├── list.rs — list active sessions (local + orphaned on-chain)
│ ├── close.rs — close sessions (cooperative, on-chain, finalize)
│ ├── sync.rs — sync local state with on-chain
│ ├── render.rs — session table rendering
│ └── util.rs — shared session helpers
└── services/ — MPP service directory
├── client.rs — service directory API client
├── model.rs — service data model
└── render.rs — service listing rendering
HTTP client with built-in MPP payment support. Handles 402 Payment Required challenges natively.
src/
├── main.rs — entry point
├── args.rs — Cli struct (flattens GlobalArgs), QueryArgs
├── app.rs — dispatch to request command
├── analytics.rs — request-specific analytics events and payloads
├── query/ — query command flow
│ ├── analytics.rs — query analytics tracking
│ ├── challenge.rs — 402 challenge detection and dispatch
│ ├── headers.rs — request header construction
│ ├── output.rs — response output formatting
│ ├── payload.rs — request body handling (--json, --data, stdin)
│ ├── prepare.rs — request preparation (URL, method, headers, body)
│ └── sse.rs — Server-Sent Events streaming
├── http/ — HTTP client and response handling
│ ├── client.rs — reqwest client construction
│ ├── fmt.rs — verbose HTTP formatting (-v output)
│ └── response.rs — response wrapper (status, headers, body)
└── payment/ — payment flows
├── challenge.rs — shared challenge parsing helpers
├── charge.rs — one-shot on-chain charge payment
├── lock.rs — per-origin file locking for channel operations
├── router.rs — payment mode dispatch (charge vs session)
├── types.rs — shared types (ResolvedChallenge, PaymentResult)
└── session/ — session-based payment
├── flow.rs — stage-driven session orchestration
├── open.rs — channel opening and initial credential handshake
├── voucher.rs — off-chain voucher signing and transport
├── streaming.rs — SSE streaming with per-token voucher top-ups
├── persist.rs — session persistence (save/update channel records)
├── receipt.rs — session receipt validation
└── error_map.rs — HTTP rejection → PaymentRejected mapping
Standalone tool for generating signed release manifests to authenticate build artifacts.
src/
├── main.rs — entry point
├── args.rs — CLI argument definitions (generate-key, sign, verify)
├── error.rs — signing-specific error types
├── key.rs — minisign keypair generation and loading
├── manifest.rs — release manifest construction
└── sign.rs — manifest signing and verification
Shared test infrastructure used by integration tests across crates. Not published.
src/
├── lib.rs — re-exports all modules
├── assert.rs — assertion helpers for CLI output
├── command.rs — test_command builder with proper config
├── fixture.rs — TestConfigBuilder for test setup
└── mock.rs — mock HTTP/payment servers
Implemented in tempo-request/src/payment/charge.rs. Handles single-request on-chain settlement.
- The server responds with HTTP 402 and a
WWW-Authenticateheader describing the payment terms. - The challenge is parsed via the
mppcrate. - A signed transaction is built using
mpp::TempoProviderand submitted on-chain. - The request is retried with an
Authorizationheader containing the payment credential (transaction hash).
This mode requires no persistent state — each request is independently settled.
Session orchestration is implemented in tempo-request/src/payment/session/. Shared session infrastructure (persistence, channel queries, close operations, tx signing) lives in tempo-common/src/payment/session/.
handle_session_request is stage-driven with explicit boundaries:
- Challenge stage — parses/validates the challenge and resolves normalized session identity.
- Deposit stage — derives deposit policy and wallet-balance clamp behavior.
- Reuse stage — discovers/revalidates reusable channels (local plus on-chain identity checks).
- Open stage — performs channel open and initial credential handshake.
- Request stage — executes the paid request and receipt persistence.
Session invariants are intentionally strict:
- Session challenge
methodDetails.chainIdis required; missingchainIdis rejected. - Paid SSE requests fail closed on stream timeout/retry exhaustion/incomplete termination.
- Persisted channel
cumulative_amountis monotonic and must never decrease.
Session HTTP rejection mapping is centralized in error_map.rs so flow.rs, open.rs, and streaming.rs share one sanitization and length-bounding policy for server-derived PaymentRejected.reason text.
- Voucher updates are attempted with
HEADfirst. - Fallback to
POSTwhenHEADis unsupported (405/501) or transport fails. - Voucher/top-up submissions use a dedicated reqwest client handle (separate from stream response reading) while preserving the same transport policy as the primary request client.
Streaming voucher retries are managed by an explicit coordinator in streaming.rs that owns pending-voucher state, retry counters, and stall-timeout backoff progression.
- On first request, a channel is opened on-chain with a deposit.
- Subsequent requests exchange off-chain vouchers — signed cumulative amounts — instead of on-chain transactions.
- SSE streaming is supported: per-token voucher top-ups are issued as streamed data arrives.
- Channel state persists across CLI invocations in a SQLite database (
channels.db). - Channels can be closed explicitly. Local rows track explicit lifecycle state (active, closing, finalizable, finalized). Orphaned channels and close readiness are derived from on-chain state when needed.
requestClose()starts the escrow grace window.withdraw()is attempted whennow >= closeRequestedAt + gracePeriod.- The CLI does not currently add an extra cushion beyond contract grace by default.
- Missing or invalid
Payment-Receipton otherwise successful paid responses emits warnings. - Runtime requests are not failed solely for missing/invalid receipts.
Error handling follows a typed-boundary model:
- Prefer source-carrying variants (
*Source) when an underlying error object exists. - Preserve user-facing wording stability at CLI boundaries by keeping display strings deterministic.
- Reserve free-form string reasons for business-rule rejections where no concrete source error exists.
Compatibility exceptions are explicit and regression-tested:
- Payment classification keeps
NetworkError::Http(...)as an opaque fallback for unmatched provider errors. - Router network mismatch intentionally uses
PaymentError::ChallengeSchemawith the preserved wording:Server requested network '...' but --network is '...'.
Browser-based WebAuthn wallet created via Tempo's passkey flow (login.rs). Authentication is delegated to the browser; the wallet address and key authorization are stored locally.
Locally generated or imported secp256k1 private key. The private key is stored inline in a mode-0600 keys.toml file.
Determined by the relationship between wallet_address and key_address (tempo-common/src/keys/signer.rs):
- Direct EOA signing — when the wallet address equals the key address, transactions are signed directly.
- Keychain (smart wallet) signing — otherwise, transactions are signed with the authorized sub-key and include the on-chain key authorization proof.
Key selection is deterministic: passkey → first key with inline key → first key (lexicographic).
- SQLite database stored at
$TEMPO_HOME/wallet/channels.db(default:~/.tempo/wallet/channels.db). - Keyed by
channel_idwith an origin index for reuse lookups. ChannelRecordstores channel state: channel ID, cumulative amount, deposit, payer/payee/token identity, and challenge echo data.- No fixed TTL is enforced; channels have no implicit expiry in local persistence.
- Pending closes are tracked separately for grace-period finalization.
- Monotonic channel accounting is enforced at storage update boundaries (
update_channel_cumulative_floor).
Protocol-critical behavior delegated to mpp is locked with local boundary tests so upstream changes cannot silently alter client conformance.
- EIP-712 voucher signatures are verified as domain-bound to
chain_idandverifying_contract. - Voucher verification is locked to canonical 65-byte signatures, and compact ERC-2098 signatures are normalized to canonical form at the local boundary before verification.
- Unknown-field tolerance is verified for session request, credential payload, and receipt parsing.
- RFC 9457 extension-field passthrough is verified in local problem parsing.
Boundary tests live in crates/tempo-request/tests/mpp_boundary.rs.
This repository is a client/reference wallet implementation. It enforces client-side requirements from the session spec and intentionally does not implement server-only operational MUSTs.
Server-side concerns explicitly out of scope include voucher rate limiting/anti-DoS policy, challenge-to-voucher audit trail persistence, receipt issuance guarantees, and per-session server accounting durability semantics.
This repo produces extension binaries (tempo-wallet, tempo-request) that are managed by the tempo launcher in the main tempo repo (tempo/crates/ext/). The launcher provides install, update, remove, and auto-update lifecycle management. tempo-sign is a build-time tool used in CI to produce signed release manifests — it is never distributed to end users.
When a user runs tempo wallet ..., the launcher:
- Looks for
tempo-walletnext to its own binary (exe_dir). - Falls back to
$TEMPO_HOME/binor~/.tempo/bin. - Searches
PATH. - If not found, attempts auto-install from the default manifest URL.
Releases are triggered by pushing a git tag matching tempo-wallet@<version> or tempo-request@<version>:
-
Build (
.github/workflows/build.yml) — Cross-compiles release binaries for four targets:linux-amd64,linux-arm64,darwin-amd64,darwin-arm64. Artifacts are uploaded to the GitHub Release. -
Sign — The
publishjob buildstempo-signfrom source, then signs every binary in the artifacts directory.tempo-signproduces a release manifest (manifest.json) containing:version— semver version stringdescription— short extension descriptionbinaries— per-platform map of{ url, sha256, signature }skill/skill_sha256/skill_signature— optional agent skill file metadata
-
Upload — Signed binaries and the manifest are uploaded to Cloudflare R2 at
s3://tempo-cli/extensions/<package>/:- Latest:
extensions/tempo-wallet/manifest.json,extensions/tempo-wallet/tempo-wallet-darwin-arm64 - Versioned:
extensions/tempo-wallet/v0.1.5/manifest.json,extensions/tempo-wallet/v0.1.5/tempo-wallet-darwin-arm64 - A
VERSIONfile is written containing the latest version string.
- Latest:
Binaries are served via https://cli.tempo.xyz/extensions/<package>/....
Signing uses minisign (Ed25519-based):
-
Signing (
tempo-sign) — Each binary is signed with a minisign secret key stored as a CI secret (RELEASE_SIGNING_KEY). The trusted comment includesfile:<platform-binary-name>andversion:<version>tab-separated tokens. -
Verification (
tempo/crates/ext/src/installer/verify.rs) — On install, the launcher:- Downloads the binary and computes its SHA-256 digest.
- Compares the digest against the manifest's
sha256field. - Verifies the minisign signature using the hardcoded public key.
- Checks that the signature's trusted comment contains the expected
file:andversion:tokens — this prevents cross-extension substitution and version replay attacks.
The hardcoded public key lives in the launcher source: RWTtoEUPuapAfh06rC7BZLjm1hG40/lsVAA/2afN88FZ8/Fdk97LzJDf.
Both the manifest URL base and public key can be overridden via TEMPO_EXT_BASE_URL and TEMPO_EXT_PUBLIC_KEY for testing.
When the launcher dispatches to an already-installed extension, it checks for updates at most once every 6 hours (controlled by the registry's checked_at timestamp):
- Fetches
manifest.jsonfrom the default URL. - If the manifest version is strictly newer (semver comparison), downloads, verifies, and replaces the binary.
- On failure, the existing binary is always used — auto-update never blocks execution.
- Auto-update is disabled when
TEMPO_HOMEis set (managed/test environments).
Version pinning (tempo add wallet 1.0.0) disables auto-install but still checks and notifies the user when a newer version is available.
Extensions can optionally bundle an agent skill file (SKILL.md) for coding assistants. During install:
- The skill file URL, SHA-256, and signature are read from the release manifest.
- The file is downloaded, checksum-verified, and signature-verified (same minisign flow as binaries, with a
skill:<package>trusted comment). - The skill is installed into every detected coding assistant's
skills/tempo-<extension>/SKILL.mddirectory (Claude Code, Amp, Cursor, Copilot, Windsurf, etc.).
Currently only tempo-request ships a skill file (the SKILL.md at the repo root).
The launcher persists extension state in $TEMPO_HOME/extensions.json (or ~/Library/Application Support/tempo/extensions.json on macOS):
{
"extensions": {
"wallet": {
"checked_at": 1710864000,
"installed_version": "0.1.5",
"pinned": false,
"description": "Manage your Tempo Wallet"
}
}
}The registry is not file-locked — concurrent tempo invocations use last-writer-wins, which is acceptable since the data is limited to timestamps and version strings.
The default manifest URL follows the pattern:
https://cli.tempo.xyz/extensions/tempo-<extension>/manifest.json
For a specific version:
https://cli.tempo.xyz/extensions/tempo-<extension>/v<version>/manifest.json