From 5f88ba1f091485642f6140dc50c7430ec44f1e38 Mon Sep 17 00:00:00 2001 From: rob Date: Sat, 22 Aug 2026 22:01:38 +0200 Subject: [PATCH] Tell solo miners the truth about how they get paid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "about the numbers" card stated the pps-classic story unconditionally. On a solo pool every substantive claim in it was wrong, and one of them was wrong in a way that costs a miner an afternoon: it told them to authorize with a Thunder address. stratum.c branches on pps_enabled at authorize, so in solo mode that is rejected — "invalid payout address in stratum username" — and the card then went on to explain the opposite error message. The card is now a partial with three bodies, branching on pool_mode. Solo says what solo does: the coinbase pays the finder directly, nothing accrues between blocks, a share that is not a block is worth nothing. It names the operator fee and where it goes, and adds two things nobody had written down — coinbase outputs mature after 100 confirmations, and the address must be P2WPKH, P2PKH or P2SH, because coinbase_address_to_script rejects taproot. Both are questions the old card left a solo miner to discover by hitting them. PPS-classic keeps the deposit and payout-worker explanation, which was always correct, and replaces the rate sentence. "Currently 1 000 sats × share difficulty" was hardcoded HTML describing a rate that is derived per template as (block value / net difficulty) * (1 - fee) and moves with difficulty; the literal had never been checked against the ledger it purported to describe. It now reads the live rate, the gross, and the fee from pool_meta, and says outright that the rate is not fixed. A pinned rate gets called out with the fee it actually implies, which is the failure the rate columns exist to expose. Everything else that was a literal is now live too: fee, operator address, pool wallet, sidechain id. Address examples follow the pool's network, so a signet pool shows tb1q rather than bc1q — misleading a miner about the network is the same class of error this whole card was making. An unknown mode names both and commits to neither. The two differ on what a share is worth and on what the username must be, so a guess there is not a harmless default. --- dashboard/README.md | 28 ++++ dashboard/public/style.css | 14 ++ dashboard/server.js | 6 +- dashboard/test/about-numbers.test.js | 174 ++++++++++++++++++++ dashboard/views/index.ejs | 45 +----- dashboard/views/partial/about-numbers.ejs | 186 ++++++++++++++++++++++ 6 files changed, 408 insertions(+), 45 deletions(-) create mode 100644 dashboard/test/about-numbers.test.js create mode 100644 dashboard/views/partial/about-numbers.ejs diff --git a/dashboard/README.md b/dashboard/README.md index 7838816..536408a 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -105,6 +105,34 @@ A non-mainnet pool is flagged with a warn-coloured rule, because "why has my payout not arrived" and "this pool is mining signet" are frequently the same question. +## "About the numbers on this page" + +The explanatory card on `/` branches on `pool_mode`, because almost nothing +in it is shared between the modes: + +| | `solo` | `pps-classic` | +| --- | --- | --- | +| A share that isn't a block | worth nothing | credited at the live rate | +| Block reward goes to | the finder, in the coinbase | the pool's BTC wallet | +| Stratum username | a **Bitcoin** address (P2WPKH / P2PKH / P2SH — **not** taproot) | a **Thunder** address | +| Rejection if you get it wrong | `invalid payout address in stratum username` | `invalid thunder address` | + +That last row is why this is not cosmetic. `src/stratum.c` branches on +`pps_enabled` at authorize, so the card's instructions are load-bearing: a +solo pool that tells miners to use a Thunder address is telling them to do +the one thing that cannot work. + +Every figure comes from `pool_meta` — rate, gross, fee, operator address, +pool wallet, network — and the address examples follow the pool's network, so +a signet pool shows `tb1q…` rather than `bc1q…`. Nothing in the card is a +literal. The version this replaced hardcoded *"1 000 sats × share +difficulty"*, which was never true of a rate that is derived per template and +moves with difficulty; a pinned rate (`rate_source = override`) is now called +out with the fee it actually implies. + +Unknown mode gets prose naming both, and no username form — same rule as the +identity strip, since guessing wrong costs a miner real time. + ## Build provenance — `/api/versions` Answers "which commit is this pool actually running?" for simplepool, the diff --git a/dashboard/public/style.css b/dashboard/public/style.css index 8b918d5..e54ba56 100644 --- a/dashboard/public/style.css +++ b/dashboard/public/style.css @@ -40,6 +40,20 @@ a:hover { text-decoration: underline; } .brand { font-weight: 700; font-size: 18px; color: var(--fg); text-decoration: none; } .tag { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; } +/* "About the numbers" card. The

