How NexusPool works, end to end
One page for the whole design: where the pool runs, what a rig speaks to it, how a solo block pays, what the signed receipts prove, how new work reaches you after a block, and how to check any of it yourself. Every number here was measured on the boxes that serve you, on the date given beside it.
01System architecture
NexusPool is four independent pools that share one published address. Each region runs its own Bitcoin Core node, its own stratum core and its own control database, and signs with the same authority key. A rig that reaches any of them gets the same terms, the same key and the same receipts.
Regions and the steered name
| region | role | what it holds |
|---|---|---|
| Los Angeles | primary, and the origin of the authority key | full pool: node, core, control, this website |
| Chicago | full pool, North America east | node, core, control |
| Frankfurt | full pool, Europe | node, core, control |
| Singapore | full pool, Asia and Oceania | node, core, control |
solo.nexuspool.io is a GeoDNS name: the answer depends on where the query comes from, and a region only appears in the answer while the balancer's health monitor sees it serving work. Measured on 2026-09-12 with client-subnet queries against the authoritative server: US west resolves to Los Angeles, US east to Chicago, Germany to Frankfurt, Singapore and Australia to Singapore.
Failover is DNS plus a reconnect. When a region stops serving, it leaves the answer, and the next lookup lands on a healthy one. DNS cannot move a socket that is already open, so a rig keeps its current connection until that socket closes, then reconnects to whatever the name resolves to. Since every region signs with one key, the reconnect is transparent to a rig that pinned it. The bare name nexuspool.io resolves to Los Angeles only and has no such failover; the steered name is the one to configure.
One design, two lineages
The canonical tree serves Bitcoin, Bitcoin Cash and Litecoin (with Dogecoin merged in). The Bitcoin regions run a narrower build of that tree: Bitcoin only, without the Scrypt and CashAddr paths, so the code on the box that pays you is the code that was tested for that chain and nothing else. Bitcoin Cash and Litecoin run on their own boxes with their own endpoints, published on their pages.
Bitcoin Core
| setting | value | why |
|---|---|---|
| version | 31.0 | same on every region |
| dbcache | 1024 MB | sized to fit the box. A cache larger than free memory swaps, and a swapping node syncs six times slower, measured on the Singapore node during its initial download in 2026-09. |
| prune | 10000 MB | the pool needs the tip and recent history, not an archive |
| maxconnections | 40 | enough peers to hear a block early, few enough to keep the node's own work cheap |
| txindex | 0 | not needed for mining. Measured: the index adds delay to every block notification. |
| zmqpubhashblock | tcp://127.0.0.1:28332 | the block hash, published once the block is connected |
| zmqpubsequence | tcp://127.0.0.1:28352 | the block-connect event, published earlier in the pipeline. See ZMQ and latency. |
Layers
Four processes, each owning one thing. Nothing crosses a boundary except the interface listed for it.
- Bitcoin Core validates the chain. It hands the core a template over RPC and announces new blocks over ZMQ.
- The core (C) speaks stratum, builds jobs, grades shares, submits blocks and seals the Glass Ledger. It writes events to a local socket and to its own append-only journal.
- Control (TypeScript, SQLite) consumes those events into a database and serves the read-only HTTP API on the loopback interface.
- The website (Next.js, server-rendered) proxies
GET /api/*to control, adds caching, and pushes live snapshots to open pages over a socket. It never writes to the pool.
The order matters for trust. A miner's work is counted in the core and signed there; control and the website only read what the core wrote. The receipts described below come from the core's journal, not from the database that renders the charts.
02Mining protocols
One port serves both protocols. The core reads the first byte of a new connection: a JSON brace is Stratum V1, anything else is a Stratum V2 handshake. There is no second address to find and no second port to open.
Endpoints
| protocol | address | notes |
|---|---|---|
| Stratum V1 | stratum+tcp://solo.nexuspool.io:3350 | plain TCP, JSON-RPC lines |
| Stratum V2 | stratum2+tcp://solo.nexuspool.io:3350/9amd6GUzTaGXASESCa75c9Rx3vWYihRyLUAE3Vrmqwgm3T9jtxN | Noise-encrypted; the base58 path segment is the authority key to pin |
| TLS | not served | nothing terminates TLS behind the balancer, so no stratum+ssl:// address is published. Measured 2026-09-13: port 3443 is closed on both names. Stratum V2 is the encrypted option. |
Stratum V1, as implemented
| message | direction | what NexusPool does with it |
|---|---|---|
| mining.subscribe | rig to pool | answers with the session's extranonce1 and the extranonce2 size |
| mining.authorize | rig to pool | username is <btc-address>.<worker>; the address is the payout and the identity. Password x, or d=NNNN to pin difficulty. |
| mining.set_difficulty | pool to rig | the share target; changes as VarDiff settles |
| mining.notify | pool to rig | a job. clean_jobs=true means the previous job is stale and must be dropped. |
| mining.submit | rig to pool | graded against the job's target; accepted, or rejected with the reason (stale, low difficulty, duplicate, bad ntime) |
After a new block the pool sends two jobs, not one. The first is a skeleton: a coinbase-only job built from the block header alone, armed 0.47 ms after the tip on Los Angeles (measured 2026-09-10) and sent before the node has even finished updating its own tip. It is valid work on the right parent, with no fees. The second is the full template with transactions, sent as soon as it exists, with clean_jobs=true. The skeleton stops your rig from hashing a dead block during the gap. Section 06 covers what fills that gap.
Stratum V2, as implemented
The handshake is a Noise protocol exchange. The pool proves it holds the private half of the authority key, and everything after the handshake is encrypted and authenticated. A rig that pins the key refuses any pool presenting a different one, which is what makes the steered name safe for V2: all four regions hold the same key.
| feature | status |
|---|---|
| standard channels | served; the rig receives ready-made headers and searches the nonce |
| extended channels | served; the rig receives the coinbase prefix and suffix and rolls its own extranonce |
| job declaration | implemented and enabled on Los Angeles since 2026-09-11. A rig may declare its own transaction set; the pool checks that the declared coinbase pays the channel's own identity before accepting it. |
| template distribution | not implemented. The pool is not a template provider for other pools. |
V1 or V2, for the rig
- Encryption. V1 is plain text on the wire. V2 is encrypted after the handshake.
- Identity of the pool. V1 trusts whatever answers the address. V2 pins a key; a pool without it is refused.
- Work format. V1 sends the full merkle branch per job. V2 standard channels send a header to search, which is smaller and cheaper for the firmware.
- Who picks transactions. V1: the pool. V2 with job declaration: the rig may.
- What you type. V1 takes a host and port. V2 takes the single URL above, or the key in hex or base58 in a separate field, depending on the firmware.
03Solo mining model
Every share you submit is graded against the current network target as well as your share target. A share that clears the network target is a block. The pool submits it, and the coinbase of that block pays your address. There is no pool balance, no payout queue and no fee: the coinbase carries one output for the reward, to you.
The odds
The site states this wherever odds appear, and never claims to improve them. What the pool can affect is waste: hashes spent on a stale parent, or work that a pool counted wrong. The rest of this page is about those two things.
VarDiff
Difficulty starts at 512 for a new connection and moves so that your rig submits shares at a steady rate. It exists because a share target too low floods the pool with shares that carry no information, and one too high leaves the dashboard blind for minutes at a time. It has no effect on your odds: a block is a block at any share difficulty.
The tuner is a state machine. A rig that reports its own hashrate is anchored to that figure and stops searching; a rig that reports nothing is measured from its shares and settles within a few windows; an anchor that goes stale is released so the rig is not held at a difficulty nobody vouches for anymore. Two figures matter to a reader of the stats:work is the difficulty the pool assigned, sdiff is the best difficulty a share actually achieved. Set the password to d=NNNN to pin a value and turn the tuner off for that connection.
Stale shares
A stale share is work on a block that is no longer the tip. It happens in the window between a new block appearing on the network and your rig receiving the next job; every hash in that window is wasted. NexusPool keeps that window short in three ways, each measured: it hears about the block from the sequence topic rather than waiting for the hash (section 07), it sends the skeleton job in under a millisecond, and it fans that job out to every connection at 14.5 µs per connection at the median (Los Angeles, 2026-09-10). The pool-wide stale rate is published live on /api/pool as stale_rate.
04Glass Ledger and custody
The Glass Ledger is a signed log. The core writes a record for every share window it counted per address, every tip change, every block it won, and seals each with a BIP-340 Schnorr signature under the region's authority key. The key is public, the byte layout is public, the records are served over HTTP. Anyone can check them.
The receipt
| field | meaning |
|---|---|
| seq | position in the journal, monotonic per region. A gap is visible. |
| type | custody.hash (work counted for one address), custody.time (a tip arriving and how long fresh work took), custody.block (a won block), custody.region (which region sealed the following records) |
| payload | for custody.hash: address, worker, shares counted, best share, the window, and attest: counted (from shares the pool received) or reconciled (also checked against the rig's own telemetry, when the firmware reports it) |
| sig | 64 bytes, Schnorr over the digest below |
The signed bytes are the domain NexusPool/Custody/v1, then seq as a little-endian uint64, the type with its length, and the payload with its length. The digest is one SHA-256 of that; the signature verifies against the x-only key with any Taproot-capable library. The exact procedure, with a thirty-line verifier, is /docs/audit; this page does not repeat it.
Endpoints
| path | serves |
|---|---|
| /.well-known/nexuspool-custody.json | the algorithm, the preimage layout, and one authority key per chain. Retired keys stay listed so older receipts keep verifying. |
| /api/custody-receipts?addr=… | the recent receipts for one address, plus the pool-wide records. addr=sample returns a page to test a verifier against. |
The keys served right now:
| chain | authority public key (x-only, hex) |
|---|---|
| btc | 124797e8bc0ec89ed60b77c30e1770c624407f72659dc97aa4c4fd7d9377106c |
| bch | 39e0a44418a2f0d831fc9166ffd4f1373bb6f263d753ed6d08862da8ed4a1d46 |
| ltc | 450a714033c4b01731820bca8c32f00f3eb72c56c14b3c8b43cb0a7d0f357387 |
Verifier status
The browser verifier on Glass Ledger shows the preimage byte by byte and lets you flip one to watch the signature fail. A standalone offline verifier exists as a statically linked binary and has checked every receipt on every region; its public release is still pending, so the published path today is the Python snippet on /docs/audit, which needs nothing from NexusPool to run.
Trust model
| the pool signs | you verify | what nobody can deny |
|---|---|---|
| the shares it counted per window, the difficulty it served, your best share, and each tip and block it saw | that every record was sealed by the holder of the published key, that not one byte changed since, and that the sequence has no holes | a receipt the pool later contradicts. Once signed and served, the count cannot be revised without the revision being provable. |
What a receipt does not prove. That the pool counted every share you sent: a share dropped before counting leaves no record. That your hashrate was what the firmware reports. A signature makes the pool accountable for what it wrote, not for silence. Read the honest-limits section on Glass Ledger before relying on a receipt for more than that.
05Block Pulse
Block Pulse measures one thing per block: how long after a new block reaches an external vantage the pool has full work on the wire. It is an instrument for the pool's own speed, not a comparison with anyone else.
The vantage is a machine that is not the pool. It runs npnotify.mjs, which connects to the stratum port like a rig, records every job it receives with a timestamp, and reads the merkle branch length to tell a full template from a skeleton. It also hears blocks from the network on its own. The difference between the two timestamps is one sample; control keeps a ring of the most recent 200 in its key-value store and /api/block-pulse serves them.
Median lead on propagation is the median of those samples when it is negative: the pool's job arrived at the vantage before the vantage heard of the block from the network. A positive median is labelled median time to work. The sign convention is deliberate: a negative number printed as a time reads as a broken panel, so the page names the case instead. Measured on mainnet, the lead has reached −98 ms.
Limits. One vantage is one place on the network; a different vantage reads different numbers, and both are right. The vantage's clock is the vantage's. A merkle branch proves a floor on the transaction count, never the exact count. This is a laboratory measurement of the pool's reflex, not a field measurement of your rig. Read it on Block Pulse.
06T' (template subtraction)
T' is the job between the skeleton and the full template. When a block arrives, the pool still holds the previous template, and most of its transactions are still valid on the new tip. Instead of waiting for Bitcoin Core to assemble a new template, T' subtracts from the one it has: it removes every transaction the new block confirmed and every transaction that conflicts with one it confirmed, recomputes fees, weight, the witness commitment and the coinbase, and publishes. Measured over 32 blocks on 2026-09-12, the body is ready 26 ms after the tip on Los Angeles and 21 ms on Frankfurt.
The three broadcasts per tip
| job | when | carries |
|---|---|---|
| skeleton | 0.95 ms after the tip (median, Los Angeles, 2026-09) | coinbase only, no fees |
| T' | 32.4 ms | the subtracted template: most of the fees, verified by the gates below |
| full template | 71.2 ms | Bitcoin Core's own getblocktemplate |
The gates
A T' that fails any gate is not published; the rig keeps the skeleton until the full template lands. In the order the code checks them:
| gate | what it refuses |
|---|---|
| NAK latch | everything, for the rest of the process, if the node ever refused a published T'. Checked first and never cleared while running. |
| G7 | subtracting from anything but a template the node itself built and validated. Never from a previous T': the validity argument holds per hop, not across hops. |
| G1 | a block body that did not fully decode. A partial parse would leave transactions the block already invalidated. |
| timing | a body that arrived so late the full template is effectively here. Past that point T' buys nothing. |
| G2, G8 | a block whose parent is not the parent our template was built on, compared as 32 raw bytes, and a body that is not the block we asked for. This is the 2-for-1 reorg case: a competing chain wins with two blocks, the node jumps two heights at once, the new tip sits at the height we targeted and a height check passes while every held transaction was validated on coins that may not exist. The raw prevhash comparison refuses it. |
| G4, G5 | a target height on a retarget boundary or a halving. Only getblocktemplate knows the bits across a retarget, and an off-by-one at a halving costs the whole reward. One block in 2016 is the cheap side of both. |
| G6 | a subsidy figure the node's own numbers disagree with. A zero counts as disagreement, never as a pass. |
Ratchet and canary
The full template always supersedes T'. T' never supersedes a newer template, and a T' whose parent has already been passed is dropped. Separately, a canary thread submits each T' body to Bitcoin Core in proposal mode, which validates without broadcasting. A rejection is logged and sets the latch above. The canary never gates a real submit: a block that is valid does not become invalid because a check was slow.
07ZMQ and latency
Bitcoin Core publishes a new block on several ZMQ topics, and they do not fire at the same time. sequence announces the block-connect event earlier in the node's pipeline than hashblock does. Measured across 343 blocks with all topics subscribed on one machine: sequence led hashblock on 343 of 343, by 20.17 ms at the median. The pool subscribes to both and acts on whichever arrives first.
The price of sequence is noise: the same topic carries every mempool add and remove. Measured at 2,017 messages per minute, of which 99.97% are mempool traffic and discarded by type before any work is done. A dedup ring keeps a burst of two blocks from being processed twice, and a divergence check has to persist for five seconds before the pool treats the subscription as wedged and rebuilds it.
The path from tip to rig
- A block-connect event arrives on
sequence, or the node's log wakes an inotify watcher, whichever is first. - The skeleton is armed from the header: 0.47 ms on Los Angeles, before the node's own tip has updated.
- The job is written to every open connection. The loop costs 14.5 µs per connection at the median and 23.6 µs at the 90th percentile, serial, so the last rig on a fully loaded box waits about 10 ms behind the first. Today it is a fraction of that.
- T' and then the full template follow on the schedule in section 06.
Two things that were measured and deliberately not done: a second node per region (no expected value at the pool's scale) and a peer-to-peer tap that would read compact blocks off the wire before the node validates them. The first job is already out before the node's tip updates; what remains is the node's validation time, and bypassing validation is not a trade a solo pool should make.
08Security
One authority key
All four regions load this key, verified from each core's own log on 2026-09-12. A rig that pins it in firmware refuses any pool presenting another, and the steered name can hand it to any region without the pin breaking. Keys that a region used before the consolidation stay published in the custody document so receipts sealed under them still verify.
Threat model
| threat | position |
|---|---|
| block withholding | a rig that finds a block and does not submit it hurts only itself: the coinbase pays the finder, so there is no pool pot to sabotage. As a signal it is statistically undetectable at the pool's current scale, and the pool does not claim to detect it. |
| 2-for-1 reorg | the case where a height check passes and the parent is wrong. Refused by gate G2 in T'; the skeleton and the full template are built from the node's own tip and are not exposed. |
| a fake pool at the address | on V1, undetectable by the protocol. On V2, refused by the key pin. |
| abuse of the stratum port | per-IP connection caps, per-connection share caps, and reject-class accounting that separates stale from invalid, so a flood cannot be mistaken for a bad job. |
Non-custodial
The pool never holds a key of yours and never holds a balance. The payout is the coinbase output of the block you found, to the address in your username, built into the block before it is submitted. Payout preflight rebuilds that exact coinbase for any address before a block exists, so you can see the output you would be paid.
09Public API
Read-only, JSON, no authentication for the public routes. The website proxies GET /api/* to control and sets the cache headers; anything else is not proxied. Rate limiting is per IP.
| route | returns | cache |
|---|---|---|
| /api/pool | pool snapshot: workers online, hashrate windows, share counters, reject classes, stale rate, height, whether the node has a template | 3 s |
| /api/globe | miner and peer positions by region, fuzzed upstream to the resolution of a city and never an IP | 3 s |
| /api/block-pulse | the vantage name, the ring cap and the samples | 3 s |
| /api/custody-receipts?addr=… | signed receipts for one address plus the pool-wide records; capped, most recent first | 5 s |
| /api/hashrate, /api/blocks, /api/top/* | history series, found blocks, leaderboards | 3 s |
| /api/user/…, /api/worker/… | per-address and per-worker stats; the address is the scope and the only handle | 5 s |
Profiles. Control runs as a public profile: every route above is readable without a key. Ingest routes (telemetry pushed by a rig or a bridge) require a key in the x-api-key header and are not reachable through the website proxy. The operations panel reads control over a private network with its own read key and is not part of the public surface.
10Operational guides
A Bitaxe, or any AxeOS device
- Open the device's web interface and go to its pool settings.
- Stratum host:
solo.nexuspool.io. Port:3350. - User: your Bitcoin address, then a dot, then a worker name, for example
bc1q….bitaxe1. - Password:
x. Save and restart the device. - Within a minute, paste the address into the lookup on the home page to see the worker appear.
An Antminer S23 Hyd
Stratum V1, stock firmware.
- Miner configuration, pool 1: URL
stratum+tcp://solo.nexuspool.io:3350. - Worker:
<bitcoin-address>.<name>. Password:x. - Leave pools 2 and 3 empty or point them at the same address; the steered name already fails over.
Stratum V2. Use it if your firmware offers a Stratum V2 pool field, or through a V1-to-V2 translator running on your own network in front of the rig.
- URL:
stratum2+tcp://solo.nexuspool.io:3350/9amd6GUzTaGXASESCa75c9Rx3vWYihRyLUAE3Vrmqwgm3T9jtxN. - If the firmware asks for the key separately, paste the base58 key from section 08.
- The worker name and address go in the same place as for V1.
MiningRigRentals
- Add a pool in your MRR account: host
solo.nexuspool.io, port3350. - Username: your Bitcoin address with a worker suffix.
- Password:
d=NNNN, with NNNN a difficulty suited to the rented hashrate. Rented rigs arrive at full speed with no history, and a pinned difficulty avoids the tuner's first windows at a target that is far too low for them. - Check the rental on the home page lookup once it starts; the address is the only identity the pool knows.
Verifying a receipt
- Fetch
https://nexuspool.io/.well-known/nexuspool-custody.jsonand read the key forbtc. - Fetch
https://nexuspool.io/api/custody-receipts?addr=<your-address>. - For one receipt, rebuild the preimage in the field order the document gives, hash it once with SHA-256, and verify the 64-byte signature against the key with a Schnorr library.
- Change one character of the payload and verify again. It must fail. The full procedure and a ready verifier are on /docs/audit.
Reading Block Pulse
- median lead on propagation, −N ms: the pool's full work reached the vantage N ms before the vantage heard of the block. Good.
- median time to work, +N ms: the vantage heard of the block first, and full work followed N ms later.
- depth: the merkle branch length of the job. A depth of L means at least 2 to the power of L minus 1, plus one, transactions were in the work. A depth of 0 is a skeleton.
- The panel is one vantage. Your own rig, elsewhere, is a different measurement, and the latency check is how you take it.
Where this fits
The verification procedure lives on /docs/audit, the receipt scheme and its honest limits on Glass Ledger, the live propagation instrument on Block Pulse, and the capability-by-capability test record on Technology. This page links to them and does not repeat them.