Fleet Group: Solo Mining Shared by a Group

Everything an operator and a hosted miner need to know about Fleet Group: how a block is divided, the rules fixed at creation, the steps in the console and on the invitation link, what the client page shows, removal and dissolution, the receipt and the CSV files, and the change we made to the pool software, published in full.

Fleet Group is solo mining shared by a closed group: your clients mine with their own Bitcoin addresses on one primary server, and every block found there is divided among them by the shares sent since the previous block, with 1% to us and your fixed quota to you, all in the coinbase. You create the group from the console with one signature; each client joins from a link with one signature. The rules are written at creation and never change.

Version 1.0 · 25 October 2026 · Bitcoin (BTC). For the comparison with Fleet Split and Fleet Pool, see SoloFury Fleet: Split, Group and Pool. Fleet Group is available to operators who already run Fleet Split; write to [email protected].

Key takeaways

  • A group has one primary server, chosen by you, and is copied to the other Fleet servers as backup. Each server keeps its own round: a block found on a server is divided among the members who sent shares to that server since the previous block found there.
  • The coinbase of a group block has 1% to us, your quota to your address, the rest to the members in proportion to their shares. A member’s part below 5,000 satoshi is not paid and stays in our fee output. There is no bonus for the member who finds the block.
  • Your quota is 1% to 49% of the whole block, one value for the group, fixed at creation. Maximum 52 members.
  • Credentials of the primary server are available right after the client signs; there is no confirmation period.
  • A member can be removed only after 14 days without shares on every server, with a reason the client reads. A group can be dissolved only when no member has sent shares in the last 14 days and no block of the group is still unbooked.
  • Each client has a page with their part of the current round, “Verify the payout now” on the real template of the next block, the group’s blocks and a PDF receipt. You have the console with members, servers, rounds, blocks, CSV files and receipts.
  • The group runs on a version of the pool software we maintain, with a 43-line change that pays your quota. The change is published below, with its test log.

How Fleet Group works

Fleet Split gives each client a block of their own. Fleet Group puts a closed set of clients together: the group finds blocks far more often than any member alone, and each block is divided among the members by the work done. It is still solo mining: nothing is paid until the group finds a block.

The coinbase of a group block

Coinbase outputReceivesAmount
Pool feeSoloFury1% of the block, fixed for everyone, plus any member part below 5,000 sat
ManagerYour address for this groupYour quota, 1% to 49% of the whole block, fixed at creation
MembersOne output per member paidThe rest, divided in proportion to the shares each member sent to that server in the round

Example on the 3.125 BTC subsidy with your quota at 5%: the pool fee output is 3,125,000 sat, your output 15,625,000 sat, and 293,750,000 sat are divided among the members. A member with 4% of the shares in the round receives 11,750,000 sat, about 0.1175 BTC. Network fees are added to the subsidy, so a real block is worth more.

One primary server, one round per server

Each Fleet server keeps its own count of shares for the group. When a server finds a block for the group, that server’s round is divided and starts again from zero; the rounds of the other servers are not touched. A round ends only with a block, never on a schedule: there is no daily or weekly reset. Shares never move from one server’s round to another’s, and a round still open when the group is dissolved is not paid. Your clients’ machines mine on the primary server you choose at creation, so in normal operation all of them take part in every block of the group. The group is copied to the other Fleet servers for failover: a machine that moves to pool 3 during an outage keeps mining for the group, but its shares count in the round of that other server, and only a block found there pays them.

The console shows, for each server, whether the group is present and aligned, the current round (since which block and when it started), the shares in it and the active members. The client page shows the same for the client’s own shares. In practice: if a client’s machines stay in failover for three days, those shares pay only if that other server finds a block, which is rare; the client page shows a red warning and the console lists the member among those with more than 5% of shares off the primary server. When it appears, check the machine’s pool order and that the primary server is reachable from the farm.

The 5,000 satoshi threshold

A coinbase output below the dust limit cannot be created, so the software has a minimum payout, set at 5,000 sat for every Fleet Group. A member’s part below it is not paid on that block and remains in our fee output; the client page and the block tables show it as “below threshold” with the amount. There is no credit carried to the next block: that is the Fleet Pool model, not Fleet Group. In practice the threshold concerns very small miners in large groups: a 1 TH/s miner in a 50 PH/s group has about 5,900 sat per block with a 5% quota, just above the line; in a 100 PH/s group about 2,900 sat, below it.

Before you start

  • You are a Fleet operator. Registration, access address and the console are the ones of Fleet Split; the same wallet that signs messages is enough.
  • One fresh address per group. It receives your quota and is the group creator in the pool software. It must not be used in any other group or Fleet Split agreement; the console refuses an address already in use.
  • Decide the rules. Primary server (the Fleet server closest to the machines), your quota as a percentage of the block from 1 to 49 (two decimals allowed), and optionally your own machines declared (TH/s, count, model). Nothing can change afterwards without a new group.
  • Tell your clients what they sign. Send them this page with the invitation. The link shows them five things to know, the rules, and asks them to declare that they understood the group may pay nothing for months or years.

Step 1: Create the group