/
 styling used to be inline style=
+ * attributes on the one hardcoded copy; it moved here when the card became a
+ * partial with three bodies, rather than being repeated three times. */
+.about-h3 { margin-top: 1em; margin-bottom: 0.25em; }
+.about-pre {
+    margin: 0;
+    padding: 0.5em;
+    background: #0002;
+    border-radius: 4px;
+    overflow-x: auto;
+    font-size: 12px;
+}
+.about-note { margin-top: 0.5em; }
+
 /* Pool identity strip — sits between the header and the health banner on
  * every page. Deliberately quiet: it is reference material a miner reads
  * once and then ignores, not an alert. The one exception is a non-mainnet
diff --git a/dashboard/server.js b/dashboard/server.js
index 7f7356c..f216cb1 100644
--- a/dashboard/server.js
+++ b/dashboard/server.js
@@ -50,6 +50,11 @@ app.use((_req, res, next) => {
      * reading it live means a proxy restart onto a different network shows
      * up on the next refresh instead of on the next dashboard restart. */
     res.locals.pool = stats.poolMeta(db);
+    /* The about-numbers card needs both to tell a miner how to connect, and
+     * it is a partial rather than an index-only block, so they live here
+     * rather than being threaded through one render call. */
+    res.locals.stratumUrl  = PUBLIC_STRATUM_URL;
+    res.locals.sidechainId = THUNDER_SIDECHAIN_ID;
     next();
 });
 
