Architecture & ports
The whole pool is one Python process — python -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.
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.
| Port | Bind | Purpose | Exposed how |
|---|---|---|---|
3333 | 0.0.0.0 | Main PPS + Monero RandomX (protocol-sniffed on the first line) | Raw TCP — open the firewall |
3334 | 0.0.0.0 | True solo accounting | Raw TCP — open the firewall |
8550 | 127.0.0.1 | Read-only stats / accounting API (FastAPI) | nginx → pool.animica.org |
8545 | 127.0.0.1 | Animica node RPC (the pool reads templates here) | loopback only (docker-proxy) |
18081 | 127.0.0.1 | monerod restricted RPC (XMR templates) | loopback only |
3000/4000/3020 | 127.0.0.1 | Web front-end / NestJS API / rig-rental | nginx |
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 containeranimica-mainnet-nodepublishing8545on 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/animicavia its venv (/root/animica/.venv), not from site-packages. For a fresh install:pip install --upgrade animicagives you the sameanimica.stratum_poolmodule and theanimicaCLI. - 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.
The pool won't self-start a node when it can already reach one. Confirm the RPC is live and at the tip:
$ 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
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.
# 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)
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.
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.
# ── 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
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.
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.
[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
$ 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)
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
| Endpoint | Accounting | Who connects |
|---|---|---|
| …:3333 | PPS pay-per-share | Animica CLI / xmrig fork (SHA3) & stock xmrig (RandomX, auto-detected) |
| …:3334 | Solo — finder credited | Animica 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.
The payout scheduler
Payouts are automatic and on-chain, run by a background
PoolPayoutScheduler — not 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…
- Computes each address's owed =
total_credit − paid_outfrompool.db. - 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. - Drops anyone below the min threshold (
1nANM default) and caps at 100 recipients per cycle. - Re-resolves a mempool-aware nonce fresh per tx and per retry, signs from the
payout wallet, broadcasts, and records a
payoutsrow (status=submitted). Retries up to 3× with backoff;nonce_gapis retryable. - 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.
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
| Table | Holds |
|---|---|
shares | Accepted/rejected shares, difficulty, target (hashrate windows read from here) |
blocks | Found blocks — height, hash, reward (nANM coinbase), worker, found_by_pool |
worker_balances | total_credit / pps_credit / solo_credit / paid_out per worker row |
payouts | status / tx_hash / nonce / retry_count / confirmed_ts |
accounting_ledger | Append-only payout_sent and fee-accrual entries |
rental_assignments | Rig-rental bindings (marketplace side) |
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:
ANIMICA_POOL_ADDRESS — your hot wallet. Nothing is split at the protocol level.pool.db (worker_balances). Under PPS that sums to ~100% of the coinbase, minus any skim.ENA_FEE_BPS > 0, that % is credited to a treasury address instead of the miner — a synthetic __ena_treasury_fee__ balance row.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).
# 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%.
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:
$ 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.
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.
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.
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.
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.
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.logXMR 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.
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 | → upstream | Serves |
|---|---|---|
/healthz, /api/pool/, /api/miners, /api/blocks, /api/mining/*, /v1/pool/ | 127.0.0.1:8550 | Pool engine (stats, config, downloads) |
/api/ena/ | 127.0.0.1:8791 | ENA coordinator |
/api/, /v1/ (else) | 127.0.0.1:4000 | Platform API (NestJS) |
/app, /app/ | 127.0.0.1:3020 | Rig-rental marketplace |
/ | 127.0.0.1:3000 | Web front-end (Next.js) |
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.
$ 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
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.
| Endpoint | Returns |
|---|---|
GET /api/pool/summary | Hashrate, miner count, blocks, height, payout status |
GET /api/pool/accounting | total_credit / gross_credit / paid_out_total — your solvency view |
GET /api/pool/accounting/ledger | Payout & fee ledger entries |
GET /api/miners · /api/miners/{id} | Paginated list · per-miner detail (address or worker name) |
GET /api/blocks/recent | Last 50 found blocks |
GET /api/pool/network | Accurate share-work network hashrate + reported figures |
GET /v1/pool/status | Payout countdown (next_payout_at, interval, enabled) |
GET /api/pool/xmr/summary | Monero 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.
$ 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
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 --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
$ ./animica-miner --pool-url stratum+tcp://pool.animica.org:3334 \ --address anim1… --worker rig-01 --pool-mode solo
XMR · stock xmrig (RandomX)
{ "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
}] }Ops & troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| No blocks found; templates stale | Node RPC behind or unreachable. Check chain.getHead height on :8545; the pool won't self-heal a lagging node. |
| xmrig disconnects every ~60s | Confirm 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 side | They were told port 3334. XMR is single-port on 3333; 3334 is SHA3 solo. |
| Payouts not going out | Ensure 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 kept | Known: not enforced in the current PPS crediting path (§08). Use ENA_FEE_BPS for a working percentage fee. |
| Config change didn't take | You must systemctl restart animica-pool.service after editing pool.env — nothing hot-reloads. |
| Web up but miners can't connect | nginx doesn't proxy stratum. Open 3333/3334 at the firewall. |
$ 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
Quick reference
| Thing | Value |
|---|---|
| Service | animica-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 RPC | http://127.0.0.1:8545/rpc |
| PPS / solo / stats | :3333 / :3334 / 127.0.0.1:8550 |
| Public stratum | stratum+tcp://pool.animica.org:3333 (solo :3334) |
| Working fee lever | ANIMICA_POOL_ENA_FEE_BPS + ..._TREASURY_ADDRESS |
| Payout cadence | 900 s · min 1 nANM · ≤100 recipients/cycle |
| Withdraw fee/margin | animica wallet send (offline key; leave miner headroom) |
| Base unit | 1 ANM = 1,000,000,000 nANM |
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.