In the console at solofury.com/fleet/manage/, open Groups and press New group.

  1. Enter a label (only for you, such as “North farm”), choose the primary server, set the manager share in percent of the whole block, and if you like declare your own machines.
  2. Paste the manager address for this group: a new bc1q… or bc1p… address of your wallet.
  3. Press Continue: the page shows the message to sign, 5 lines with Copy and QR. Sign it in your wallet with that address and paste the signature.
  4. Press Create the group. The console confirms the fixed rules: manager share, primary server, minimum payout 5,000 sat, no finder bonus, round reset at every block, maximum 52 members, removal only after 14 days without shares.

The group is created on the primary server at once and copied to the others by our synchronisation within a few minutes. The console shows “Group on servers” with the count. Your own address can mine in the group too: its credentials are in the group card under Your machine settings.

Step 2: Invite a client

In the group card, under Invitations, press New invitation: a label for the client, the validity (24 hours, 3 days or 7 days) and the client’s declared machines, if you know them. The declared values are pre-filled on the client’s page; the client can correct them and you see both values side by side.

The console shows the link and the expiry. Give it only to the client: it works once. Until the client signs, you can Revoke it. The list shows each invitation as not opened, opened but not signed, used or expired. “Group full” appears at 52 members.

Step 3: What the client does

The client opens the link on a computer or phone and sees, in their language: who invited them, the primary server, the members count, five things to know before signing, the group rules, and an estimate on a block found today for the manager and for them. Then:

  1. Enters their payout address (bc1q… or bc1p…) and checks the declared machines.
  2. Copies the 5-line message (or scans the QR), signs it in the wallet that holds the address and pastes the signature.
  3. Ticks I accept the group rules and the SoloFury Fleet terms, and the declaration: “I understand this is solo mining in a group: a payout comes ONLY if the group finds a block. Months or years can pass without blocks, and in that case I receive nothing, even if my machines worked the whole time. If my part is below 5,000 sat, I receive nothing on that block either.”
  4. Presses Sign and join.

The link also says how to leave: switch the machines off or move them, and after 14 days without shares the manager removes the address (Step 8). The client can ask for it from their page at any time.

The address must not be in another group or in a Fleet Split agreement, and must not be your manager address. The signature proves ownership and contains no amount, recipient or transaction. The messages and signatures are kept with the group record and printed in the receipt.

Step 4: Credentials, right away

After the signature the client page shows You are in the group with the machine settings, and a Download the receipt (PDF) button. Unlike Fleet Split, there is no confirmation period: the primary server already has the member, and a block found there pays by the rules from the first share.

FieldValue
Usernameclient-address.MACHINE_NAME
PasswordAs shown on the page
Pool 1Primary server, first port
Pool 2Primary server, second port
Pool 3The same group on another Fleet server; shown on the client page once the group is copied to every server
Stratum V2Same hosts and ports; the authority public key is shown next to them

Keep pools 1 and 2 on the primary server, so that shares land in the round that pays with the whole group. Pool 3 is a safety net for an outage. Only the hosts and ports shown on the page belong to the group: on a regular solo port of our pool, the client’s address mines plain solo, outside the group. Whole machines work with any firmware; Braiins OS pool groups can send a fixed share of every machine, as explained in the Fleet Split guide.

Step 5: Follow the group in the console

The group card shows: status on the servers, active members in the last 24 hours against total and maximum, measured hashrate against declared, blocks found and those not booked yet, and the open alerts: leave requests, members removable now, removals in progress, invitations about to expire, members with more than 5% of shares off the primary server, members without shares for 48 hours.

The Members table lists, for each member: address and label, date joined, declared and measured machines (with the client’s correction next to yours), last activity, days without shares, when they become removable, leave requests, shares in the current rounds, and a private note only you can read. The Servers and rounds table shows each server’s round, shares and active members. For each member there is a Receipt button.

Step 6: When a block is found

A group block appears in the console under Blocks of the group with height, date, server, booked or not booked, coinbase value, your output, members paid and the fee output including parts below the threshold. The software books a block once it has its first confirmations; until then it is “not booked”, and no removal or dissolution is possible. The client page shows the same block with the client’s own output and the other members masked. The coins are in the addresses from the block itself and become spendable after 100 confirmations, about 17 hours; the page shows the count. The coinbase is public on any explorer.

Step 7: CSV files and receipts

  • CSV rounds per server: one row per server and member of the current rounds, with shares and parts.
  • CSV blocks: one row per coinbase output of every group block, with the parts below the threshold. Both files carry the SHA-256 of their content in the X-SF-SHA256 response header, the same fingerprint printed in the PDF.
  • Group receipt (PDF): the fixed rules, the addresses, your proof of ownership, the terms version and the hash of the document. The per-member receipt adds the member’s message, signature and declaration. The client downloads the same receipt from the invitation link, which stays valid for that purpose.

Step 8: Remove a member

A member can be removed only after 14 days without shares on every server and with 0 shares in every current round. Press Remove: the console shows the last share on each server and tells you if the removal is possible, and from which date otherwise. Choose a reason from the list: machines switched off, contract ended, client request, moved to another group, other (with a note). The reason is recorded and the client reads it on their page. The removal is executed by SoloFury on every server within a few minutes.

