// schema · cryptonote stratum · monero pool ↔ miner

How pools dispatch work
and miners send shares back.

A look at the conversation between a Monero mining pool and a RandomX miner like XMRig. Despite being called "Stratum", the Monero/CryptoNote variant doesn't speak the Bitcoin protocol — it's a JSON-RPC over raw TCP dialect with just four verbs: login, getjob, submit, keepalived. Add server-pushed job notifications and you have the entire mining loop.

json-rpc 2.0 tcp + tls randomx algorithm port 3333 / 5555 / 14444 typical line-delimited frames
01 · architecture

§1Three actors, two protocols

A mining pool sits between the Monero daemon and a fleet of miners. It talks JSON-RPC over HTTP to monerod (to pull block templates and submit found blocks) and a line-delimited JSON-RPC over TCP protocol — the CryptoNote Stratum dialect — to thousands of miners. The pool's job is to chop the network's monolithic mining target into bite-sized shares that small miners can actually find, then aggregate everyone's hashes into a credit ledger.

monerod daemon block template mempool chain state POOL stratum + scheduler · template fetcher · job dispatcher · share validator · vardiff engine · accounting / payouts aggregates 1000s → 1 block XMRig #1 CPU · 8 kH/s XMRig #2 CPU · 12 kH/s XMRig #N …thousands more ↑ all hashing RandomX get_block_template submit_block (on win) JSON-RPC / HTTP :18081 jobs shares ↗ CryptoNote Stratum / TCP
02 · connection lifecycle

§2From login to first hash

A miner opens a single long-lived TCP socket (usually wrapped in TLS) and never closes it for the whole session — sometimes days. Every message is a JSON object on its own line, terminated by \n. There are exactly four client methods and one server push in the entire protocol.

MINER XMRig client POOL stratum server TCP connect (+ TLS handshake) login · { login: addr.worker, pass, agent, algo[] } result · { id: "session_id", job: {…first job…}, status: "OK" } → miner now hashing LOOP · while connected submit · { id, job_id, nonce, result } result · { status: "OK" } or error job · server-pushed notification (no id) sent on new block / template refresh → drop current work KEEPALIVE · every ~60s of idle keepalived · { id: "session_id" } result · { status: "KEEPALIVED" } no traffic for 15min → pool times out the socket
1

Connect

TCP to pool.host:3333 (or 14444 for TLS). One socket per miner. The pool may run many ports each with a different starting difficulty.

2

Login

First message must be login: wallet address (the payout), optional .worker_id suffix, miner agent string, supported algos.

3

First job

The login response embeds the first job. No separate getjob needed. Miner starts hashing the moment it parses this reply.

4

Steady state

Miner pushes submit on every share. Pool pushes job on every chain tip change. Both sides exchange keepalived pings to hold the socket.

03 · login

§3The login handshake, byte by byte

The pool validates the wallet address against its expected prefix (mainnet starts with 4 or 8), splits the worker ID off the address (everything after the first dot or +), creates a session, fetches the current block template if it doesn't have one cached, and ships back a session_id the miner will quote in every subsequent submit.