@@ -148,7 +153,6 @@ app.get('/', (_req, res) => {
     const node   = stats.nodeStatus(db);
     res.render('index', {
         ov, lb, lbAddr, blocks, node,
-        stratumUrl:  PUBLIC_STRATUM_URL,
         fmtHashrate: stats.fmtHashrate,
         fmtBtc:      stats.fmtBtc,
     });
diff --git a/dashboard/test/about-numbers.test.js b/dashboard/test/about-numbers.test.js
new file mode 100644
index 0000000..e56e175
--- /dev/null
+++ b/dashboard/test/about-numbers.test.js
@@ -0,0 +1,174 @@
+/* The "about the numbers" card.
+ *
+ * The card it replaces stated the pps-classic story unconditionally, which
+ * on a solo pool told miners to authorize with a Thunder address — rejected
+ * by stratum.c with "invalid payout address in stratum username". So these
+ * tests are mostly about the card not saying the other mode's thing: what a
+ * share is worth and what the username must be are exactly the two facts
+ * that differ, and both cost a miner real time when stated wrongly.
+ *
+ * The rate is asserted to be the live one, never a literal. The literal it
+ * replaces ("1 000 sats × share difficulty") was hardcoded HTML describing a
+ * rate that is derived per template and moves with difficulty.
+ */
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import fs from 'node:fs';
+import os from 'node:os';
+import ejs from 'ejs';
+import Database from 'better-sqlite3';
+
+import * as fmt from '../lib/fmt.js';
+import * as stats from '../lib/stats.js';
+import { openDb } from '../lib/db.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const VIEWS = path.resolve(__dirname, '../views');
+
+const OPERATOR = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';
+const POOL_BTC = 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3';
+const URL_ = 'stratum+tcp://pool.example.org:3334';
+
+const PPS = {
+    pool_mode: 'pps-classic', fee_bps: 100, network: 'signet',
+    rate_source: 'derived', rate_sats_per_diff: 2783.22,
+    gross_sats_per_diff: 2811.33, effective_fee_bps: 100,
+    operator_address: OPERATOR, pool_btc_address: POOL_BTC,
+};
+const SOLO = {
+    pool_mode: 'solo', fee_bps: 100, network: 'signet',
+    rate_source: 'derived', rate_sats_per_diff: 0, gross_sats_per_diff: 2811.33,
+    effective_fee_bps: 0, operator_address: OPERATOR, pool_btc_address: null,
+};
+
+const card = (pool, extra = {}) =>
+    ejs.renderFile(path.join(VIEWS, 'partial/about-numbers.ejs'),
+                   { ...fmt.all, pool, stratumUrl: URL_, sidechainId: 9, ...extra },
+                   { views: [VIEWS] });
+
+test('pps-classic states the live rate, not a hardcoded one', async () => {
+    const html = await card(PPS);
+    assert.match(html, /pps-classic/);
+    assert.match(html, /2,783\.22/, 'the rate the proxy actually published');
+    assert.match(html, /2,811\.33/, 'gross, before fee');
+    assert.match(html, /1\.00%/);
+    /* The literal this card exists to remove. */
+    assert.doesNotMatch(html, /1[  ]000[  ]sats/);
+    assert.match(html, /not a fixed number/);
+});
+
+test('pps-classic names the Thunder username and both addresses', async () => {
+    const html = await card(PPS);
+    assert.match(html, /your-Thunder-address/);
+    assert.match(html, /invalid thunder address/);
+    assert.ok(html.includes(POOL_BTC));
+    assert.ok(html.includes(OPERATOR));
+    assert.ok(html.includes(URL_));
+    /* Must not tell a pps miner to use a Bitcoin address. */
+    assert.doesNotMatch(html, /your-bitcoin-address/);
+});
+
+test('solo says nothing about Thunder, deposits or PPS credit', async () => {
+    const html = await card(SOLO);
+    assert.match(html, /\bsolo\b/);
+    /* None of the pps-classic machinery exists in solo. */
+    assert.doesNotMatch(html, /pps_credits/);
+    assert.doesNotMatch(html, /BIP300 deposit/);
+    assert.doesNotMatch(html, /payout worker/);
+    assert.doesNotMatch(html, /sidechain/);
+    /* Thunder appears exactly twice, and both are negations: "there is no
+     * Thunder payout in this mode", and "a Thunder address is rejected".
+     * Both earn their place — a miner arriving from a pps-classic pool, or
+     * from the card this replaces, needs to be told. The count is pinned so
+     * copy drift cannot quietly reintroduce the pps-classic story here. */
+    assert.equal((html.match(/Thunder/g) || []).length, 2);
+    assert.match(html, /no pool wallet and no Thunder payout/);
+    assert.match(html, /as is a Thunder<\/strong> address/);
+    /* Never as an instruction. */
+    assert.doesNotMatch(html, /your-Thunder-address/);
+    /* No pool wallet in solo, so its address must not appear. */
+    assert.ok(!html.includes(POOL_BTC));
+});
+
+test('solo tells miners to use a bitcoin address, and says taproot is not', async () => {
+    const html = await card(SOLO);
+    assert.match(html, /your-bitcoin-address/);
+    assert.doesNotMatch(html, /your-Thunder-address/);
+    assert.match(html, /P2WPKH/);
+    assert.match(html, /[Tt]aproot.*not\s*<\/strong>?\s*supported|not\s*<\/strong>\s*and is rejected|Taproot/);
+    assert.match(html, /invalid payout address in stratum username/);
+    /* Coinbase maturity — the "why can't I spend it" question. */
+    assert.match(html, /100 confirmations/);
+    assert.ok(html.includes(OPERATOR), 'solo still pays an operator fee');
+});
+
+test('address examples follow the network the pool is actually on', async () => {
+    assert.match(await card({ ...SOLO, network: 'main' }),    /bc1q…/);
+    assert.match(await card({ ...SOLO, network: 'main' }),    /bc1p…/);
+    assert.match(await card({ ...SOLO, network: 'signet' }),  /tb1q…/);
+    assert.match(await card({ ...SOLO, network: 'regtest' }), /bcrt1q…/);
+    /* A mainnet example on a signet pool misleads as surely as the wrong
+     * address type does. */
+    assert.doesNotMatch(await card({ ...SOLO, network: 'signet' }), /bc1q…/);
+});
+
+test('an ambiguous network falls back to prose instead of inventing a prefix', async () => {
+    /* "test/signet/regtest" comes from a base58 operator address, which
+     * genuinely cannot say which chain it is. */
+    const html = await card({ ...SOLO, network: 'test/signet/regtest' });
+    assert.match(html, /P2WPKH/);
+    assert.doesNotMatch(html, /1q…/, 'no bech32 prefix invented');
+});
+
+test('a pinned rate is called out, with the fee it actually implies', async () => {
+    const html = await card({ ...PPS, rate_source: 'override', effective_fee_bps: 644.3 });
+    assert.match(html, /pinned/);
+    assert.match(html, /6\.44%/);
+    assert.match(html, /\/health/);
+});
+
+test('pps-classic before the first template does not claim a rate of zero', async () => {
+    const html = await card({ ...PPS, rate_sats_per_diff: 0, gross_sats_per_diff: 0 });
+    assert.match(html, /has not published a rate yet/);
+    assert.doesNotMatch(html, /0\.00\s*sats/);
+});
+
+test('an unknown mode describes both and commits to neither', async () => {
+    for (const pool of [null, { pool_mode: null, fee_bps: 0 }]) {
+        const html = await card(pool);
+        assert.match(html, /has not published its mode/);
+        /* Both named, so a miner knows what to ask the operator — but no
+         * username form is asserted, because guessing costs them time. */
+        assert.match(html, /solo/);
+        assert.match(html, /pps-classic/);
+        assert.doesNotMatch(html, /your-Thunder-address/);
+        assert.doesNotMatch(html, /your-bitcoin-address/);
+    }
+});
+
+test('the overview still renders end to end with the partial in place', async () => {
+    /* Against real stats output rather than a hand-written `ov`, so this
+     * catches the partial breaking the page it lives on without going stale
+     * every time the overview grows a field. */
+    const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'sp-about-')), 'shares.db');
+    const sdb = new Database(file);
+    sdb.exec(fs.readFileSync(path.resolve(__dirname, '../../schema.sql'), 'utf8'));
+    sdb.close();
+    const db = openDb(file);
+
+    const html = await ejs.renderFile(path.join(VIEWS, 'index.ejs'), {
+        ...fmt.all, pool: PPS, stratumUrl: URL_, sidechainId: 9,
+        health: null, active: 'overview',
+        ov:     stats.overview(db),
+        lb:     stats.leaderboard(db),
+        lbAddr: stats.leaderboardByAddress(db),
+        blocks: stats.recentBlocks(db, 5),
+        node:   stats.nodeStatus(db),
+        fmtHashrate: stats.fmtHashrate,
+        fmtBtc:      stats.fmtBtc,
+    }, { views: [VIEWS] });
+    assert.match(html, /About the numbers on this page/);
+    assert.match(html, /2,783\.22/);
+});
diff --git a/dashboard/views/index.ejs b/dashboard/views/index.ejs
index 905ec15..2a4663e 100644
--- a/dashboard/views/index.ejs
+++ b/dashboard/views/index.ejs
@@ -13,50 +13,7 @@
             
         <% } %>
 
