Coldtill Get Coldtill

Architecture

Paste into Claude Code, Codex, Cursor or your favourite coding agent. View Markdown

Coldtill is one static Go binary with the React dashboard embedded. It runs as four processes that share a PostgreSQL database and never call each other directly.

flowchart LR
  customer[Customer browser] --> caddy[Caddy TLS]
  shop[Merchant backend] --> caddy
  caddy --> api
  api --> pg[(PostgreSQL)]
  watcher --> pg
  sweeper --> pg
  payouts --> pg
  watcher --> rpc[RPC nodes]
  sweeper --> rpc
  payouts --> rpc
  watcher --> hooks[Merchant webhooks]
  api --> swap[Swap providers]
Process Responsibilities Holds a hot key Holds COLDTILL_SECRET
api Merchant API, dashboard, hosted checkout, conversion quotes no yes
watcher Chain watchers, confirmations and reorgs, payment expiry, conversion tracking, webhook delivery no yes
sweeper Deploys EVM deposit contracts when needed, sweeps EVM and Solana deposits to cold wallets yes no
payouts Signs, broadcasts, fee-bumps and confirms approved payouts yes no

The split matters for security: the process exposed to the internet (api) holds no private key, and the processes that hold keys accept no network connections. See Security.

Source layout

Path Contents
cmd/coldtill Entry point, init wizard, process wiring
internal/api HTTP handlers: merchant API, dashboard API, checkout, branding assets
internal/payment Payment lifecycle: create, attribute deposits, fill status, expiry, webhook events
internal/chains/evm, sol, btc Per-chain clients, watchers and sweepers
internal/rpcpool Node pools with preferred node, failover and health checks
internal/payout Payout creation, limits, approval and the signing loop
internal/convert Auto-convert: providers, quotes, tracking, on-chain delivery checks
internal/webhook Signing, queueing and delivery with retries
internal/branding Logo upload normalisation
internal/hotwallet, internal/secrets age-encrypted hot keys, sealed secrets
internal/netguard Outbound HTTP that refuses private addresses
internal/store Migrations, sqlc queries and generated code
contracts/ Foundry project: DepositFactory, DepositWallet, tests and fuzzing
solana/ Anchor deposit program
web/ React dashboard and checkout (Vite)
sdk/typescript, plugins/woocommerce Integrations
deploy/ Docker Compose, Caddyfile, install script

Data model

The main tables, simplified:

  • projects: settings, branding, sweep thresholds, payout limits, auto-convert options.
  • customers: one row per merchant customer ID within a project.
  • customer_addresses: each customer's permanent deposit address per chain family, with its derivation index.
  • payments: amount in USD, status, filled and pending USD, expiry. A customer has at most one open payment.
  • deposits: every incoming transfer, with block number, confirmations and status (pending, confirmed, orphaned).
  • deposit_wallets: per project and family, the cold wallet, factory or program config, and per-network deployment state.
  • sweeps and sweep_addresses: batched sweep transactions and the addresses they covered.
  • hot_wallets: encrypted keys. project_hot_wallets assigns them to projects.
  • payouts and address_book: outgoing payments and approved destinations.
  • conversions: auto-convert swaps, their quotes, provider status and verified delivery.
  • webhook_endpoints and webhook_deliveries: endpoints with sealed secrets, and the delivery queue.
  • rpc_nodes and networks: node pools and per-network settings (confirmations, enabled, last processed block).
  • audit_log: who did what, for every sensitive dashboard action.

Payment lifecycle

  1. Create. POST /api/v1/payment upserts the customer and replaces their previous open payment, unless that payment has a deposit still confirming, in which case the request is refused with 409.
  2. Address. When the customer picks a chain at checkout, Coldtill assigns their permanent address for that family, deriving it if needed. EVM addresses are counterfactual: the contract is only deployed when the first sweep needs it.
  3. Detect.
    • EVM: the watcher scans blocks in bounded ranges for ERC-20 Transfer logs to watched addresses, and periodically reconciles native balances.
    • Solana: the watcher polls signatures for each deposit address and reads token balance changes on the canonical token account.
    • Bitcoin: the watcher polls Esplora for each watched address.
  4. Confirm. Deposits become confirmed after the network's configured confirmations, with block hashes re-checked. A deposit whose block disappears in a reorg goes back to pending; one whose transaction vanishes is orphaned and reversed.
  5. Fill. Confirmed USD is compared with the amount owed, with a 1% tolerance: PARTIALLY_FILLED, FILLED or OVER_FILLED. Prices come from CoinGecko with Coinbase as fallback; stablecoins count as $1.
  6. Notify. Every change queues a webhook. Closing a payment sends exactly one final event.
  7. Expire. Open payments past their expiry (24 hours by default) become CANCELLED. Late deposits still count toward the filled amount, but don't reopen the payment.

Sweeps

The sweeper runs per project and network:

  1. It lists deposit addresses holding at least the project's activation minimum in confirmed, unswept value.
  2. It sweeps when any trigger fires: the total reaches the sweep amount ($100 by default), enough addresses hold funds (20), or the oldest unswept deposit has waited the sweep interval (24 hours). Gas must also be a small enough share of the value, and a failed sweep backs off for 10 minutes.
  3. EVM: a single transaction to your factory deploys any missing deposit contracts and flushes each one. Tokens that fail to transfer emit FlushFailed; those addresses stay unswept and are retried later.
  4. Solana: a transaction with one sweep_sol or sweep_token instruction per account, sized to fit the 1232-byte limit. The canonical token account is always swept; at most three other token accounts worth at least $1 each are included, so dust accounts can't block a sweep.
  5. Every sweep is signed and stored before it's broadcast, then tracked until it's confirmed, replaced or failed.

Payouts

Payouts are created by the dashboard or the API, checked against limits and the address book, held for approval if needed, then picked up by the payouts process. It signs, stores, broadcasts, fee-bumps and confirms. See Payouts.

Nonces and shared hot wallets

The sweeper and payouts processes can share an EVM hot wallet. Nonces are reserved under a Postgres advisory lock per wallet and network, inside the same transaction that stores the signed transaction. If a transaction fails permanently while a later nonce is already in flight, Coldtill fills the gap with a zero-value self-transfer so the later transaction can still be mined.

Node pools

Each network has a pool of RPC nodes (Esplora for Bitcoin). The preferred node is tried first, and a failing node is skipped for a cool-down period. A health loop probes every node so the dashboard can show status and the last processed block. URLs are redacted in errors, since providers often embed API keys in them. Decisions where one node's word isn't enough ("this payout never landed", "this Bitcoin deposit was dropped") ask every node.