From a6bcc69288d4886e0af3511fa9340fc9a360bc4f Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 13 Jul 2026 18:51:40 +0200 Subject: [PATCH] sol mainnet payouts tasks --- docs/PRD.md | 2 +- ...tai-off-chain-accrual-and-reserved-mint.md | 52 ++++++++ ...9-mainnet-usdt-cutover-two-wallet-pilot.md | 101 +++++++++++++++ docs/prd.json | 23 +++- packages/tracker/meshnet_tracker/billing.py | 3 + packages/tracker/meshnet_tracker/cli.py | 13 +- tests/test_settlement_loop.py | 119 +++++++++++++++++- 7 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 docs/adr/0025-tai-off-chain-accrual-and-reserved-mint.md create mode 100644 docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md diff --git a/docs/PRD.md b/docs/PRD.md index 334b97b..c497673 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -92,7 +92,7 @@ The gateway receives a client request, asks the tracker for an inference route ( 7. Begin accepting inference connections ### Payment flow (ADR-0015 supersedes ADR-0002 settlement mechanics) -Clients pre-fund an API key with USDT. The tracker meters each request against the off-chain ledger. Periodic settlement batches USDT payouts from the custodial treasury to node operators proportional to work units. Fraud penalties forfeit pending balance (ADR-0018); strike/ban state persists in the tracker registry. TAI token emission remains deferred (ADR-0002 roadmap). +Clients pre-fund an API key with USDT. The tracker meters each request against the off-chain ledger. Periodic settlement batches USDT payouts from the custodial treasury to node operators proportional to work units (default: every 24 h or when pending ≥ 5 USDT). Fraud penalties forfeit pending balance (ADR-0018); strike/ban state persists in the tracker registry. TAI reward accrual is deferred — see ADR-0025 for reserved-mint / off-chain phase B/C; ADR-0002 roadmap for public listing. ### Fraud detection (ADR-0018; historical ADR-0003) Validators re-run ~5% of completed requests with TOPLOC activation verification. Caught cheaters forfeit pending balance and receive strikes; three strikes bans the wallet. Probation (first N unpaid jobs) remains the anti-sybil re-entry cost. diff --git a/docs/adr/0025-tai-off-chain-accrual-and-reserved-mint.md b/docs/adr/0025-tai-off-chain-accrual-and-reserved-mint.md new file mode 100644 index 0000000..820beaf --- /dev/null +++ b/docs/adr/0025-tai-off-chain-accrual-and-reserved-mint.md @@ -0,0 +1,52 @@ +# ADR-0025: TAI reserved mint and off-chain accrual (phase B/C) + +## Status: Accepted + +## Context + +ADR-0015 chose **USDT-direct custodial settlement** for alpha and near-term production. Clients pay USDT; nodes receive batched USDT SPL payouts. ADR-0002's TAI reward token, revenue-backed floor, and open-market listing gates remain the long-term design but are **not** the live payment path. + +The owner wants TAI to exist without the cost and legal surface of a public launch: no AMM, no open listing, no client-facing TAI, no on-chain stake machinery. + +## Decision + +### Phase B — Reserved mainnet mint (cheap, optional early) + +- Create a fixed-supply TAI SPL mint on **mainnet** when treasury work happens (~0.002 SOL). +- Entire initial supply sits in a **team-controlled** wallet (same custody posture as the USDT treasury today). +- **No public emission, no market, no client UX.** Mint exists for name reservation and future programmatic rewards only. +- Document mint address in operator config; do not advertise to users. + +### Phase C — Off-chain TAI accrual alongside USDT (before automatic on-chain TAI payouts) + +- Extend the billing ledger with **`tai_pending[wallet]`** accrued from completed inference work using a simple rule (e.g. USDT node share × configurable TAI-per-USDT rate, or fixed TAI per work unit). +- TAI accrual is **display-only + ledger-persisted** initially; nodes see pending TAI in dashboard/CLI. +- **Clients never pay or hold TAI.** USDT remains the only client-facing asset. +- Optional manual or scheduled **TAI SPL batch transfers** from the team wallet (same batching pattern as USDT `send_payouts`) — operator-triggered until automatic emission is justified by volume. +- The existing **10% protocol USDT cut** continues to accumulate as future TAI liquidity per ADR-0015/0002; do not redirect it until a deliberate liquidity event. + +### Explicit non-goals (this ADR) + +- Open-market listing, AMM, or DEX liquidity +- Buyback floor endpoint or backing-price oracle (ADR-0002 machinery) +- On-chain stake deposits or slash contracts +- Paying clients rebates or accepting TAI for inference +- Replacing USDT node payouts with TAI-only payouts before volume gates in ADR-0002 pass + +## Relation to ADR-0002 listing gates + +Public TAI listing stays gated on **$50k cumulative USDT volume** and **25+ nodes / 15+ wallets**. Phase B/C may proceed **below** those gates because they do not create a public market — only reserved supply and off-chain accounting. + +Securities review remains required before any **public** distribution or listing; off-chain accrual to hired/known operators with manual SPL transfers is an operator discretion, not a product promise. + +## Consequences + +- USDT mainnet pilot (two-wallet setup) is unblocked without TAI complexity. +- TAI narrative is preserved at minimal cost (mint + ledger column + optional manual transfers). +- Automatic TAI emission can later reuse the US-033 settlement loop shape with a second mint and separate pending bucket. +- Dashboard and APIs must label TAI balances as **non-withdrawable** until an on-chain payout batch confirms. + +## Verification + +- USDT settlement tests remain authoritative for production payouts (`tests/test_settlement_loop.py`). +- When phase C lands: ledger tests for `tai_pending` accrual, idempotent gossip replication, and optional TAI batch payout adapter tests mirroring USDT. diff --git a/docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md b/docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md new file mode 100644 index 0000000..116cb7e --- /dev/null +++ b/docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md @@ -0,0 +1,101 @@ +Status: ready-for-agent + +# US-049 — Mainnet USDT cutover: two-wallet pilot checklist + +Priority: High (first real-money friends test) +Stage: Operator runbook + config verification + +## Goal + +Move from **Solana devnet + mock-USDT** to **Solana mainnet + real USDT** for a minimal pilot: **one client wallet** (inference payer) and **one node-operator wallet** (payout recipient). Treasury holds USDT and pays SOL fees. TAI stays phase B/C per [ADR-0025](../adr/0025-tai-off-chain-accrual-and-reserved-mint.md). + +## Wallet roles + +| Role | Keypair | On-chain use | +|---|---|---| +| **Treasury** | Operator `treasury-keypair.json` (multisig when ready) | Holds USDT float + SOL for fees; sends batched node payouts | +| **Client** | Your inference-user wallet | SPL USDT → treasury; bound to API key for ledger credit | +| **Node** | Your node-operator wallet | Receives USDT payout batches from treasury | + +The node process already creates/loads a Solana wallet at startup; the client wallet is bound via accounts/dashboard (`POST /v1/wallet/register` or US-041 flows). + +## Pre-flight (devnet smoke — do not skip) + +- [ ] Tracker with `--solana-rpc-url https://api.devnet.solana.com`, mock mint, treasury keypair +- [ ] `--settle-period 60 --payout-threshold 0` — confirm payout appears on dashboard **Settlement history** with explorer link +- [ ] Run `python -m pytest tests/test_settlement_loop.py -q` — includes prod 24h/5 USDT gate tests +- [ ] One inference request → node pending → settlement tx → node wallet balance increases + +## Mainnet config change (config-only cutover) + +Replace devnet values; **no code deploy required** beyond what is already on the branch. + +```bash +# Example — use your mainnet RPC provider +meshnet-tracker start \ + --solana-rpc-url https://api.mainnet-beta.solana.com \ + --usdt-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ + --treasury-keypair ~/.config/solana/meshnet-treasury-mainnet.json \ + --settle-period 86400 \ + --payout-threshold 5.0 \ + --payout-dust-floor 0.01 \ + --starting-credit 0 \ + --devnet-topup 0 +``` + +| Flag | Devnet (test) | Mainnet (pilot) | +|---|---|---| +| RPC | `api.devnet.solana.com` | Mainnet RPC (Helius/QuickNode/etc.) | +| `--usdt-mint` | mock mint from `devnet_setup.py` | Real USDT mint (`EPjF…` on Solana) | +| `--settle-period` | `60` (fast verify) | `86400` (24 h) | +| `--payout-threshold` | `0` | `5.0` USDT | +| `--starting-credit` | `1.0` (optional) | `0` | +| `--devnet-topup` | `1.0` | `0` | + +## Treasury funding + +- [ ] Fund treasury wallet with **SOL** for fees (~0.1–0.5 SOL to start; ~$0.001 per daily batch + ~$0.30 once per new node ATA) +- [ ] Fund treasury with **USDT** for node payouts (your float — e.g. first week of expected node earnings) +- [ ] Client wallet holds USDT; send a test SPL transfer to treasury ATA; confirm deposit watcher credits API key within one poll interval + +## Two-wallet pilot steps + +1. **Start tracker** on mainnet config above (single settlement tracker per ADR-0016). +2. **Client path:** register account → create API key → bind **client wallet** → deposit USDT to treasury → verify ledger balance on dashboard. +3. **Node path:** start `meshnet-node` with **node wallet** keypair → register → serve inference. +4. **Inference:** client sends `POST /v1/chat/completions` with API key; verify 402 before deposit, success after. +5. **Accrual:** confirm node **pending USDT** on dashboard rises; client balance debits. +6. **Payout (24 h):** wait for `--settle-period` **or** temporarily lower to `300` for first pilot verification, then restore `86400`. +7. **Threshold path:** alternatively, accumulate ≥ `5` USDT pending in one session to trigger immediate batch without waiting 24 h. +8. **Verify on-chain:** settlement history shows mainnet tx signature; node wallet USDT ATA balance increased; pending zeroed. + +## Safety checks + +- [ ] `--devnet-topup 0` — no faucet on mainnet +- [ ] `--starting-credit 0` — no free inference credit +- [ ] Treasury keypair not committed to git; file mode 600 +- [ ] Plan multisig migration before large float ([alpha runbook](../../.scratch/alpha-hardening/runbooks/02-treasury-key-rotation.md) intent) +- [ ] Issue **21** (TOPLOC calibration) before production audit thresholds on untrusted nodes + +## Cost estimate (this pilot) + +- **SOL fees:** pennies per day at 1 batch × 1 node +- **USDT:** whatever clients deposit and nodes earn (treasury is passthrough for payouts) +- **TAI mint (optional, ADR-0025 phase B):** ~0.002 SOL one-time — defer if not needed for pilot + +## Acceptance criteria + +- [ ] Devnet checklist completed once +- [ ] Mainnet tracker serves dashboard; billing enabled +- [ ] Client wallet deposit → ledger credit → inference → debit +- [ ] Node wallet receives ≥1 confirmed USDT payout batch on mainnet +- [ ] 24 h period enforced: sub-threshold pending not paid before period (covered by `tests/test_settlement_loop.py`) +- [ ] ≥5 USDT pending triggers payout without waiting full period (covered by tests) +- [ ] Rollback documented: switch RPC + mint back to devnet if needed + +## Related + +- ADR-0015 (USDT custodial settlement) +- ADR-0025 (TAI reserved mint / off-chain accrual — not blocking this pilot) +- US-033 / US-032 (settlement + deposits) +- `scripts/devnet_setup.py` (devnet only) diff --git a/docs/prd.json b/docs/prd.json index c10a8c2..4ed7cf2 100644 --- a/docs/prd.json +++ b/docs/prd.json @@ -1051,10 +1051,31 @@ "US-046" ], "completionNotes": "Skip auto-join when model explicit; sequential source try with progress; 30 s model-source timeout; assignment_vram_mb for CPU; per-file /v1/model-files/download with manifest and retries. Remaining: live Windows two-machine retest." + }, + { + "id": "US-049", + "title": "49 — Mainnet USDT cutover: two-wallet pilot checklist", + "description": "Operator runbook to move from Solana devnet + mock-USDT to mainnet + real USDT for a minimal pilot: one client wallet (deposits USDT, pays for inference) and one node wallet (receives batched payouts). Treasury holds USDT float and SOL for fees. TAI deferred per ADR-0025.", + "acceptanceCriteria": [ + "Devnet smoke completed: settlement loop pays with --settle-period 60 and mock mint", + "python -m pytest tests/test_settlement_loop.py -q passes (includes 24 h / 5 USDT gate tests)", + "Mainnet tracker configured: real USDT mint, --starting-credit 0, --devnet-topup 0, --settle-period 86400", + "Client wallet deposit credits API key ledger; inference debits balance", + "Node wallet receives at least one confirmed mainnet USDT payout batch", + "Sub-threshold pending not paid before 24 h; ≥5 USDT pending triggers immediate payout" + ], + "priority": 49, + "status": "open", + "notes": "Source issue: docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md. ADR-0025 covers optional TAI mint; not blocking this pilot.", + "dependsOn": [ + "US-032", + "US-033", + "US-039" + ] } ], "metadata": { - "updatedAt": "2026-07-13T16:15:00.000Z", + "updatedAt": "2026-07-13T16:40:00.000Z", "statusVocabulary": { "open": "Not started", "in-design": "Decisions pending before implementation can begin", diff --git a/packages/tracker/meshnet_tracker/billing.py b/packages/tracker/meshnet_tracker/billing.py index d3e9442..c6b7576 100644 --- a/packages/tracker/meshnet_tracker/billing.py +++ b/packages/tracker/meshnet_tracker/billing.py @@ -21,6 +21,9 @@ import uuid DEFAULT_PRICE_PER_1K_TOKENS = 0.02 # USDT DEFAULT_STARTING_CREDIT = 0.0 # USDT; accounts must be funded before inference DEFAULT_BILLING_DB_PATH = "billing.sqlite" +DEFAULT_SETTLE_PERIOD = 86400.0 # seconds — max time between node payouts (24 h prod) +DEFAULT_PAYOUT_THRESHOLD = 5.0 # USDT — immediate payout when pending exceeds this +DEFAULT_PAYOUT_DUST_FLOOR = 0.01 # USDT — never pay less than this NODE_REVENUE_SHARE = 0.90 # nodes 90% / protocol cut 10% (ADR-0015) diff --git a/packages/tracker/meshnet_tracker/cli.py b/packages/tracker/meshnet_tracker/cli.py index 79aec3f..a4629df 100644 --- a/packages/tracker/meshnet_tracker/cli.py +++ b/packages/tracker/meshnet_tracker/cli.py @@ -8,7 +8,12 @@ import time from pathlib import Path from .accounts import DEFAULT_ACCOUNTS_DB_PATH -from .billing import DEFAULT_BILLING_DB_PATH +from .billing import ( + DEFAULT_BILLING_DB_PATH, + DEFAULT_PAYOUT_DUST_FLOOR, + DEFAULT_PAYOUT_THRESHOLD, + DEFAULT_SETTLE_PERIOD, +) from .capability import ALL_POLICIES as ALL_CAPABILITY_POLICIES from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH from .logging_setup import ( @@ -237,19 +242,19 @@ def main() -> None: common.add_argument( "--settle-period", type=float, - default=86400.0, + default=DEFAULT_SETTLE_PERIOD, help="Max seconds between payouts to a node (dev: 60, prod: 86400)", ) common.add_argument( "--payout-threshold", type=float, - default=5.0, + default=DEFAULT_PAYOUT_THRESHOLD, help="Pending USDT that triggers an immediate payout (dev: 0)", ) common.add_argument( "--payout-dust-floor", type=float, - default=0.01, + default=DEFAULT_PAYOUT_DUST_FLOOR, help="Never pay out less than this many USDT", ) common.add_argument( diff --git a/tests/test_settlement_loop.py b/tests/test_settlement_loop.py index 443ee64..347e815 100644 --- a/tests/test_settlement_loop.py +++ b/tests/test_settlement_loop.py @@ -12,7 +12,12 @@ import urllib.request import pytest from meshnet_contracts import LocalSolanaContracts -from meshnet_tracker.billing import BillingLedger +from meshnet_tracker.billing import ( + BillingLedger, + DEFAULT_PAYOUT_DUST_FLOOR, + DEFAULT_PAYOUT_THRESHOLD, + DEFAULT_SETTLE_PERIOD, +) from meshnet_tracker.server import TrackerServer MODEL = "stub-model" @@ -172,3 +177,115 @@ def test_non_leader_never_signs(): assert ledger.get_node_pending("wallet-a") > 0 finally: tracker.stop() + + +# Production defaults — sourced from billing.py (cli.py uses the same constants) +PROD_SETTLE_PERIOD = DEFAULT_SETTLE_PERIOD +PROD_PAYOUT_THRESHOLD = DEFAULT_PAYOUT_THRESHOLD +PROD_DUST_FLOOR = DEFAULT_PAYOUT_DUST_FLOOR + + +def _set_pending_since(ledger: BillingLedger, wallet: str, ts: float) -> None: + with ledger._lock: + ledger._pending_since[wallet] = ts + + +def test_prod_settlement_constants(): + "Production settlement policy is 24 h period, 5 USDT threshold, 0.01 dust floor.\n\nTags: billing, settlement" + assert DEFAULT_SETTLE_PERIOD == 86400.0 + assert DEFAULT_PAYOUT_THRESHOLD == 5.0 + assert DEFAULT_PAYOUT_DUST_FLOOR == 0.01 + + +def test_payables_24h_period_not_due_before_elapsed(): + "Sub-threshold pending is not payable until the 24 h period elapses.\n\nTags: billing, settlement" + ledger = BillingLedger(starting_credit=100.0, default_price_per_1k=0.02) + ledger.charge_request("client", MODEL, 1000, [("node-wallet", 12)]) + assert ledger.get_node_pending("node-wallet") == pytest.approx(0.018) + + t0 = 1_700_000_000.0 + _set_pending_since(ledger, "node-wallet", t0) + due_early = ledger.payables( + threshold=PROD_PAYOUT_THRESHOLD, + max_period=PROD_SETTLE_PERIOD, + dust_floor=PROD_DUST_FLOOR, + now=t0 + PROD_SETTLE_PERIOD - 1.0, + ) + assert due_early == [] + + +def test_payables_24h_period_triggers_at_boundary(): + "Sub-threshold pending becomes payable exactly after the 24 h period.\n\nTags: billing, settlement" + ledger = BillingLedger(starting_credit=100.0, default_price_per_1k=0.02) + ledger.charge_request("client", MODEL, 1000, [("node-wallet", 12)]) + pending = ledger.get_node_pending("node-wallet") + + t0 = 1_700_000_000.0 + _set_pending_since(ledger, "node-wallet", t0) + due = ledger.payables( + threshold=PROD_PAYOUT_THRESHOLD, + max_period=PROD_SETTLE_PERIOD, + dust_floor=PROD_DUST_FLOOR, + now=t0 + PROD_SETTLE_PERIOD, + ) + assert due == [("node-wallet", pytest.approx(pending))] + + +def test_payables_threshold_triggers_before_24h_period(): + "Pending ≥ 5 USDT pays immediately without waiting the 24 h period.\n\nTags: billing, settlement" + ledger = BillingLedger(starting_credit=1000.0, default_price_per_1k=0.02) + # cost = 300k * 0.02 / 1k = 6.0 USDT → node share 5.4 USDT + ledger.charge_request("client", MODEL, 300_000, [("node-wallet", 12)]) + assert ledger.get_node_pending("node-wallet") >= PROD_PAYOUT_THRESHOLD + + due = ledger.payables( + threshold=PROD_PAYOUT_THRESHOLD, + max_period=PROD_SETTLE_PERIOD, + dust_floor=PROD_DUST_FLOOR, + now=time.time(), + ) + assert len(due) == 1 + assert due[0][0] == "node-wallet" + + +def test_prod_defaults_hold_sub_threshold_pending(): + "Tracker with prod 24 h / 5 USDT settings does not pay tiny balances early.\n\nTags: billing, settlement" + ledger = BillingLedger(starting_credit=100.0, default_price_per_1k=0.02) + ledger.charge_request("client", MODEL, 1000, [("node-wallet", 12)]) + treasury = _FakePayoutTreasury() + tracker = _make_tracker( + ledger, + treasury, + threshold=PROD_PAYOUT_THRESHOLD, + period=PROD_SETTLE_PERIOD, + dust=PROD_DUST_FLOOR, + ) + tracker.start() + try: + time.sleep(0.5) + assert treasury.batches == [] + assert ledger.get_node_pending("node-wallet") > 0 + finally: + tracker.stop() + + +def test_prod_threshold_triggers_immediate_payout(): + "Tracker with prod 24 h period still pays when pending ≥ 5 USDT.\n\nTags: billing, settlement" + ledger = BillingLedger(starting_credit=1000.0, default_price_per_1k=0.02) + ledger.charge_request("client", MODEL, 300_000, [("node-wallet", 12)]) + treasury = _FakePayoutTreasury() + tracker = _make_tracker( + ledger, + treasury, + threshold=PROD_PAYOUT_THRESHOLD, + period=PROD_SETTLE_PERIOD, + dust=PROD_DUST_FLOOR, + ) + tracker.start() + try: + assert _wait_for(lambda: treasury.batches) + assert ledger.get_node_pending("node-wallet") == pytest.approx(0.0) + assert treasury.batches[0][0][0] == "node-wallet" + assert treasury.batches[0][0][1] >= PROD_PAYOUT_THRESHOLD + finally: + tracker.stop()