-        
-

About the numbers on this page

-

- This is the PPS-classic build of simplepool, - paying out in Thunder - (BIP300 drivechain sidechain #9). Every accepted share - credits your worker a fixed number of sats — currently - 1 000 sats × share difficulty — whether - or not that share becomes a block. Accruals accumulate under - your Thunder address in pps_credits. -

-

- When a share becomes a block, the coinbase pays the pool's own - Bitcoin wallet (a normal P2WPKH output) for the miner-share of - the reward, and the operator address for a small fee (1%). The - pool operator then periodically batches accumulated BTC into - Thunder via a canonical BIP300 deposit transaction — this is - the step that actually moves value onto the sidechain. Once - the pool's Thunder reserve is funded, the payout worker sends - Thunder transactions to each miner's Thunder address on a - cadence to drain their owed balance. -

-

- Why the two-step deposit? The LayerTwo-Labs enforcer only - credits deposits that spend real, spendable UTXOs. Coinbases - don't qualify; embedding the deposit directly in the coinbase - results in a stranded output. Standard drivechain mining - pools all route through a pool BTC wallet. -

- -

Connect a miner

-
<%= stratumUrl %>
-username:  <your-Thunder-address>[.<rig_label>]
-password:  (ignored — any value)
-

- The username must be a valid Thunder address — base58 of a 20-byte - hash, e.g. 3Z6z1hPySNkFeB7HiKPCgqu4TZez. Generate one - with thunder-cli get-new-address on any Thunder node. - Using a Bitcoin address is rejected with - "invalid thunder address" and no shares accrue. - The optional .<rig_label> groups multiple rigs - paying the same address as separate rows on the per-worker page. -

-
+ <%- include('partial/about-numbers') %>

Bitcoin node tip

diff --git a/dashboard/views/partial/about-numbers.ejs b/dashboard/views/partial/about-numbers.ejs new file mode 100644 index 0000000..a1c7baf --- /dev/null +++ b/dashboard/views/partial/about-numbers.ejs @@ -0,0 +1,186 @@ +<%# What the figures on the overview mean, and how to point a rig at this pool. + + Branches on pool_mode because almost nothing here is shared between the + two modes: solo pays the finder in the coinbase and accrues nothing, + pps-classic credits every share and settles through a pool wallet and a + BIP300 deposit. The card used to state the pps-classic story + unconditionally, which on a solo pool told miners to authorize with a + Thunder address — the one thing that cannot work there (stratum.c + rejects it with "invalid payout address in stratum username"). + + Every figure comes from res.locals.pool, i.e. from pool_meta, i.e. from + the running proxy. Nothing here is a literal, because the previous + literal — "1 000 sats × share difficulty" — was never true of a rate + that is derived per template and moves with difficulty. + + Locals: pool, stratumUrl, sidechainId. %> +<% +const _p = (typeof pool !== 'undefined' && pool) ? pool : null; +const _mode = _p && _p.pool_mode ? _p.pool_mode : null; +const _net = _p && _p.network ? _p.network : null; +const _url = (typeof stratumUrl !== 'undefined' && stratumUrl) + ? stratumUrl : 'stratum+tcp://:3334'; +const _sid = (typeof sidechainId !== 'undefined' && sidechainId != null) + ? sidechainId : 9; +const _feePct = _p && _p.fee_bps ? (_p.fee_bps / 100).toFixed(2) + '%' : null; +const _sats = n => Number(n).toLocaleString('en-US', + { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + +/* Address examples for the network this pool is actually on — a miner told + * to use "bc1q…" on a signet pool has been misled just as surely as one told + * to use a Thunder address in solo mode. + * + * Only for networks the encoding pins down. "test/signet/regtest" comes from + * a base58 operator address, which genuinely cannot say which — so it falls + * through to prose rather than inventing a prefix. */ +function _examples(net) { + if (net === 'main') return { w: 'bc1q…', t: 'bc1p…', l: '1…', s: '3…' }; + if (net === 'regtest') return { w: 'bcrt1q…', t: 'bcrt1p…', l: 'm… / n…', s: '2…' }; + if (net === 'test' || net === 'signet' || net === 'test/signet') + return { w: 'tb1q…', t: 'tb1p…', l: 'm… / n…', s: '2…' }; + return null; +} +const _ex = _examples(_net); +%> +
+

About the numbers on this page

+ +<% if (_mode === 'pps-classic') { %> +

+ This pool runs in pps-classic mode, paying out in + Thunder (BIP300 drivechain sidechain #<%= _sid %>). + Every accepted share credits your worker whether or not that share + becomes a block. Credits accumulate under your Thunder address in + pps_credits. +

+ +

+ The rate is not a fixed number. It is derived from + each block template as + (block value ÷ network difficulty) × (1 − fee), so it + moves with difficulty and with the fees in the template. + <% if (_p && _p.rate_sats_per_diff > 0) { %> + Right now: <%= _sats(_p.rate_sats_per_diff) %> sats + per unit of share difficulty<% if (_feePct) { %>, net of the + <%= _feePct %> operator fee<% } %><% if (_p.gross_sats_per_diff > 0) { %> + (gross <%= _sats(_p.gross_sats_per_diff) %>)<% } %>. + <% } else { %> + The proxy has not published a rate yet — it does so on its first + block template, so this fills in once the pool is receiving work. + <% } %> + Every credit is stored with the exact rate it was paid at, so you can + recompute your own balance on your worker page rather than take the + pool's word for it. +

+ + <% if (_p && _p.rate_source === 'override') { %> +

+ ⚠ The operator has pinned this rate rather than + deriving it. A pinned rate does not track difficulty, and is taken + already net of fee — what it actually implies here is a + <%= (_p.effective_fee_bps / 100).toFixed(2) %>% fee<% + if (_feePct) { %>, against the <%= _feePct %> configured<% } %>. + See /health. +

+ <% } %> + +

+ When a share becomes a block, the coinbase pays the pool's own + Bitcoin wallet<% if (_p && _p.pool_btc_address) { %> + (<%= _p.pool_btc_address %>)<% } %> + for the miner-share of the reward, and the operator address<% if (_p && _p.operator_address) { %> + (<%= _p.operator_address %>)<% } %> + for the fee<% if (_feePct) { %> of <%= _feePct %><% } %>. The operator + then periodically batches accumulated BTC into Thunder via a canonical + BIP300 deposit transaction — this is the step that actually moves + value onto the sidechain. Once the pool's Thunder reserve is funded, + the payout worker sends Thunder transactions to each miner's Thunder + address on a cadence to drain their owed balance. +

+ +

+ Why the two-step deposit? The LayerTwo-Labs enforcer only credits + deposits that spend real, spendable UTXOs. Coinbases don't qualify; + embedding the deposit directly in the coinbase results in a stranded + output. Standard drivechain mining pools all route through a pool BTC + wallet. +

+ +

Connect a miner

+
<%= _url %>
+username:  <your-Thunder-address>[.<rig_label>]
+password:  (ignored — any value)
+

+ The username must be a valid Thunder address — base58 of a 20-byte + hash, e.g. 3Z6z1hPySNkFeB7HiKPCgqu4TZez. Generate one + with thunder-cli get-new-address on any Thunder node. + Using a Bitcoin address is rejected with + "invalid thunder address" and no shares accrue. + The optional .<rig_label> groups multiple rigs + paying the same address as separate rows on the per-worker page. +

+ +<% } else if (_mode === 'solo') { %> +

+ This pool runs in solo mode. Each block it finds pays + the miner who found it, directly in that block's coinbase. Nothing + accrues between blocks: a share that does not become a block earns + nothing — it only proves you are working and sets your difficulty. + There is no PPS credit, no pool wallet and no Thunder payout in this + mode. +

+ +

+ When one of your shares does become a block, the coinbase pays + your address the block reward<% if (_feePct) { %> + minus the operator fee of <%= _feePct %><% if (_p && _p.operator_address) { %>, + which goes to <%= _p.operator_address %><% } %><% } %>. + Coinbase outputs mature after 100 confirmations + (roughly 16 hours), so a block shows up here well before it is + spendable in your wallet.<% if (_feePct) { %> If the fee would come to + under 546 sats it is dropped as dust and you take the whole reward.<% } %> +

+ +

Connect a miner

+
<%= _url %>
+username:  <your-bitcoin-address>[.<rig_label>]
+password:  (ignored — any value)
+

+ The username must be a Bitcoin address this pool can build an output + for: + <% if (_ex) { %> + P2WPKH (<%= _ex.w %>), + P2PKH (<%= _ex.l %>) or + P2SH (<%= _ex.s %>) on + <%= _net %>. Taproot (<%= _ex.t %>) is not + supported + <% } else { %> + P2WPKH, P2PKH or + P2SH. Taproot is not supported + <% } %> + and is rejected at authorize, as is a Thunder address + — that is the other mode. Both fail with + "invalid payout address in stratum username". + The optional .<rig_label> groups multiple rigs + paying the same address as separate rows on the per-worker page. +

+ +<% } else { %> + <%# Same rule as the identity strip: an unknown mode gets prose that is + true either way, never a guess. The two modes differ on what a share + is worth and on what the username must be, so guessing wrong here + costs a miner real time. %> +

+ This pool has not published its mode yet, and the two differ in ways + that change how you connect: solo pays the finder + directly in the coinbase and takes a Bitcoin address as the stratum + username, while pps-classic credits every accepted + share and takes a Thunder address. Restart the proxy to publish which + one this is, or ask the operator before pointing a rig at it. +

+

Connect a miner

+
<%= _url %>
+username:  <address>[.<rig_label>]   (see above)
+password:  (ignored — any value)
+<% } %> +