2 Commits

Author SHA1 Message Date
Dobromir Popov
a6bcc69288 sol mainnet payouts tasks 2026-07-13 18:51:40 +02:00
Dobromir Popov
c938d38031 more docs review 2026-07-13 18:37:07 +02:00
9 changed files with 547 additions and 11 deletions

View File

@@ -26,7 +26,7 @@ Active workstream (started 2026-07-04): alpha hardening of the money/trust path.
Both are already migrated into `.scratch/alpha-hardening/prd.json` (AH-021 updated, AH-023 added) and the README index — ready for Ralph to pick up unattended.
**Ralph note:** `scripts/ralph_progress.py` tracks `docs/prd.json` (35/35 done) and does NOT see `.scratch/alpha-hardening/issues/`. No ralph loop is running and no `.ralph-tui/` state exists. `.scratch/alpha-hardening/prd.json` now has 23 stories (AH-001…AH-023); point Ralph at that file for the alpha-hardening branch. Do NOT use `ralph auto --parallel` on server.py-touching issues — 21 and 23 both touch `server.py`/`billing.py`/`audit.py`; if run in the same Ralph pass, run them serially, not in parallel (merge-conflict risk, same lesson as 03/04 previously).
**Ralph note:** `scripts/ralph_progress.py` tracks `docs/prd.json` (US-001…US-047; base 35/35 done, friends-test arc 3647 open/in-progress). Alpha hardening uses `.scratch/alpha-hardening/prd.json` (AH-001…AH-023). Point Ralph at the prd.json for the branch you're running.
**Why:** three audits agreed the alpha blockers are unauthenticated gossip (anyone can inject billing events), the free-credit faucet, and ephemeral bans.
**How to apply:** work test-first per issue acceptance criteria; use `.venv`; `cryptography` belongs in node deps (wallet.py imports it — causes many of the 24 "failures" in a fresh env). See [[project-status]] and [[autonomous-work-style]].

View File

@@ -8,7 +8,7 @@ metadata:
# Project Status (2026-07-13)
> Doc reconciliation 2026-07-13: `docs/PRD.md`, ADR numbering (0024 = distributed GGUF), and `docs/issues/` statuses updated to match `docs/prd.json` (US-001…US-035 done). Post-035 work remains in issues 3648 and `.scratch/`.
> Doc reconciliation 2026-07-13: `docs/PRD.md`, ADR numbering (0024 = distributed GGUF), and `docs/issues/` statuses updated to match `docs/prd.json` (US-001…US-035 done). **US-036…US-047** added to `docs/prd.json` for the friends-test / LAN-serving arc. Post-035 scratch work remains in `.scratch/`.
All 35 user stories in docs/prd.json are done (35/35), including the reward-system arc US-030…US-035 completed 2026-07-02:

View File

@@ -1,4 +1,4 @@
Status: done (base program US-001…US-035 complete; see `docs/prd.json`. Post-035 work lives in `docs/issues/36+` and `.scratch/`. Payment/settlement superseded by ADR-0015; fraud by ADR-0018.)
Status: done (base program US-001…US-035 complete; friends-test arc US-036…US-047 in `docs/prd.json`. Payment/settlement superseded by ADR-0015; fraud by ADR-0018.)
# Distributed Inference Network — PRD
@@ -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.

View File

@@ -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.

View File

@@ -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.10.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)

View File

