AnimicaPost-quantum L1 · SHA3-256 PoW
Operator Runbook · pool.animica.org

Run an Animica mining pool

A complete, ground-truthed guide to standing up the Stratum pool that powers pool.animica.org — from a synced node and a hot wallet through the fee levers, the on-chain payout scheduler, and exactly how you (the operator) get paid.

Ports 3333 · 3334 · 8550 Mode PPS + solo + XMR PoW SHA3-256 Payouts on-chain / 15 min Service animica-pool.service
01

Architecture & ports

The whole pool is one Python processpython -m animica.stratum_pool, run by the systemd unit animica-pool.service. That single process owns all three sockets, talks to your node over loopback JSON-RPC, and writes accounting to a local SQLite file. Everything else (web front-end, platform API, rig-rental) is a companion service behind nginx.

Minersxmrig fork / animica CLI
stratum+tcp →
Pool process:3333 pps+xmr · :3334 solo · :8550 api
JSON-RPC →
Animica node127.0.0.1:8545/rpc (docker)
monerod127.0.0.1:18081 · dual-mine
← credits →
pool.dbSQLite · shares/blocks/balances/payouts
→ on-chain
Payout schedulersigns tx from pool wallet · 15 min

The raw stratum ports 3333 and 3334 bind 0.0.0.0 directly on the Python process — they are not proxied by nginx. Only the read-only stats API (:8550) and the web apps sit behind nginx.

PortBindPurposeExposed how
33330.0.0.0Main PPS + Monero RandomX (protocol-sniffed on the first line)Raw TCP — open the firewall
33340.0.0.0True solo accountingRaw TCP — open the firewall
8550127.0.0.1Read-only stats / accounting API (FastAPI)nginx → pool.animica.org
8545127.0.0.1Animica node RPC (the pool reads templates here)loopback only (docker-proxy)
18081127.0.0.1monerod restricted RPC (XMR templates)loopback only
3000/4000/3020127.0.0.1Web front-end / NestJS API / rig-rentalnginx
02

Prerequisites

  • A fully-synced Animica mainnet node exposing RPC at http://127.0.0.1:8545/rpc. On the live host this is the docker container animica-mainnet-node publishing 8545 on loopback. The pool polls it for block templates (miner.getBlockTemplate) and submits found blocks (miner.submitBlock). If the node is behind, the pool mines a stale tip and finds nothing.
  • The Animica package on the box — the live pool runs from the repo checkout at /root/animica via its venv (/root/animica/.venv), not from site-packages. For a fresh install: pip install --upgrade animica gives you the same animica.stratum_pool module and the animica CLI.
  • A pool hot wallet — one Animica (anim1…, ML-DSA-65) address that receives every mined coinbase and signs payouts. Section 03.
  • (Optional) a pruned monerod for XMR dual-mining, with restricted RPC on 127.0.0.1:18081. Section 09.
  • A domain, DNS and TLS — an A record for your pool host, a Let's Encrypt cert, and firewall rules that let miners reach 3333/3334. Section 10.
Sanity check the node before anything else

The pool won't self-start a node when it can already reach one. Confirm the RPC is live and at the tip:

verify node rpc
$ curl -s http://127.0.0.1:8545/rpc \
    -d '{"jsonrpc":"2.0","id":1,"method":"chain.getHead","params":[]}' \
    -H 'content-type: application/json' | jq .result.height
03

Pool wallet & keys

Every ANM block your pool finds pays its full coinbase on-chain to one address — your pool hot wallet (ANIMICA_POOL_ADDRESS). The same wallet signs the payouts that go back out to miners. So this one key controls all pooled funds.

create the pool wallet (do this on a secure box)
# Generates an ml_dsa_65 (0x1003) keypair + bech32m anim1 address.
$ animica wallet create --name pool-hot
# → address: anim1zqp...   seed phrase: (24 words — WRITE DOWN, KEEP OFFLINE)
!
The seed never touches the pool host's config