A client can also ask for removal from their page, signing with their address. The request appears in your alerts with the date. From the day the removal becomes possible you have 7 days to execute it; after that SoloFury removes the member. No share is lost: while a member still has shares in a current round, the removal simply waits for that round to end with a block or for the 14 days to pass.

Step 9: Dissolve the group

Dissolve the group is possible only when no member has sent shares in the last 14 days and no block of the group is unbooked. All members are recorded as removed with “group dissolved”; the block history stays readable in the console and on the client pages. A client who moves to a new group of yours signs a new invitation.

The client page

Each member has a page at solofury.com/miner/?addr=<address>&coin=btc, public by address like every miner page of the pool. The Fleet Group panel shows:

  • Your workers in the last hour, the primary server, and a red warning when the machines are in failover on another server: in that case the shares count only toward a block of that other server, so check the pool order on the machines or tell your manager.
  • Your part of the round on each server and If a block came now: your output, the manager’s, the pool fee, and whether your part is below 5,000 sat.
  • The rules as fixed at creation, the group’s size and hashrate, and the blocks of the group with your output on each.
  • Verify the payout now: the page asks the primary server for the real template of the next block and compares the coinbase with the rules. It reports the pool fee, the manager share and your part, or a warning if something does not match. The last check and its date stay on the page.
  • Ask for removal, with a signature, as in Step 8.
  • The date you joined, the declaration you signed, and the receipt.

Only the member’s own part is shown; the other members appear masked in the block history. Telegram alerts for the group (block found, members without shares, members removable, leave requests) are being added to the Fleet bot; this page will say when they are available.

The pool software and your quota

The Fleet servers for Bitcoin run blitzpool (AGPL-3.0), branch main of 22 October 2026, commit d10ab0e. Its Team Mining feature divides a group block among the members by shares, but has no fixed quota for the person who runs the group. We added a change of 43 lines in one file of the group engine (crates/bp-group-solo-engine/src/distribution.rs): it reads the manager quota from a column of the group’s database row and pays it to the manager address as a fixed fraction of the miner part, using the mechanism blitzpool already has for the finder bonus. With the quota at zero the behaviour is identical to the original software. Share validation, block template construction, the coinbase outputs, the block history, the API and the stratum are unchanged. Inside the software your quota is stored in parts per million of the 99% miner part (5% of the block = 50,505 ppm), so the amount can differ from the exact percentage by 1 or 2 satoshi.

How you check it:

  • Before a block: “Verify the payout now” on the client page reads the real template of the next block and shows each output in satoshi, including the manager’s. The check compares the manager output with the group’s quota to the satoshi: if the quota were missing from a server (the column the change reads is added by us, not by a software migration, and an absent value means zero), the page would show the manager share at 0 with a warning, so the client’s check covers the manager’s risk too.
  • After a block: the coinbase is on any explorer; the console and the CSV files list each output.
  • In the code: the full diff is below, with the log of the 98 tests of the group engine run with the change, 4 of them added for it. The same diff is applied on every Fleet server.

Updates. Every new blitzpool version is first run on one server for at least a day, then brought to the others with 7 days’ notice, never while a block of a group is not booked. Security fixes are applied at once and announced afterwards. The version in production and the diff are kept on this page.