@@ -1,6 +1,6 @@
{
"name": "Distributed Inference Network",
"description": "Build a distributed inference network with node, gateway, tracker, SDK, contracts, and P2P shard distribution components from the grill session PRD.",
"description": "Distributed inference network: base program US-001…US-035 complete; post-alpha friends-test and LAN-serving arc US-036…US-047 tracked here. Active scratch features (alpha hardening, NCA, distributed GGUF) have separate prd.json files under .scratch/.",
"branchName": "ralph/distributed-inference-network",
"userStories": [
{
@@ -814,10 +814,268 @@
"US-033"
],
"completionNotes": "GET /dashboard served from embedded dashboard.html (package-data, no build step) by any tracker. Panels: hive/leader (raft status), nodes+coverage grouped by model, client balances, node pending + protocol cut, settlement history with devnet explorer links, strikes/bans/forfeitures (GET /v1/registry/wallets + snapshot forfeits), RPM stats. 4s auto-refresh via fetch polling. 3 tests in tests/test_dashboard.py."
},
{
"id": "US-036",
"title": "36 — Streamed chat completions over the relay RPC path",
"description": "Public NAT deployments proxy every chat request tracker → relay → head node. Implement true multi-frame SSE streaming over the relay WebSocket so clients see live tokens and relayed streams bill through the same SSE accounting loop as direct proxy streams. Inter-node /forward activation hops stay single-frame (ADR-0014).",
"acceptanceCriteria": [
"stream: true chat via relay delivers SSE chunks incrementally (≥2 distinct frame arrivals before [DONE])",
"Relayed streamed request records nonzero billed tokens and node work credit",
"Non-streamed relayed requests and /forward binary hops behave exactly as before (single frame, body_base64 intact)",
"Legacy single-frame response from an old node is accepted as terminal",
"Idle stream (no frame for 120 s) returns 504 and cleans up the relay-side queue",
"Extend tests/test_gossip_and_relay.py alongside test_relay_rpc_round_trips_http_request_to_peer",
"python -m pytest passes from repo root"
],
"priority": 36,
"status": "needs-review",
"notes": "Source issue: docs/issues/36-relay-streamed-chat.md. Implemented via _stream_relayed_frames in server.py; verify on public NAT relay before friends-test.",
"dependsOn": [
"US-029",
"US-031"
],
"completionNotes": "Multi-frame relay-http-response protocol; node relay_bridge line-by-line SSE emit; relay server per-request asyncio.Queue; tracker _stream_relayed_frames with SSE billing parity. Client mid-stream disconnect accepted limitation for alpha."
},
{
"id": "US-037",
"title": "37 — Concurrent request handling in the node relay bridge",
"description": "RelayHttpBridge currently handles relay-http-request envelopes serially, blocking up to 300 s per request. Off-LAN a node can be head of one route and downstream hop of another — overlapping routes through a shared node break. Dispatch on a bounded ThreadPoolExecutor (default 8, configurable) with per-frame WS send locking compatible with US-036 streaming.",
"acceptanceCriteria": [
"While one relayed request is in flight, a second relay-http-request to the same node completes without waiting for the first",
"Responses are correctly matched by request_id when they complete out of order",
"More than N simultaneous requests queue and all eventually complete; thread count never exceeds N workers",
"Bridge survives a relay reconnect with workers still in flight (no crash, no deadlock; orphaned responses dropped)",
"Configurable via meshnet-node start --relay-concurrency N (env MESHNET_RELAY_CONCURRENCY)",
"Extend tests/test_gossip_and_relay.py",
"python -m pytest passes from repo root"
],
"priority": 37,
"status": "open",
"notes": "Source issue: docs/issues/37-relay-bridge-concurrency.md. Critical for public friends-test; blocks concurrent head + hop on same node.",
"dependsOn": [
"US-036"
]
},
{
"id": "US-038",
"title": "38 — Tracker cluster join via a single seed peer",
"description": "Tracker cluster membership is static today — a newcomer configured with only one existing peer is never learned by the rest of the hive and quorum math diverges. A joining tracker configured with any one live seed announces via hive-HMAC-signed POST /v1/cluster/join; membership changes replicate through the Raft log and persist across restarts.",
"acceptanceCriteria": [
"Start trackers A+B; start C with only A as seed → within one election timeout A, B, and C report the same 3-peer membership on GET /v1/cluster/peers, and a value proposed on C commits on A and B",
"Join without a valid hive signature is rejected with 403; join to a follower is forwarded to the leader",
"Restarting C with its seed offline rejoins from persisted membership",
"Standalone tracker (no seeds) behaves exactly as today",
"python -m pytest passes from repo root"
],
"priority": 38,
"status": "open",
"notes": "Source issue: docs/issues/38-tracker-seed-join.md. Out of scope: peer removal, joint consensus, automatic seed retry.",
"dependsOn": [
"US-013",
"US-017"
]
},
{
"id": "US-039",
"title": "39 — Caller Credit granted once per account; chat requires account keys",
"description": "DEFAULT_STARTING_CREDIT=0 and no grant path leaves every fresh public tracker request at 402. Grant Caller Credit once per account on first API key creation via deterministic event id caller-credit-{account_id}; chat on accounts-enabled trackers requires a real active sk-mesh- key (401 for invented bearers).",
"acceptanceCriteria": [
"Fresh account → first key → key has --starting-credit balance; chat succeeds",
"Second key on the same account → no additional credit",
"Revoke-and-recreate keys → still no additional credit (deterministic event id)",
"Random bearer string on an accounts-enabled tracker → 401, never 402/free work",
"Tracker without accounts store: gate behavior unchanged",
"--starting-credit 0 disables the grant entirely (mainnet posture)",
"python -m pytest passes from repo root"
],
"priority": 39,
"status": "open",
"notes": "Source issue: docs/issues/39-caller-credit-account-keys.md. Critical for friends-test inference.",
"dependsOn": [
"US-031",
"US-035"
]
},
{
"id": "US-040",
"title": "40 — Devnet top-up button on the dashboard",
"description": "After Caller Credit (US-039) is spent, devnet friends need a dashboard faucet refill without on-chain USDT deposits. POST /v1/account/topup (session-authenticated) credits a configured fixed amount per click; flag off returns 404 and hides the button.",
"acceptanceCriteria": [
"Flag off: endpoint 404s, dashboard shows no top-up button",
"Flag on: logged-in user tops up own key, balance rises by exactly N",
"Topping up another account's key → 403",
"python -m pytest passes from repo root"
],
"priority": 40,
"status": "open",
"notes": "Source issue: docs/issues/40-devnet-dashboard-topup.md. Mainnet deployments set --devnet-topup 0.",
"dependsOn": [
"US-039"
]
},
{
"id": "US-041",
"title": "41 — Account wallet: browser-extension signing, in-browser generation, export-only",
"description": "Accounts need a visible wallet for deposit attribution without the tracker ever holding private keys. Dashboard integrates Solana wallet-adapter connect+nonce proof, or in-browser keypair generation with one-time export; no private-key import endpoint.",
"acceptanceCriteria": [
"Connect-extension flow stores a verified pubkey (rejects unsigned/mismatched nonce proofs)",
"Generate flow: pubkey lands on the account; private key is never sent to the tracker, export works",
"No endpoint or UI accepts a private key",
"Deposits to the shown address credit the account's keys via the existing watcher",
"Address visible on the account panel after either flow",
"python -m pytest passes from repo root"
],
"priority": 41,
"status": "open",
"notes": "Source issue: docs/issues/41-account-wallet-keypair.md. Not needed for devnet friends test; needed before mainnet.",
"dependsOn": [
"US-032",
"US-039"
]
},
{
"id": "US-042",
"title": "42 — GGUF/llama.cpp node backend (phase C whole-model first)",
"description": "Node backend is transformers-only today; large MoE models on consumer hardware require GGUF via llama.cpp. Phase C: whole-model GGUF nodes (single-hop routes) first; partial-layer distributed GGUF deferred to ADR-0024. Also: GGUF catalog entries, Strix Halo/Vulkan hardware detection, download dir applies to GGUF files.",
"acceptanceCriteria": [
"A node with --gguf <repo-or-path> --quant IQ3_XXS serves /v1/chat/completions via llama.cpp with GPU offload where available",
"Tracker treats it as a full-coverage node (single-hop routes, billing works)",
"Streamed responses work through the tracker proxy and the relay (US-036)",
"python -m pytest passes from repo root (llama.cpp behind an optional extra)"
],
"priority": 42,
"status": "in-design",
"notes": "Source issue: docs/issues/42-gguf-llamacpp-node-backend.md. Distributed native path: ADR-0024. Sequencing: phase C before A/B investigation.",
"dependsOn": [
"US-036"
]
},
{
"id": "US-043",
"title": "43 — Dashboard model search and model cards",
"description": "Dashboard lacks model-centric discovery. Add server-side HF search proxy merged with tracker presets and live coverage; model cards show architecture, coverage gaps, pricing, memory per quant, and a request-this-model action. Featured section driven by CURATED_MODELS including GGUF once US-042 lands.",
"acceptanceCriteria": [
"Searching a HF repo id or free text returns results without the browser calling HF directly",
"A served model's card shows live coverage and a working chat-now state",
"An unserved model's card shows the request action and estimated memory per quant",
"python -m pytest passes from repo root"
],
"priority": 43,
"status": "open",
"notes": "Source issue: docs/issues/43-dashboard-model-search-cards.md. Post-deploy polish.",
"dependsOn": [
"US-035"
]
},
{
"id": "US-044",
"title": "44 — Tracker as model-file source; nodes download only their shard",
"description": "Second nodes joining a fleet today download entire HF snapshots even for small shard assignments. Tracker --models-dir advertises layer-scoped safetensors subsets; nodes race tracker/peer sources before HF allow_patterns fallback. Hard half remaining: meta-device partial model materialization so resident memory scales with shard size, not full model size.",
"acceptanceCriteria": [
"Tracker started with --models-dir / MESHNET_MODELS_DIR advertises local model-file sources in assignment responses",
"Tracker serves a tar stream (or per-file API) containing only safetensors files for the assigned layer range plus config/tokenizer/index metadata",
"Node downloader tries exact-shard peers, then tracker/peer file subsets, then HF snapshot_download with allow_patterns — never silently full-repo when layer index is available",
"Two-machine test: machine B receives only its assigned range from machine A — nothing fetched from HF",
"Machine B resident memory scales with its shard size, not model size",
"Checksums verified end-to-end; corrupted transfer falls back cleanly",
"Single-node/full-model flows unchanged",
"python -m pytest passes from repo root"
],
"priority": 44,
"status": "in-progress",
"notes": "Source issue: docs/issues/44-tracker-shard-source-partial-download.md. Download path largely implemented 2026-07-06; partial LOAD (meta-device materialization) and two-machine acceptance remain.",
"dependsOn": [
"US-004",
"US-012"
],
"completionNotes": "Tracker models-dir indexing, layer-scoped tar stream, HF allow_patterns client-side from remote index, per-file download API with retries, symlink dereference in tar writers. Remaining: true partial model load and live two-machine verification."
},
{
"id": "US-045",
"title": "45 — Dual-rate billing: separate input and output token prices",
"description": "Ledger has one price_per_1k_tokens and stream vs non-stream paths disagree on input vs output counting. Charge both input and output tokens at separate rates per model; HF pricing refresher applies 80% of each marketplace side separately.",
"acceptanceCriteria": [
"Streamed and non-streamed requests for the same exchange bill the same split (input charged in both)",
"A model with asymmetric provider rates bills input and output differently; usage_for / billing events expose the split",
"Old persisted billing events replay byte-identically (balances unchanged)",
"HF refresh sets both rates from the marketplace row, not the average",
"Spend cap (--max-charge-per-request) uses the dual rates",
"python -m pytest passes from repo root"
],
"priority": 45,
"status": "in-progress",
"notes": "Source issue: docs/issues/45-dual-rate-billing.md. Billing correctness before friends test.",
"dependsOn": [
"US-031"
]
},
{
"id": "US-046",
"title": "46 — Tracker .env awareness + first-node auto-join bootstrap",
"description": "Fresh trackers return 503 on auto-join because deployability ignores the joining caller's hardware, and meshnet-tracker ignores .env MESHNET_DOWNLOAD_DIR. Fix empty-registry bootstrap, tracker env loading parity with node CLI, models-dir fallback chain, and tar dereference for HF symlink snapshots.",
"acceptanceCriteria": [
"Fresh tracker (empty registry) + caller with enough memory for a recommended preset → /v1/network/assign returns 200 with model_sources populated when tracker holds a local snapshot",
"Fresh tracker + caller too small for any recommended preset → still 503",
"meshnet-tracker start in a directory with .env setting MESHNET_DOWNLOAD_DIR serves /v1/model-files/download from that dir with no extra flags",
"Explicit --models-dir and MESHNET_MODELS_DIR still take precedence",
"python -m pytest passes from repo root"
],
"priority": 46,
"status": "needs-review",
"notes": "Source issue: docs/issues/46-tracker-env-and-first-node-autojoin.md. Verified live 2026-07-06.",
"dependsOn": [
"US-044"
],
"completionNotes": "Empty-registry synthesizes caller as candidate node; tracker loads .env; models_dir falls back MESHNET_MODELS_DIR → MESHNET_DOWNLOAD_DIR; tar dereference=True. Pytest passes aside from known port-7000 env conflict."
},
{
"id": "US-047",
"title": "47 — Tracker-first model downloads: visibility, sane timeouts, RAM-based sizing",
"description": "Explicit --model startup should skip pointless auto-join; tracker/peer sources preferred over HF with visible progress and 30 s socket timeouts; client abort during tar stream logs one line; CPU nodes size shards from RAM not phantom GPU VRAM; per-file downloads for robustness over fragile multi-GB tar streams.",
"acceptanceCriteria": [
"Node started with explicit --model never queries /v1/network/assign and never prints auto-join unavailable",
"Tracker/peer model source preferred; HF only when no source, all sources fail, or --tracker-source-disabled",
"Tracker-source downloads print progress every 512 MB and print exception + URL on failure",
"A ≥2 s read stall no longer aborts a tracker model-source download (30 s socket timeout)",
"Client disconnect during /v1/model-files/download logs one line on the tracker, no traceback",
"CPU node with big RAM gets a RAM-sized shard: sizing paths ignore VRAM unless device=cuda",
"Live two-machine retest: Windows node downloads from tracker at LAN speed with RAM-sized shard assignment",
"python -m pytest passes from repo root"
],
"priority": 47,
"status": "in-progress",
"notes": "Source issue: docs/issues/47-model-source-download-visibility.md. Engineering largely complete 2026-07-06; live two-machine retest pending.",
"dependsOn": [
"US-044",
"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-01T00:00:00.000Z",
"updatedAt": "2026-07-13T16:40:00.000Z",
"statusVocabulary": {
"open": "Not started",
"in-design": "Decisions pending before implementation can begin",

View File

@@ -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)

View File

@@ -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(

View File

@@ -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()