Only the public address goes in /etc/animica/pool.env. That file's own header says it: "Public payout address only. Do not put wallet seeds, private keys, or RPC passwords here." Keep the seed offline. The payout scheduler signs with a key supplied out-of-band — it never reads a seed from the env file.

You need two addresses if you want a real operator cut (see Fees): the pool wallet, and a separate fee/treasury wallet you control. Keep both seeds offline. For XMR you also register a Monero receive address.

04

pool.env — annotated reference

The unit loads two config layers: the checked-in ops/profiles/mainnet.env (sourced by run.sh) and your per-machine overrides in /etc/animica/pool.env (EnvironmentFile). Everything below is an environment variable; there are no secrets here. This is the live mainnet configuration, annotated.

/etc/animica/pool.env
# ── Identity / coinbase ─────────────────────────────
ANIMICA_POOL_MODE=pps                     # pps | solo | both
ANIMICA_POOL_ADDRESS=anim1zqpkpp780…ws44wt   # coinbase + hot wallet (required)
ANIMICA_POOL_PAYOUT_WALLET=anim1zqpkpp780…ws44wt # signs payouts; defaults to POOL_ADDRESS

# ── Node RPC ────────────────────────────────────────
ANIMICA_RPC_URL=http://127.0.0.1:8545/rpc

# ── Stratum binds (raw TCP, NOT nginx) ──────────────
ANIMICA_STRATUM_BIND=0.0.0.0:3333          # pps + xmr (single-port detect)
ANIMICA_STRATUM_SOLO_BIND=0.0.0.0:3334     # true-solo listener
ANIMICA_POOL_API_BIND=127.0.0.1:8550       # stats API — keep loopback

# ── Fees (the only percentage levers) ───────────────
ANIMICA_POOL_ENA_FEE_BPS=0                 # PPS skim, bps. 0 = miners keep 100%
ANIMICA_POOL_ENA_TREASURY_ADDRESS=anim1zqp6ew0…qnpr2 # where the skim is paid
ANIMICA_POOL_SOLO_FEE_BPS=500              # 5% solo fee (see §08 caveat)

# ── Payout scheduler ────────────────────────────────
ANIMICA_POOL_PAYOUT_INTERVAL_SECONDS=900    # 15 min; 0 disables auto-payout
# ANIMICA_POOL_PAYOUT_MIN_AMOUNT=1               # nANM threshold (default 1)
# ANIMICA_POOL_PAYOUT_MAX_RECIPIENTS=100         # per-cycle cap

# ── Share difficulty (pinned for xmrig-compat) ──────
ANIMICA_STRATUM_MIN_DIFFICULTY=1.0
ANIMICA_STRATUM_MAX_DIFFICULTY=1.0
ANIMICA_STRATUM_SHARE_DIFFICULTY_FLOOR=1.0 # never emit set_difficulty < 1.0

# ── Monero dual-mining ──────────────────────────────
ANIMICA_POOL_XMR_ENABLED=1
ANIMICA_POOL_MONEROD_URL=http://127.0.0.1:18081
ANIMICA_POOL_XMR_ADDRESS=49RcNPVM…rXfJ        # pool's Monero receive addr

# ── Safety rails ────────────────────────────────────
ANIMICA_POOL_CREDIT_CAP_ENABLED=1          # credited can't outrun mined coinbase
i
Applying a change is always the same two steps

Edit /etc/animica/pool.env, then systemctl restart animica-pool.service. (If you edit the unit file itself, systemctl daemon-reload first.) There are no hot-reloaded fee flags — a restart re-reads the whole config.

05

The service & profile

The unit is a thin wrapper: it sources the mainnet profile, activates the venv, and launches the module. One process, restart-always.