The complete diff (sf-group-manager-fee, on blitzpool d10ab0e: 43 lines in distribution.rs plus 4 integration tests)
diff --git a/crates/bp-group-solo-engine/src/distribution.rs b/crates/bp-group-solo-engine/src/distribution.rs
index 451b6c6..db8fb6b 100644
--- a/crates/bp-group-solo-engine/src/distribution.rs
+++ b/crates/bp-group-solo-engine/src/distribution.rs
@@ -171,6 +171,22 @@ async fn compute_distribution(
       .ok_or(DistributionError::GroupNotFound { group_id })?;
   let finder_bonus_ppm = group_row.finder_bonus_ppm.unwrap_or(0).max(0) as u32;

+    // SoloFury patch (sf-group-manager-fee): a fixed manager quota for the
+    // group creator, as ppm of the miner cut, read from the optional column
+    // `pplns_group."managerFeePpm"` (added by the operator, not by a
+    // migration). When set (> 0) it REPLACES the finder bonus: the same
+    // score-weight boost `S·m/(1e6−m)` pays exactly `m` of the miner cut to
+    // the creator's address at every revenue, and the session's own finder
+    // bonus is not applied. A missing column or a NULL/0 value leaves
+    // upstream behaviour untouched. Only the recipient and the ppm change;
+    // share validation, templates and the §4 builder are upstream code.
+    let manager_fee_ppm = sf_manager_fee_ppm(pool, group_id).await;
+    let (bonus_ppm, bonus_address): (u32, AddressId) = if manager_fee_ppm > 0 {
+        (manager_fee_ppm, group_row.creator_address.clone())
+    } else {
+        (finder_bonus_ppm, finder_address.clone())
+    };
+
   // 2. Round state from Redis. Mode-aware: a PROP group reads its per-round
   //    aggregate; a Window group trims to the sliding window first, so the
   //    built distribution is always current (even for an idle group).
@@ -196,8 +212,8 @@ async fn compute_distribution(
       fee_percent: config.fee_percent,
       min_payout_sats: config.min_payout_sats,
       coinbase_weight_budget: config.coinbase_weight_budget,
-        finder_bonus_ppm,
-        finder_address: Some(finder_address),
+        finder_bonus_ppm: bonus_ppm,
+        finder_address: Some(&bonus_address),
       // An empty round is routine (every reset DELs the hash). Only the
       // uncached call bootstraps, and builds are per-finder, so a bootstrap
       // distribution neither outlives the empty round nor reaches another
@@ -238,3 +254,26 @@ mod tests {
       assert_eq!(dist_cfg.coinbase_weight_budget, 60_000);
   }
}
+
+/// SoloFury patch: `pplns_group."managerFeePpm"` if the column exists and the
+/// value is positive, else 0. The query is runtime (not a sqlx macro) so the
+/// offline query cache and the upstream schema stay untouched; a missing
+/// column is logged once per process and treated as 0.
+async fn sf_manager_fee_ppm(pool: &PgPool, group_id: Uuid) -> u32 {
+    static WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
+    let r: Result<Option<i32>, sqlx::Error> =
+        sqlx::query_scalar(r#"SELECT "managerFeePpm" FROM pplns_group WHERE id = $1"#)
+            .bind(group_id)
+            .fetch_one(pool)
+            .await;
+    match r {
+        Ok(Some(ppm)) if ppm > 0 => ppm as u32,
+        Ok(_) => 0,
+        Err(err) => {
+            if WARNED.set(()).is_ok() {
+                tracing::warn!(%err, "sf-group-manager-fee: managerFeePpm unreadable (column missing?); using 0");
+            }
+            0
+        }
+    }
+}
diff --git a/crates/bp-group-solo-engine/tests/sf_manager_fee_integration.rs b/crates/bp-group-solo-engine/tests/sf_manager_fee_integration.rs
new file mode 100644
index 0000000..b4be12c
--- /dev/null
+++ b/crates/bp-group-solo-engine/tests/sf_manager_fee_integration.rs
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//! SoloFury patch `sf-group-manager-fee`: the group creator receives a fixed
+//! quota of the miner cut, read from `pplns_group."managerFeePpm"`.
+//! Needs Postgres (`BP_PG_URL`) and Redis (`BP_REDIS_URL`); skips otherwise.
+
+use std::time::Duration;
+
+use bp_common::AddressId;
+use bp_group_solo_engine::config::GroupSoloEngineConfig;
+use bp_group_solo_engine::distribution::{DistributionBuilder, DistributionConfig};
+use bp_group_solo_engine::round::GroupRoundStore;
+use redis::Client;
+use sqlx::{postgres::PgPoolOptions, PgPool};
+use uuid::Uuid;
+
+const REDIS_URL: &str = "redis://127.0.0.1:16379";
+const PG_URL: &str = "postgres://postgres:postgres@localhost:15433/public_pool";
+const FEE_ADDR: &str = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy";
+const MANAGER: &str = "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3";
+const CLIENT_A: &str = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
+const CLIENT_B: &str = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
+const SUBSIDY: u64 = 312_500_000;
+
+struct H {
+    pool: PgPool,
+    builder: DistributionBuilder,
+    round: GroupRoundStore,
+    group_id: Uuid,
+}
+
+async fn spawn_or_skip(redis_db: u8, manager_ppm: Option<i32>, finder_ppm: Option<i32>) -> Option<H> {
+    let pg_url = std::env::var("BP_PG_URL").unwrap_or_else(|_| PG_URL.to_string());
+    let redis_base = std::env::var("BP_REDIS_URL").unwrap_or_else(|_| REDIS_URL.to_string());
+    let pool = match tokio::time::timeout(
+        Duration::from_secs(2),
+        PgPoolOptions::new().max_connections(2).connect(&pg_url),
+    )
+    .await
+    {
+        Ok(Ok(p)) => p,
+        _ => {
+            eprintln!("PG unavailable — skipping");
+            return None;
+        }
+    };
+    // Own slots in the distribution binary's range (20..23 are unused there).
+    let redis_db =
+        bp_test_support::redis_db_in_range(bp_test_support::redis_db::GS_DISTRIBUTION, redis_db).await;
+    let client = Client::open(format!("{redis_base}/{redis_db}")).ok()?;
+    let mut conn = match tokio::time::timeout(
+        Duration::from_secs(2),
+        bp_test_support::connection_manager(client),
+    )
+    .await
+    {
+        Ok(Ok(c)) => c,
+        _ => {
+            eprintln!("Redis unavailable — skipping");
+            return None;
+        }
+    };
+    redis::cmd("FLUSHDB").query_async::<()>(&mut conn).await.ok()?;
+
+    // The operator adds the column outside the migrations (see the patch).
+    sqlx::query(r#"ALTER TABLE pplns_group ADD COLUMN IF NOT EXISTS "managerFeePpm" integer"#)
+        .execute(&pool)
+        .await
+        .expect("add column");
+    let group_id = Uuid::new_v4();
+    sqlx::query(
+        r#"INSERT INTO pplns_group
+             (id, name, "creatorAddress", "adminTokenHash", active, "createdAt", "updatedAt",
+              "isPublic", "finderBonusPpm", "managerFeePpm")
+           VALUES ($1, $2, $3, $4, true, 0, 0, false, $5, $6)"#,
+    )
+    .bind(group_id)
+    .bind(format!("sf-mgr-{group_id}"))
+    .bind(MANAGER)
+    .bind(format!("hash-{group_id}"))
+    .bind(finder_ppm)
+    .bind(manager_ppm)
+    .execute(&pool)
+    .await
+    .expect("seed group");
+
+    let round = GroupRoundStore::new(conn);
+    let cfg = DistributionConfig::from_engine_config(&GroupSoloEngineConfig {
+        fee_address: Some(AddressId::new(FEE_ADDR).unwrap()),
+        ..GroupSoloEngineConfig::default()
+    });
+    let builder = DistributionBuilder::new(pool.clone(), round.clone(), cfg);
+    Some(H { pool, builder, round, group_id })
+}
+
+async fn cleanup(h: &H) {
+    sqlx::query("DELETE FROM pplns_group WHERE id = $1")
+        .bind(h.group_id)
+        .execute(&h.pool)
+        .await
+        .ok();
+}
+
+fn outputs(h_entries: &[(AddressId, u64)], a: &str) -> Vec<i64> {
+    h_entries
+        .iter()
+        .filter(|(x, _)| x.as_str() == a)
+        .map(|(_, s)| *s as i64)
+        .collect()
+}
+
+#[tokio::test]
+async fn manager_quota_is_an_exact_fraction_of_the_miner_cut() {
+    // 5 % of the block with a 1 % pool fee = 50 505 ppm of the miner cut.
+    let Some(h) = spawn_or_skip(20, Some(50_505), None).await else { return };
+    let a = AddressId::new(CLIENT_A).unwrap();
+    let b = AddressId::new(CLIENT_B).unwrap();
+    let g = h.group_id.to_string();
+    h.round.record_share(None, &g, a.as_str(), 75.0, 1).await.unwrap();
+    h.round.record_share(None, &g, b.as_str(), 25.0, 2).await.unwrap();
+
+    let result = h.builder.build(h.group_id, SUBSIDY, &a).await.expect("ok");
+    let entries = result.distribution.payout_entries_at(SUBSIDY).expect("§4 vector");
+    let pot = bp_share::miner_pot_sats(result.distribution.fee_ppm, SUBSIDY) as i64;
+
+    let mgr = outputs(&entries, MANAGER);
+    assert_eq!(mgr.len(), 1, "manager has exactly one output");
+    let expected_mgr = pot * 50_505 / 1_000_000;
+    assert!((mgr[0] - expected_mgr).abs() <= 2, "manager {} expected {}", mgr[0], expected_mgr);
+
+    let rest = pot - mgr[0];
+    let oa = outputs(&entries, CLIENT_A)[0];
+    let ob = outputs(&entries, CLIENT_B)[0];
+    assert!((oa - rest * 3 / 4).abs() <= 2, "client A {} expected {}", oa, rest * 3 / 4);
+    assert!((ob - rest / 4).abs() <= 2, "client B {} expected {}", ob, rest / 4);
+    // The fee output is still exactly the pool fee: nothing of the quota leaks to the pool.
+    let fee = outputs(&entries, FEE_ADDR)[0];
+    assert!((fee - (SUBSIDY as i64 - pot)).abs() <= 2, "fee {} expected {}", fee, SUBSIDY as i64 - pot);
+    cleanup(&h).await;
+}
+
+#[tokio::test]
+async fn manager_quota_replaces_the_finder_bonus_and_is_finder_independent() {
+    let Some(h) = spawn_or_skip(21, Some(20_202), Some(300_000)).await else { return };
+    let a = AddressId::new(CLIENT_A).unwrap();
+    let b = AddressId::new(CLIENT_B).unwrap();
+    let g = h.group_id.to_string();
+    h.round.record_share(None, &g, a.as_str(), 50.0, 1).await.unwrap();
+    h.round.record_share(None, &g, b.as_str(), 50.0, 2).await.unwrap();
+
+    let ra = h.builder.build(h.group_id, SUBSIDY, &a).await.expect("ok");
+    let rb = h.builder.build(h.group_id, SUBSIDY, &b).await.expect("ok");
+    let ea = ra.distribution.payout_entries_at(SUBSIDY).unwrap();
+    let eb = rb.distribution.payout_entries_at(SUBSIDY).unwrap();
+    // Same coinbase whoever the finder is: equal peers get equal outputs, the
+    // 30 % finder bonus of the row is NOT applied.
+    assert_eq!(outputs(&ea, CLIENT_A), outputs(&ea, CLIENT_B));
+    assert_eq!(outputs(&ea, MANAGER), outputs(&eb, MANAGER));
+    assert_eq!(outputs(&ea, CLIENT_A), outputs(&eb, CLIENT_A));
+    cleanup(&h).await;
+}
+
+#[tokio::test]
+async fn zero_or_null_quota_keeps_upstream_behaviour() {
+    let Some(h) = spawn_or_skip(22, None, Some(3_200)).await else { return };
+    let a = AddressId::new(CLIENT_A).unwrap();
+    let b = AddressId::new(CLIENT_B).unwrap();
+    let g = h.group_id.to_string();
+    h.round.record_share(None, &g, a.as_str(), 50.0, 1).await.unwrap();
+    h.round.record_share(None, &g, b.as_str(), 50.0, 2).await.unwrap();
+    let r = h.builder.build(h.group_id, SUBSIDY, &a).await.expect("ok");
+    let e = r.distribution.payout_entries_at(SUBSIDY).unwrap();
+    assert!(outputs(&e, MANAGER).is_empty(), "no manager output without a quota");
+    assert!(outputs(&e, CLIENT_A)[0] > outputs(&e, CLIENT_B)[0], "upstream finder bonus still applies");
+    cleanup(&h).await;
+}
+
+#[tokio::test]
+async fn empty_round_bootstrap_still_pays_the_manager_quota() {
+    let Some(h) = spawn_or_skip(23, Some(50_505), None).await else { return };
+    let a = AddressId::new(CLIENT_A).unwrap();
+    let r = h.builder.build(h.group_id, SUBSIDY, &a).await.expect("ok");
+    let e = r.distribution.payout_entries_at(SUBSIDY).unwrap();
+    let pot = bp_share::miner_pot_sats(r.distribution.fee_ppm, SUBSIDY) as i64;
+    let mgr = outputs(&e, MANAGER);
+    assert_eq!(mgr.len(), 1);
+    assert!((mgr[0] - pot * 50_505 / 1_000_000).abs() <= 2);
+    assert!((outputs(&e, CLIENT_A)[0] - (pot - mgr[0])).abs() <= 2, "bootstrap claimant gets the rest");
+    cleanup(&h).await;
+}
Test log of the group engine with the change (24 October 2026, 98 tests, 0 failed)
# blitzpool-sf test run — 2026-10-24T00:29Z — base d10ab0e + sf-group-manager-fee (cb06164)
rustc rustc 1.97.0 (2d8144b78 2026-07-07) · Postgres 16 (migrations 0000-0022) · Redis 8 (1024 db)
   Running unittests src/lib.rs (target/debug/deps/bp_group_solo_engine-2d27263d2fe5c576)
test config::tests::a_non_zero_fee_validates ... ok
test config::tests::a_typo_in_the_fee_address_is_refused ... ok
test config::tests::default_validates_clean ... ok
test config::tests::fee_percent_above_hundred_rejects ... ok
test config::tests::fee_percent_nan_rejects ... ok
test config::tests::fee_percent_negative_rejects ... ok
test config::tests::min_payout_below_dust_rejects ... ok
test config::tests::min_payout_exactly_dust_accepts ... ok
test config::tests::the_default_config_is_refused_because_it_has_no_fee_address ... ok
test config::tests::weight_budget_too_low_rejects ... ok
test distribution::tests::distribution_config_from_engine_config_carries_fields ... ok
test engine::tests::engine_error_carries_source_variants ... ok
test engine::tests::history_rows_transcribe_every_paid_output_besides_the_pool ... ok
test engine::tests::lookup_error_reuses_cached_mode_never_misroutes_window ... ok
test engine::tests::trim_watermark_gates_to_new_buckets_only ... ok
test reset::tests::custom_across_the_dst_fall_back_fires_at_local_midnight ... ok
test reset::tests::daily_on_the_dst_fall_back_day_moves_to_the_next_day ... ok
test reset::tests::next_fire_custom_no_last_reset_fires_at_next_midnight ... ok
test reset::tests::next_fire_custom_with_recent_last_reset_skips_until_interval_elapsed ... ok
test reset::tests::next_fire_daily_in_utc ... ok
test reset::tests::next_fire_daily_in_zurich_tz ... ok
test reset::tests::next_fire_monthly_lands_on_first ... ok
test reset::tests::next_fire_on_a_day_without_midnight_does_not_panic ... ok
test reset::tests::next_fire_weekly_lands_on_monday ... ok
test reset::tests::preset_from_wire_strings ... ok
test reset::tests::reset_runner_is_cloneable_without_c_clone_bound ... ok
test reset::tests::reset_schedule_from_row_handles_missing_fields ... ok
test reset::tests::weekly_across_the_dst_fall_back_lands_on_monday ... ok
test round::tests::best_share_partial_eq ... ok
test round::tests::key_helpers_format_correctly ... ok
test round::tests::round_stats_total_is_sum_of_per_address ... ok
test round::tests::window_key_helpers_format_correctly ... ok
test reset::tests::monthly_across_the_dst_fall_back_lands_on_the_first ... ok
test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
   Running tests/distribution_integration.rs (target/debug/deps/distribution_integration-94ee904bdefb95c1)
test an_empty_round_pays_the_finder_not_the_whole_block_to_the_pool ... ok
test build_for_nonexistent_group_returns_group_not_found ... ok
test a_share_after_a_bootstrap_reaches_the_next_build ... ok
test build_with_shares_returns_payouts_and_writes_snapshot ... ok
test concurrent_same_finder_builds_share_one_compute ... ok
test finder_bonus_from_db_row_is_applied ... ok
test distinct_rewards_for_same_group_finder_run_independently ... ok
test invalidate_all_triggers_fresh_compute ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
   Running tests/engine_integration.rs (target/debug/deps/engine_integration-7a149d8fac6cb6fa)
test a_group_coinbase_below_the_block_subsidy_is_refused ... ok
test a_group_block_far_off_the_reference_is_still_booked ... ok
test a_redelivered_apply_books_nothing ... ok
test a_redelivered_apply_does_not_reset_the_round_again ... ok
test a_retry_completes_a_reset_the_first_apply_did_not_reach ... ok
test a_richer_block_leaves_nobody_owing ... ok
test block_found_books_the_job_coinbase_after_the_round_moved ... ok
test build_distribution_returns_payouts_after_shares ... ok
test concurrent_applies_of_one_block_book_it_once ... ok
test duplicate_block_found_does_not_double_the_history ... ok
test manual_reset_wipes_group_state ... ok
test on_block_found_applies_distribution_and_resets_round ... ok
test on_block_found_keeps_round_when_reset_flag_false ... ok
test reader_best_difficulty_after_shares ... ok
test on_block_found_with_finder_bonus_merges_duplicate_outputs ... ok
test record_reject_updates_round_rejected_total ... ok
test record_share_then_round_stats_sees_it ... ok
test reschedule_group_arms_and_tears_down_reset_cron ... ok
test spawn_core_skips_startup_reset_crons ... ok
test window_grow_invalidates_mode_cache_keeps_in_window_bucket ... ok
test window_mode_kick_drops_the_member_from_the_payout_source ... ok
test window_mode_record_path_trims_aged_buckets ... ok
test window_mode_reject_is_windowed_not_tallied ... ok
test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.56s
   Running tests/regtest_group_solo_block_submit.rs (target/debug/deps/regtest_group_solo_block_submit-22aa8a04b030d4a0)
test group_solo_three_member_distribution_block_accepted_by_core ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
   Running tests/reset_integration.rs (target/debug/deps/reset_integration-381fc545f8d05a69)
test reset_custom_preset_skips_if_interval_not_elapsed ... ok
test reset_custom_preset_fires_when_interval_elapsed ... ok
test reset_debounce_skips_within_60_seconds ... ok
test reset_fires_past_debounce_window ... ok
test reset_nonexistent_group_returns_error ... ok
test reset_scheduled_wipes_redis_pg_state_and_stamps ... ok
test reset_one_group_does_not_affect_another ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
   Running tests/round_integration.rs (target/debug/deps/round_integration-0ffcfc32bf229e96)
test a_redelivered_share_is_still_deduped_across_a_round_reset ... ok
test forget_member_subtracts_contribution ... ok
test multiple_groups_redis_state_is_isolated ... ok
test forget_member_window_removes_the_address_from_both_lanes ... ok
test read_by_address_returns_aggregate ... ok
test read_payout_shares_window_trims_on_read ... ok
test record_reject_increments_per_address ... ok
test read_round_stats_returns_per_address_and_rejected ... ok
test record_share_writes_all_keys ... ok
test record_share_is_idempotent_per_share_id ... ok
test reset_full_wipes_everything_including_last_accepted ... ok
test reset_for_block_found_preserves_last_accepted_share_at ... ok
test update_best_share_only_replaces_on_improvement ... ok
test round_stats_window_reads_the_reject_lane_not_the_running_tally ... ok
test windowed_record_is_idempotent_per_share_id ... ok
test windowed_record_aggregates_into_buckets ... ok
test windowed_reject_lands_in_its_bucket_and_window_aggregate ... ok
test windowed_trim_and_reset_cover_the_reject_lane ... ok
test windowed_trim_does_not_strand_a_negative_entry ... ok
test windowed_trim_drops_aged_buckets ... ok
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
   Running tests/sf_manager_fee_integration.rs (target/debug/deps/sf_manager_fee_integration-2f0a9f24a1978b6e)
test manager_quota_is_an_exact_fraction_of_the_miner_cut ... ok
test empty_round_bootstrap_still_pays_the_manager_quota ... ok
test zero_or_null_quota_keeps_upstream_behaviour ... ok
test manager_quota_replaces_the_finder_bonus_and_is_finder_independent ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
   Running tests/stream_equivalence.rs (target/debug/deps/stream_equivalence-1186db8b4e5fe73c)
test in_process_and_stream_paths_leave_identical_group_solo_round ... ok
test duplicate_entries_in_the_stream_do_not_break_group_solo_equivalence ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Rules in one table

RuleValue
Pool fee1% of the block, fixed; parts below the threshold stay in this output
Your quota1% to 49% of the whole block, up to two decimals, one value per group, fixed at creation
Division among membersIn proportion to the shares sent to the server that found the block, since the previous block found there
Minimum payout5,000 satoshi per member per block
Finder bonusNone
MembersMaximum 52 per group
Primary serverChosen at creation; the group is copied to the other Fleet servers as failover
Addressesbc1q… and bc1p…; one fresh manager address per group; each client address in one group or agreement only
Invitation24 hours, 3 days or 7 days; one use; can be revoked until signed
CredentialsPrimary server at once; third pool entry when the group is on every server
Removal of a memberAfter 14 days without shares on every server, with a reason; the client can ask for it; SoloFury steps in after 7 days
DissolutionOnly with no member shares in the last 14 days and no unbooked block
BlocksBooked after the first confirmations; coins spendable after 100
Dedicated serverFrom 100 PH/s, on request, a server near your farm run by SoloFury as primary server
Support[email protected], reply within 1 business day

The full rules are in the Fleet terms, version 1.3, section 13.

What the numbers say

The group finds blocks as one, so the group’s hashrate sets the frequency, and each member’s share of the work sets the size of their part. The table uses the network difficulty of 16 October 2026.

Group hashrateAverage time per blockBlocks per yearAt least 1 block in 12 months
20 PH/s349 days1.065%
50 PH/s140 days2.693%
100 PH/s70 days5.299.5%
200 PH/s35 days10.5above 99.9%

Two examples with a 5% manager quota. A client with two machines, about 470 TH/s, in a 20 PH/s group has 2.35% of the shares: about 0.069 BTC per block on the subsidy alone, with the group finding about one block a year. A member with 2 PH/s in a 50 PH/s group has 4% of the shares: about 0.1175 BTC per block on the subsidy alone, about 0.31 BTC per year on average. Your quota comes from every block of the group: with 5% of the block and 2.6 blocks per year, about 0.41 BTC per year on average. Averages over many years, not an income: a block can come tomorrow or take much longer.

Source: network difficulty 140.58 T, set at block 971,712 on 16 October 2026. Formula: average time = difficulty × 232 ÷ hashrate; chance in a period t = 1 − e−t/T. Network fees excluded.

How we work

  • Non-custodial payout. The outputs are in the coinbase we build; the network creates the bitcoin at each address. We never hold a member’s part.
  • Ownership by signature. You prove the manager address, each client proves their own address. Signatures, messages and the declaration are kept and printed in the receipt.
  • Rules written once. Quota, server, threshold, limit and round rule are fixed at creation and shown identically in the console, on the client page, in the receipt and in the terms.
  • Published software. The change to the pool software is on this page in full, with its tests, and every client can read the next block’s coinbase at any time.
  • Announced changes. Updates that may interrupt mining are announced 7 days ahead on pages, Telegram and email; security fixes are applied at once and announced afterwards.

Conclusion

Fleet Group takes one signature from you and one from each client. From then on, every block found on the primary server pays you, us and the members in the coinbase, by shares, with rules nobody can change. The group finds blocks far more often than a single client, and each number on the console, the client page, the receipt and the CSV files comes from the chain or from the next block’s real template. Send your clients this page with the invitation: it answers the questions they have before they sign.

Open a group for your clients

One primary server, your fixed quota, the rest to the members by shares, all in the coinbase. Write to [email protected] if you are not a Fleet operator yet, or open the console.

Open the console →

Read next

Frequently Asked Questions

Is every block of the group divided among all members?

A block is divided among the members who sent shares to the server that found it, since the previous block found on that server. Each Fleet server keeps its own round. Machines that mine on the primary server, as the credentials say, take part in every block of that server.

Can I change my quota or the primary server after the group is created?

No. Quota, primary server, threshold, maximum members and the round rule are written at creation and the clients sign them. A different rule means a new group and new signatures.

What happens to a part below 5,000 satoshi?

It is not paid and stays in the SoloFury fee output. The client page shows it before and after the block, with the amount. A 1 TH/s miner in a 100 PH/s group is below the threshold on a normal block; in a 50 PH/s group it is just above.

Can a client start mining right after signing?

Yes. The primary server is ready at once and a block found there pays by the rules from the first share. The third pool entry, on another Fleet server, appears on the client page once the group is copied everywhere, within a few minutes.

How does a client leave the group?

The client disconnects the machines and, if they want, asks for removal from their page with a signature. After 14 days without shares on every server, the manager removes the member with a reason the client can read; if the manager does nothing for 7 days, SoloFury does it. Shares already sent in a round still count.

Why did you change the pool software, and can I read the change?

blitzpool divides a group block among members but has no fixed quota for the person who runs the group. We added 43 lines that pay that quota to the manager address. The complete diff and the test log are on this page, and the client page can show the real coinbase of the next block at any time.

Is there a bonus for the member who finds the block?

No. The finder bonus is set to zero in every Fleet Group. The block is divided only by shares in the round.

What size of group makes sense?

The group finds blocks as one. At the difficulty of October 2026, 50 PH/s finds 2.6 blocks per year on average and 100 PH/s about 5.2. A group has at most 52 members, and parts below 5,000 sat are not paid, so very small miners fit better in a smaller group.