→ client → pool
{ "id": 1, "jsonrpc": "2.0", "method": "login", "params": { "login": "4AdUndXHHZ…wallet….rig01", "pass": "x", // usually unused "agent": "XMRig/6.21.3 (Linux x64)", "algo": ["rx/0", "rx/wow"] // supported RandomX variants } }
← pool → client
{ "id": 1, "jsonrpc": "2.0", "result": { "id": "a7f3c2…", // session id, quote in submits "job": { "blob": "0c0c8a8b…00000000…0e", "job_id": "k9x2v1", "target": "711b0d00", "algo": "rx/0", "height": 3214872, "seed_hash": "a4b1…" }, "status": "OK" } }

What the pool does on receiving login

a

Address validation

Decodes the base58 wallet, checks the network prefix byte matches mainnet (0x12) or subaddress (0x2a). Reject if junk.

b

Session creation

Generates a random session_id, indexes the miner by it. Stores agent string, IP, starting difficulty, last-share timestamp.

c

Job assignment

Pulls the current block template, mutates its extra_nonce field so this miner gets a unique search space, computes the per-miner share target.

04 · the job

§4The job · what's inside that blob

The blob is the heart of every job. It's the hex-encoded serialized block header that the miner will hash. The pool has already inserted a coinbase paying the pool's wallet, picked the tx set, and computed the merkle root. The miner's only freedom is the nonce field at bytes 39–42: it changes those 4 bytes, recomputes the RandomX hash of the whole blob, checks against the target.

FieldTypeMeaning
blobhex stringThe serialized mining blob. ~76 bytes for Monero. The miner hashes this exact byte sequence with only the nonce mutated.
job_idopaque stringPool-assigned tag. Echoed back in submit so the pool knows which template the share refers to.
targethex (LE u32 or u64)The share difficulty target. Hash result interpreted as little-endian; if hash < target → valid share.
algostringrx/0 for mainline Monero RandomX. Other variants exist for forks (rx/wow, rx/arq).
heightintegerBlock height this template builds on. Used by the miner for logging and to drop stale work.
seed_hashhex stringThe RandomX dataset seed (changes every ~2048 blocks). Triggers the miner to rebuild its 2 GB dataset.
next_seed_hashhex (optional)Heads-up about the next seed so the miner can pre-build the dataset off-thread before the rotation.

Inside the blob (Monero block header layout)

major / minor
version
timestamp
(varint)
prev_id
(32 bytes)
★ NONCE
(4 bytes · 39..42)
miner_tx + tx_hashes
(merkle leaves)
version bytes  ·  timestamp  ·  previous block id  ·  nonce — the only thing the miner mutates  ·  tx data

The pool injects an extra_nonce inside the miner transaction so each connected miner gets a slightly different blob — that way two miners never duplicate work on the same 4-byte nonce range. If the miner ever sees the same blob twice, the pool has a bug.

05 · hashing

§5What the miner does · the RandomX loop

RandomX is Monero's ASIC-resistant PoW (since Nov 2019). It compiles a per-seed random program into native machine code and executes it inside a 2 GB dataset, hammering the CPU's branch predictor, cache, and integer/FP units the way real workloads do. The whole point: a general-purpose CPU is roughly as efficient at this as bespoke silicon, which is why XMRig and friends are CPU miners.

i

Dataset init

On first job (or seed change): allocate ~2 GB, expand seed_hash into the dataset via Argon2d + AES. Takes ~30s. Done once per ~2048 blocks.

ii

Nonce loop

For each candidate nonce: write nonce into blob[39..42], run RandomX(blob) → 32-byte hash. Each thread takes a slice of the 232 nonce space.

iii

Compare to target

Interpret last 8 bytes of hash as little-endian u64. If hash_le < target → it's a share. If hash_le < network_target → it's a block.

iv

Submit

Stop everything, fire off submit with the winning nonce and hash, resume hashing from where the thread left off. Latency matters: stale shares get rejected.

Why shares exist at all

The current network difficulty makes a real block hash a 1-in-billions event per nonce. A small miner could hash for months without ever finding one. So the pool sets a much easier target — typically tuned so each miner finds one share every ~30 seconds. Shares are proof the miner did work; they don't go on chain, but they earn the miner credit in the pool's accounting. Only when one miner's share happens to also satisfy the network target does an actual block get produced.

06 · share submission

§6Submitting a share

When a miner finds a nonce satisfying its share target, it sends a submit with four fields: session id, job id, the winning nonce (as 8-hex little-endian), and the resulting hash. The pool validates in a strict order and replies either OK or a typed error.

→ client → pool
{ "id": 2, "jsonrpc": "2.0", "method": "submit", "params": { "id": "a7f3c2…", // session id from login "job_id": "k9x2v1", "nonce": "3a4f0100", // 4 bytes, lowercase hex "result": "e2c4…0000" // the RandomX hash } }
← pool → client (happy path)
{ "id": 2, "jsonrpc": "2.0", "result": { "status": "OK" }, "error": null } // or, sad path: { "error": { "code": -1, "message": "Low difficulty share" } }

The pool's validation pipeline · in this exact order

CheckFailure modeWhat it catches
1. Session knownUnauthenticatedThe id matches an active session. Rejects shares from miners who never logged in or have been kicked.
2. Job existsInvalid job idThe job_id is in the session's recent-jobs cache. Pools usually keep the last 4-8 jobs to absorb in-flight shares after a job change.
3. Nonce well-formedMalformed nonceRegex ^[0-9a-f]{8}$. Eight lowercase hex characters, no more, no less.
4. Not duplicateDuplicate shareThe pool maintains a per-job set of seen nonces. Resubmitting the same nonce is either a bug or an attempted credit-stuffing exploit.
5. Not staleBlock expiredIf the job's height is below current chain tip, the work is on a dead template — share dropped, miner's stale counter ticks up.
6. Hash < targetLow difficulty sharePool re-runs RandomX on the submitted blob+nonce, compares to the per-miner target. (Trusted pools may skip this for trusted miners — "share trust".)
7. Hash < net target?— (the good case)If yes, this share is a block. Pool immediately calls submit_block on monerod with the full blob.
07 · server push

§7Pushing new jobs · the only server-initiated message

Whenever monerod tells the pool about a new block (via ZMQ chain_main or by polling get_block_template), every miner currently hashing on the old template is wasting electricity. The pool fans out a job notification — the one JSON-RPC frame that has no id field, marking it as a one-way push the miner must not reply to.

{ "jsonrpc": "2.0", "method": "job", // note: no "id" key → it's a notification "params": { "blob": "0c0c…new template…0e", "job_id": "m4q8z2", "target": "711b0d00", "algo": "rx/0", "height": 3214873, "seed_hash": "a4b1…", // same as before — dataset still valid "next_seed_hash": "b9e7…" // new! → start pre-building in background } }

On receipt, the miner immediately drops its current nonce loop and starts over with the new blob. Any share computed against the old job_id is now stale; if it arrives at the pool after the rotation, it's counted as a stale share and (typically) not credited. Good miners minimize stale rate to under 1%.

α

Triggers for a push

(1) New block from monerod's ZMQ — clean job, drop everything. (2) Template refresh (extra tx in mempool, more fees) — optional, some pools merge. (3) Merge-mining tip change.

β

Fan-out cost

10k connected miners means 10k goroutines/sockets all writing simultaneously. This is why xmrig-proxy exists: it concentrates 100k miners into a few hundred upstream pool sockets.

γ

Race against propagation

From the moment monerod sees a new block to the moment every miner is hashing the new template: typically <500ms. Each extra millisecond is hashpower burned on dead work.

08 · accounting

§8VarDiff & the payout backend

A pool serves both a Raspberry Pi doing 200 H/s and a 64-core Threadripper doing 50 kH/s. One target can't fit both — too easy and the Threadripper drowns the pool in submits; too hard and the Pi never finds a share. VarDiff retargets each miner individually to keep the share rate near a configured cadence (typically 1 share / 30s).

1

Sample

Pool watches the rolling interval between this miner's shares over the last retargetTime (e.g. 30s) and computes the ratio vs the goal.

2

Retarget

If the miner is too fast, raise the target's difficulty (lower the numeric target). Too slow, lower it. Bounded by maxJump (usually ±100%) and a variancePercent deadband to avoid thrash.

3

Push new target

The next pushed job simply carries the new target field. No special "set_difficulty" message exists in the CryptoNote dialect — difficulty rides on the job.

From shares to XMR · payout schemes

SchemeHow miners are paidTrade-off
PPLNSPay-per-last-N-shares. When a block is found, reward is split across the last N shares submitted to the pool (sliding window).Most common in Monero. Penalizes pool-hopping. Income is variance-bound to the pool's luck.
PPSPay-per-share. Pool pays a fixed XMR amount per accepted share, immediately, from its own reserves.Predictable income for the miner. Pool bears all the variance risk; charges higher fee.
SOLOMiner gets the entire block reward when their share happens to be the winning one. Nothing otherwise.For large miners. Pool acts as pure stratum/template service.
P2PoolDecentralized sidechain — each "share" is itself a mini-block on the sidechain. No central operator; payouts settle on the Monero chain automatically.Trustless, non-custodial. Slightly higher orphan rate. Increasingly popular in Monero post-2021.