/etc/systemd/system/animica-pool.service
[Service]
WorkingDirectory=/root/animica
Environment=PYTHONUNBUFFERED=1 HOME=/root PYTHONPATH=/root/animica/python:/root/animica
EnvironmentFile=-/etc/animica/pool.env          # leading '-' = optional
ExecStart=/root/animica/ops/run.sh --profile mainnet pool
Restart=always
RestartSec=5
KillSignal=SIGINT
lifecycle
$ systemctl start   animica-pool.service
$ systemctl restart animica-pool.service     # after any pool.env edit
$ systemctl status  animica-pool.service
$ journalctl -u animica-pool.service -f      # stratum + API logs (journald)
06

The Stratum surface

Animica's native PoW is SHA3-256 over the canonical CBOR block header — a share is accepted when int256(sha3_256(header)) ≤ target. The protocol is a custom object-based JSON-RPC (a hashshare proof), so stock xmrig can't mine the SHA3 side — miners use the animica CLI or the Animica xmrig fork. Stock xmrig can mine the Monero side on the same port.

Authentication username = your address

There is no account system. The Stratum username is the miner's anim1 payout address, optionally suffixed with a worker label using ., /, or : — e.g. anim1zqp…abc.rig-01. The password is not a secret; it's only read for an optional pps/solo mode hint, so any value (x) works. A submit whose address doesn't start with anim1 is rejected.

Ports recap

EndpointAccountingWho connects
…:3333PPS pay-per-shareAnimica CLI / xmrig fork (SHA3) & stock xmrig (RandomX, auto-detected)
…:3334Solo — finder creditedAnimica CLI / xmrig fork with --pool-mode solo

Share difficulty is pinned

Live config pins min = max = 1.0 with a 1.0 wire floor. The floor exists because some xmrig builds disconnect on a sub-1.0 set_difficulty. The server's own per-session vardiff is disabled — every accepted share is effectively a block-finder share at the network threshold θ. Do not lower the share target below 1.0.

07

The payout scheduler

Payouts are automatic and on-chain, run by a background PoolPayoutSchedulernot an API endpoint. It only starts when ANIMICA_POOL_PAYOUT_INTERVAL_SECONDS > 0 (live: 900 = 15 min).

How PPS credit works

Each accepted share credits reward × credit_fraction, where credit_fraction is the probability that share also solves a block — i.e. shares are priced at expected value, with no full-reward bonus for the winning share. Summed over a block, per-share credits total ~one block reward, so miners collectively receive ~100% of mined coinbase (minus any fee). This is self-limiting by design.

Each cycle, the scheduler…

  1. Computes each address's owed = total_credit − paid_out from pool.db.
  2. Caps the cycle to the net-unpaid mined budget (Σ block rewards − Σ payouts); if there isn't enough, it pro-rates. A cycle never pays out more than the pool actually mined.
  3. Drops anyone below the min threshold (1 nANM default) and caps at 100 recipients per cycle.
  4. Re-resolves a mempool-aware nonce fresh per tx and per retry, signs from the payout wallet, broadcasts, and records a payouts row (status=submitted). Retries up to 3× with backoff; nonce_gap is retryable.
  5. A reconcile loop rebroadcasts dropped txs and releases credit (rolls back paid_out) if a tx is confirmed-dropped — with a timestamp cutoff to avoid double-paying grandfathered payouts.
i
The credit cap is your exposure limit

ANIMICA_POOL_CREDIT_CAP_ENABLED=1 stops credited balances from ever outrunning actually-mined coinbase (measured from a baseline). It's what keeps the pool solvent — and it means any operator margin can only ever come from the mined-minus-credited headroom.

pool.db — the ledger sqlite:////root/.animica/chain-1/pool.db

TableHolds
sharesAccepted/rejected shares, difficulty, target (hashrate windows read from here)
blocksFound blocks — height, hash, reward (nANM coinbase), worker, found_by_pool
worker_balancestotal_credit / pps_credit / solo_credit / paid_out per worker row
payoutsstatus / tx_hash / nonce / retry_count / confirmed_ts
accounting_ledgerAppend-only payout_sent and fee-accrual entries
rental_assignmentsRig-rental bindings (marketplace side)
08

