# Architecture

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.

```mermaid
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](https://coldtill.com/docs/security.md).

## 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](https://coldtill.com/docs/payouts.md).

## 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.

---

Source: https://coldtill.com/docs/architecture/ · All Coldtill docs: https://coldtill.com/llms.txt
