Argos Panoptes sees all — a multi-chain, real-time deposit detector.
Pano monitors blockchain addresses across multiple chains (EVM, Bitcoin, Solana) and emits standardised deposit events the moment a transfer is detected. It is designed to run as a sidecar service or standalone daemon, feeding downstream systems via files, databases, message queues, webhooks, SSE, or WebSockets.
Pano watches blockchain addresses for incoming deposits. It ingests addresses via file, database, HTTP API, or AMQP queue; scans EVM, Bitcoin, and Solana chains through configurable JSON-RPC endpoints; deduplicates and tracks confirmations; and delivers standardised deposit events to files, databases, AMQP, webhooks, SSE, or WebSockets.
The processing flow is linear: ingress → detector → egress. All behaviour is driven by a single TOML config file.
Pano uses Cargo features to keep the default build minimal:
| Feature | Default | Enables |
|---|---|---|
sqlite |
yes | SQLite ingress/egress (via sqlx) |
server |
no | HTTP server, API, SSE, WebSocket, dashboard |
amqp |
no | AMQP (RabbitMQ) ingress/egress (via lapin) |
postgres |
no | PostgreSQL ingress/egress (via sqlx) |
pg |
no | Alias for postgres |
webhook |
no | HMAC-signed webhook egress delivery |
full |
no | All features: server, webhook, sqlite, postgres, amqp |
Enable features at build time:
cargo install pano --features "server,postgres,amqp"Config sections that require a disabled feature produce a clear error at startup, e.g.: pano.egress.webhook.enabled requires feature "webhook".
cargo install panoOr from source:
git clone https://github.com/melonask/pano
cd pano
cargo build --releasecp Config.example.toml Config.toml
# Edit chains, assets, and RPC endpoints, then:
pano --config Config.tomlOr with environment variable:
PANO_CONFIG=Config.toml panopano [OPTIONS] [COMMAND]
Commands:
ping Verify the daemon process is present (container healthcheck)
help Print this message or the help of the given subcommand(s)
Options:
-c, --config <CONFIG> Path to configuration file [env: PANO_CONFIG=Config.toml]
-h, --help Print help
-V, --version Print version
Pano reads Config.toml by default. Use --config to point at any TOML file, including a merged multi-package config (see Universal Integration Config below).
Pano is configured through a single TOML file. The configuration model has two layers:
- Shared root sections — reusable profiles for chains, assets, paths, transports, and stores. These may be shared with other packages (Ladon, Bria, Oracles) when running from a merged config.
[pano]namespace — Pano-specific behaviour: server, detector, ingress, egress, and override permissions.
Environment variable substitution uses ${VAR} (required) and ${VAR:-default} (optional fallback).
Pano can read a merged Config.toml that contains sections for multiple packages. It:
- Parses shared root sections (
[chains],[assets],[paths],[transports],[stores],[http],[log],[meta],[runtime]) - Parses the
[pano]namespace - Ignores
[ladon],[bria], and[oracles]sections - Rejects unknown fields inside
[pano]
This means the same Config.toml works for all packages:
ladon --config Config.toml
pano --config Config.toml
bria --config Config.toml
oracles --config Config.tomlPano resolves package-local references against shared profiles:
| Pano field | Resolves to | Local override |
|---|---|---|
pano.chains = ["eth"] |
[chains.eth] |
N/A |
pano.assets = ["eth"] |
[assets.eth] |
N/A |
pano.ingress.file.path_ref = "pano_watches" |
[paths.pano_watches].path |
path = "..." |
pano.egress.file.path_ref = "pano_events" |
[paths.pano_events].path |
path = "..." |
pano.ingress.amqp.transport = "local" |
[transports.amqp.local] |
N/A |
pano.egress.amqp.transport = "local" |
[transports.amqp.local] |
N/A |
pano.egress.webhook.transport = "ops" |
[transports.webhook.ops] |
URL, timeout, retries |
pano.ingress.sqlite.store = "pano" |
[stores.pano] |
N/A |
pano.egress.pg.store = "postgres" |
[stores.postgres] |
N/A |
Explicit package-local values override profile values. Unknown references fail with actionable errors.
All Pano behaviour lives under the [pano] namespace:
| Field | Default | Description |
|---|---|---|
chains |
[] |
Chain ids from [chains.<id>] to scan |
assets |
[] |
Asset ids from [assets.<id>] to detect |
Requires feature server.
| Field | Default | Description |
|---|---|---|
enabled |
false |
Start the HTTP server |
bind |
"0.0.0.0" |
Bind address |
port |
3210 |
Listen port |
prefix |
"v1" |
URL prefix for API routes |
api_key |
"" |
Optional shared API key (Bearer or X-Pano-API-Key header) |
dashboard_path_ref |
"" |
Path profile from [paths] for static dashboard |
dashboard_export |
false |
Export masked config.json and addresses.json into dashboard dir |
shutdown_timeout_secs |
1 |
Graceful shutdown timeout for background tasks |
| Field | Default | Description |
|---|---|---|
dedup_window_size |
100000 |
Max recent event keys for in-memory deduplication (0 = unbounded) |
delivery_workers |
8 |
Async workers for per-address egress override delivery |
delivery_queue_capacity |
4096 |
Queue capacity for override delivery |
command_queue_capacity |
256 |
Queue capacity between ingress and detector |
stale_event_eviction_multiplier |
10 |
Multiplier for stale event eviction threshold |
stale_event_eviction_min_blocks |
1000 |
Minimum blocks before stale event eviction |
max_decimals |
30 |
Maximum accepted asset decimal places |
Applied to all chains as default RPC tuning.
| Field | Default | Description |
|---|---|---|
max_concurrent |
10 |
Max concurrent RPC requests per chain |
delay_ms |
0 |
Fixed delay between RPC calls |
batch_size |
200 |
Block batch size for EVM/Bitcoin scan cycles |
scan_lookback_blocks |
50 |
Re-scan depth for reorg safety (Solana effective default: 500) |
scan_interval_secs |
5 |
Minimum seconds between scans |
scan_timeout_secs |
60 |
Max wall-clock seconds per chain scan |
max_native_scan_per_cycle |
100 |
EVM native-coin blocks scanned per cycle |
request_timeout_secs |
15 |
Per-request RPC HTTP timeout |
max_retries |
3 |
Retry rounds across configured endpoints |
retry_base_ms |
500 |
Base delay for exponential retry backoff |
solana_max_supported_transaction_version |
0 |
Solana getTransaction option |
solana_scan_mode |
"blocks" |
Solana scan mode: "blocks" or "signatures" |
evm_log_address_batching |
true |
EVM address-array batching for eth_getLogs |
| Field | Default | Description |
|---|---|---|
assets |
false |
Allow per-watch asset overrides |
| Field | Default | Description |
|---|---|---|
webhook / file / pg / sqlite / queue / http |
false |
Allow per-watch egress channel overrides |
| Field | Default | Description |
|---|---|---|
command_queue_capacity |
4096 |
Bounded command queue capacity |
[pano.ingress.file]—enabled,path_ref,path(override),poll_interval_secs[pano.ingress.http]— (requiresserver)enabled,path,transport[pano.ingress.sqlite]— (requiressqlite)enabled,store,poll_interval_secs,table[pano.ingress.pg]— (requirespg/postgres)enabled,store,poll_interval_secs,table[pano.ingress.amqp]— (requiresamqp)enabled,transport,exchange,routing_key,consumer_tag
[pano.egress.file]—enabled,path_ref,path(override),template[pano.egress.sqlite]— (requiressqlite)enabled,store,table[pano.egress.pg]— (requirespg/postgres)enabled,store,table[pano.egress.amqp]— (requiresamqp)enabled,transport,exchange,detected_routing_key,confirmed_routing_key[pano.egress.webhook]— (requireswebhook)enabled,transport,url(override),secret,signature_header,timeout_secs,max_retries,retry_base_ms[pano.egress.stream]— (requiresserver)enabled,sse,websocket,ws_heartbeat_secs,sse_keepalive_secs,broadcast_capacity
| Backend | Feature | Default | Notes |
|---|---|---|---|
| SQLite | sqlite |
Yes | In-process; ideal for single-instance and local dev. |
| PostgreSQL | pg or postgres |
No | Multi-instance; shared connection pooling. |
Database connections are configured through shared [stores.<id>] profiles:
[stores.pano]
driver = "sqlite"
url = "sqlite://data/pano/pano.db"
# Or for Postgres:
[stores.postgres]
driver = "postgres"
url = "${DATABASE_URL:-postgres://localhost/pano}"Ingress and egress sections reference stores via store = "<id>".
Instead of repeating file paths and connection details in every Pano section, define them once in shared profiles:
[paths.pano_watches]
kind = "file"
path = "data/pano/addresses.jsonl"
format = "jsonl"
[transports.amqp.local]
url = "amqp://localhost:5672"
reconnect_secs = 5
[transports.webhook.ops]
url = "${OPS_WEBHOOK_URL:-}"
timeout_secs = 10
max_retries = 3Then reference them from Pano:
[pano.ingress.file]
enabled = true
path_ref = "pano_watches"
[pano.egress.amqp]
enabled = true
transport = "local"
exchange = "pano.egress"
[pano.egress.webhook]
enabled = true
transport = "ops"| Variable | Description |
|---|---|
PANO_CONFIG |
Path to the TOML config file (default: Config.toml) |
RUST_LOG |
Log filter (e.g. pano=info, pano=debug) |
Any ${VAR} or ${VAR:-default} in config values is resolved at load time. Required vars without a default cause a load error.
See Config.example.toml for a complete annotated example with all sections.
┌──────────────────────────────┐
│ Ingress │
│ HTTP / File / DB / Queue │
└──────────────┬───────────────┘
│ Watch / Unwatch / SyncAll commands
▼
┌──────────────────────────────┐
│ Detector │
│ Chain scanners (BTC/EVM/SOL)│
│ Dedup window │
│ Confirmation tracker │
│ Egress router │
└──────────────┬───────────────┘
│ DepositEvent (broadcast)
▼
└──────────────────────────────┘
│ Egress │
│ File / DB / Queue / Webhook │
│ SSE / WebSocket │
└──────────────────────────────┘
| Chain type | CAIP-2 namespace | Notes |
|---|---|---|
| EVM-compatible | eip155 |
Native ETH and ERC-20 tokens via eth_getLogs |
| Bitcoin | bip122 |
Native BTC via getblock (verbosity 2) |
| Solana | solana |
Native SOL and SPL tokens via getBlock |
All events share this envelope:
{
"event_id": "01HXYZ...",
"event": "pano.deposit.detected",
"version": 1,
"occurred_at": "2025-06-01T12:00:00Z",
"data": {
"tx_id": "0xabc...",
"caip2": "eip155:1",
"symbol": "USDC",
"address": "0xrecipient...",
"block_number": 19000000,
"log_index": 3,
"amount": "1000000",
"sender": "0xsender...",
"confirmations": 1,
"timestamp": "2025-06-01T11:59:58Z"
}
}# Run tests with default features (sqlite only)
cargo test
# Run all tests including optional features
cargo test --features full
# Build with specific features
cargo build --features "server,postgres,amqp"
# Lint
cargo clippy --features full
# Format
cargo fmt# Unit and integration tests
cargo test --features full
# Run end-to-end tests (requires local blockchain nodes)
cargo test e2e_multichain --features full -- --ignored --nocaptureMIT OR Apache-2.0. See LICENSE in the repository root.