Fees & how the operator gets paid

This is the part most guides hand-wave. Here is the honest, code-verified picture. First, the physical reality of where the money is:

① mine
Every ANM block your pool finds pays its entire coinbase on-chain to ANIMICA_POOL_ADDRESS — your hot wallet. Nothing is split at the protocol level.
② credit
The pool records what each miner is owed in pool.db (worker_balances). Under PPS that sums to ~100% of the coinbase, minus any skim.
③ skim
If ENA_FEE_BPS > 0, that % is credited to a treasury address instead of the miner — a synthetic __ena_treasury_fee__ balance row.
④ pay out
The scheduler sends owed balances (miners and the treasury row) out of the hot wallet on-chain, every 15 min.
⑤ your margin
Whatever is never credited out simply stays in the hot wallet. There is no "collect fee" command — you withdraw it with a normal signed transaction.

The three fee levers

1. PPS percentage fee — ANIMICA_POOL_ENA_FEE_BPS (this is the real one). Basis points skimmed off every PPS credit before it reaches the miner. It accrues to ANIMICA_POOL_ENA_TREASURY_ADDRESS and the normal scheduler auto-pays it there each cycle. It's named for the ENA training treasury, but functionally it's just "skim N bps to an address you choose." Point the treasury address at your own operator wallet and set a nonzero bps — that is your working, auto-paid pool fee. Range 0–10000 bps. Live value is 0 (miners keep 100%; it was 3000 = 30% until 2026-07-08).

charge a 1.5% pool fee that auto-pays to your wallet
# in /etc/animica/pool.env
ANIMICA_POOL_ENA_FEE_BPS=150                    # 150 bps = 1.5%
ANIMICA_POOL_ENA_TREASURY_ADDRESS=anim1<your-fee-wallet>
$ systemctl restart animica-pool.service

2. Solo-port fee — ANIMICA_POOL_SOLO_FEE_BPS (default 500 = 5%). Intended to keep 5% of a solo-found block, paying the finder 95%.

!
Caveat: the solo fee is not enforced on the live PPS pool today

Verified against the current code: metrics.py forces the accounting mode to the pool's own mode (pps), so blocks arriving on the solo port are credited as PPS and solo_fee_bps is never applied in the crediting path — it's read in config and advertised as pool_fee_bps: 500 in /api/mining/config, but dead in practice. (The bundled test_solo_port asserts a 5% cut and currently fails.) Don't budget on solo-fee income until the solo accounting path is wired; the reliable percentage lever is #1.

3. XMR dual-mine fee — hardcoded 5%. On the Monero side, POOL_FEE_BPS = 500 of every block reward (plus rounding dust) stays at your pool's Monero address; 95% is owed to miners and paid via monero-wallet-rpc. Section 09.

Withdrawing your fee / margin

There is no operator withdraw command, API, or auto-pay-to-operator step for the ANM side (grep for withdraw/take_fee in the pool code returns nothing). Both your skim (if the treasury address is your own) and any uncredited margin live in the pool hot wallet on-chain. You take them out with an ordinary signed transaction:

withdraw from the pool wallet — run where the seed lives, NOT on pool.env
$ animica wallet send \
    --from  anim1<pool-hot-address> \
    --to    anim1<your-cold-wallet> \
    --amount 250                          # ANM (1 ANM = 1e9 nANM)
# signs with your offline key — the pool process never sees it.
!
Withdraw carefully — this wallet also owes miners

The hot wallet is the same balance the scheduler pays miners from. Leave enough headroom that a payout cycle can still settle every owed balance, or payouts will fail and retry. Only sweep the part above outstanding owed (visible at /api/pool/accounting). Never move miners' funds.

i
Reality of the current config: operator margin ≈ 0 by design

Right now the pool runs PPS with ENA_FEE_BPS=0 and the credit cap on, so miners are credited ~100% of expected value and there is essentially no headroom to withdraw. That's intentional generosity, not a bug. To actually earn, set lever #1 above (and, if you want, keep the XMR 5%). Everything you skim then accrues to the address you named and auto-pays there.

09

XMR dual-mining

Dual-mining lets stock xmrig point at your pool and mine Monero (RandomX, rx/0) for extra yield. It runs in single-port mode: a client that sends a cryptonote {"method":"login"} on its first line to :3333 is protocol-detected and routed to the XMR handler; everyone else stays on the SHA3 path — same port.

!
Tell XMR miners port 3333, not 3334

An internal module docstring still describes 3334 as the cryptonote port, but in this deployment 3334 is the Animica SHA3 solo port and XMR rides 3333 via protocol-detect. Advertising 3334 to xmrig will land them on the solo listener.

Requirements: a pruned monerod with restricted RPC on loopback, and a pool Monero receive address. The XMR login field must still be an anim1 address — XMR shares are attributed to the miner's Animica identity, and the miner registers a separate Monero payout address via the portal.

/etc/systemd/system/monerod.service (excerpt)
ExecStart=/usr/local/bin/monerod --data-dir=/var/lib/monero \
    --rpc-bind-ip=127.0.0.1 --rpc-bind-port=18081 --restricted-rpc \
    --p2p-bind-port=18080 --prune-blockchain --no-zmq \
    --log-file=/var/log/monerod.log

XMR payouts are not handled by the stratum process — a separate monero-wallet-rpc (default 127.0.0.1:18083) does the transfers, driven by an animica pool xmr-payout job on a cron, logging to /var/lib/animica-pool/xmr-payouts.jsonl. The pool's 5% stays at the pool Monero address; withdraw it the same way you'd move any Monero.

10

nginx, DNS & TLS

nginx fronts the web + API surface on 443; it does not touch the raw stratum ports. DNS is one A record for your pool host; certs are Let's Encrypt.

Route map longest ^~ prefix wins

Path→ upstreamServes
/healthz, /api/pool/, /api/miners, /api/blocks, /api/mining/*, /v1/pool/127.0.0.1:8550Pool engine (stats, config, downloads)
/api/ena/127.0.0.1:8791ENA coordinator
/api/, /v1/ (else)127.0.0.1:4000Platform API (NestJS)
/app, /app/127.0.0.1:3020Rig-rental marketplace
/127.0.0.1:3000Web front-end (Next.js)
Open the stratum ports at the firewall

Miners connect straight to the Python process. The web vhost being up doesn't help them — you must allow inbound TCP on 3333 and 3334.

expose raw stratum
$ ufw allow 3333/tcp    # pps + xmr
$ ufw allow 3334/tcp    # solo
$ ufw allow 443/tcp     # web / api (nginx)
# keep 8545, 8550, 18081 on loopback — never expose them
11

Dashboard & APIs

The :8550 API is read-only — pure stats and accounting, with no payout or withdraw route (by design). The public front-end and per-miner stats pages all read from it.

EndpointReturns
GET /api/pool/summaryHashrate, miner count, blocks, height, payout status
GET /api/pool/accountingtotal_credit / gross_credit / paid_out_total — your solvency view
GET /api/pool/accounting/ledgerPayout & fee ledger entries
GET /api/miners · /api/miners/{id}Paginated list · per-miner detail (address or worker name)
GET /api/blocks/recentLast 50 found blocks
GET /api/pool/networkAccurate share-work network hashrate + reported figures
GET /v1/pool/statusPayout countdown (next_payout_at, interval, enabled)
GET /api/pool/xmr/summaryMonero side stats · /miner/{addr} for owed/paid

Front-ends: the Next.js web app at / (pages /mine, /miner lookup, /stats, /download), the Astro pool-web static site, and the rig-rental app at /app. Miners check their own numbers with no login at /miner by entering their address or worker name.

operator quick-checks
$ curl -s http://127.0.0.1:8550/api/pool/summary | jq
$ curl -s http://127.0.0.1:8550/api/pool/accounting | jq   # owed vs paid vs mined
$ curl -s http://127.0.0.1:8550/v1/pool/status | jq .payout_countdown_seconds
12

Onboard miners

Give miners these three connection recipes. The username is always their own anim1 address; the password is a throwaway.

PPS · Animica SHA3

pip install animica
$ pip install --upgrade animica
$ animica miner mine-blocks --count 0 \
    --pool-stratum stratum+tcp://pool.animica.org:3333 \
    --address anim1<your-address>
# or the reference binary:
$ ./animica-miner --pool-url stratum+tcp://pool.animica.org:3333 \
    --address anim1… --worker rig-01 --pool-mode pps

Solo · finder credited

port 3334
$ ./animica-miner --pool-url stratum+tcp://pool.animica.org:3334 \
    --address anim1… --worker rig-01 --pool-mode solo

XMR · stock xmrig (RandomX)

xmrig config.json — note port 3333, algo rx/0
{ "pools": [{
    "url":  "pool.animica.org:3333",
    "algo": "rx/0",
    "user": "anim1<your-address>",   # XMR credited to your ANM identity
    "pass": "x",
    "tls":  false,
    "keepalive": true
}] }
13

Ops & troubleshooting

SymptomLikely cause / fix
No blocks found; templates staleNode RPC behind or unreachable. Check chain.getHead height on :8545; the pool won't self-heal a lagging node.
xmrig disconnects every ~60sConfirm the keepalived reply path is intact (it returns {"status":"KEEPALIVED"}). Don't set share difficulty below 1.0 — some builds drop on sub-1.0 set_difficulty.
XMR miners land on the wrong sideThey were told port 3334. XMR is single-port on 3333; 3334 is SHA3 solo.
Payouts not going outEnsure PAYOUT_INTERVAL_SECONDS > 0. Check /v1/pool/status for last_payout_error; a nonce_gap is retried automatically. Confirm the hot wallet has balance above outstanding owed.
Solo 5% fee not being keptKnown: not enforced in the current PPS crediting path (§08). Use ENA_FEE_BPS for a working percentage fee.
Config change didn't takeYou must systemctl restart animica-pool.service after editing pool.env — nothing hot-reloads.
Web up but miners can't connectnginx doesn't proxy stratum. Open 3333/3334 at the firewall.
watch the pool live
$ journalctl -u animica-pool.service -f
$ ss -ltnp | grep -E ':3333|:3334|:8550'          # confirm all three sockets
$ tail -f /var/log/nginx/pool.animica.org.error.log
14

Quick reference

ThingValue
Serviceanimica-pool.service → ops/run.sh --profile mainnet pool
Config file/etc/animica/pool.env (restart to apply)
Ledger DB/root/.animica/chain-1/pool.db (SQLite WAL)
Node RPChttp://127.0.0.1:8545/rpc
PPS / solo / stats:3333 / :3334 / 127.0.0.1:8550
Public stratumstratum+tcp://pool.animica.org:3333 (solo :3334)
Working fee leverANIMICA_POOL_ENA_FEE_BPS + ..._TREASURY_ADDRESS
Payout cadence900 s · min 1 nANM · ≤100 recipients/cycle
Withdraw fee/marginanimica wallet send (offline key; leave miner headroom)
Base unit1 ANM = 1,000,000,000 nANM
!
The one rule that protects everyone

Only public addresses ever go in pool.env or any file on the pool host. Seeds and private keys stay offline; the pool never needs them to run, only to sign withdrawals you initiate yourself. Never move miner funds.