Skip to content

feat: provider base rewards — economical earnings floor (max(earned, floor))#282

Open
Gajesh2007 wants to merge 5 commits into
masterfrom
feat/streamable-base-rewards
Open

feat: provider base rewards — economical earnings floor (max(earned, floor))#282
Gajesh2007 wants to merge 5 commits into
masterfrom
feat/streamable-base-rewards

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds a provider base-rewards earnings floor to stabilize supply during cold-start — the economical successor to the rejected additive stream-payments design.

The rule is a draw against earnings, not a bonus:

payout_i = earned_i + max(0, floor_i − k · earned_i)      // k = 1  ⇒  max(earned, floor)

The floor is reduced dollar-for-dollar by what a provider earns, so the subsidy targets idle machines and self-extinguishes to $0 as real demand grows. Total spend is hard-capped by a fixed monthly pool. It is the worst-case marketing promise ("a 64GB+ Mac covers your Netflix even when the network is quiet") without the runaway cost of paying base + usage.

Off by default behind EIGENINFERENCE_BASE_REWARDS — zero behavior change when unset.

Full design + economics: docs/base-rewards.md.

Before / After

Payout model — the old path paid per-token only (an idle machine earns ~$0 and churns); the new path tops a machine up to its floor and disappears once it out-earns it.

flowchart LR
  subgraph Before["Before — per-token only"]
    A1[Provider serves tokens] --> A2["earned = sum of per-token"]
    A2 --> A3["payout = earned"]
    A4[Quiet network] --> A5["earned approx 0 -> payout approx 0 -> churn"]
  end
Loading
flowchart LR
  subgraph After["After — earnings floor: max(earned, floor)"]
    B1["earned = organic per-token"] --> B3
    B2["floor = tier(verified mem) x avail x taper"] --> B3
    B3["draw = max(0, floor - k*earned)"] --> B4["payout = earned + draw"]
    B4 --> B5{"earned >= floor?"}
    B5 -- yes --> B6["draw = 0, keep 100%"]
    B5 -- no --> B7["topped up to the floor"]
  end
Loading

Settlement flow (new) — once per closed monthly epoch, restart-safe and idempotent, with the pool cap enforced under a cross-instance lock:

flowchart TD
  T["Hourly tick"] --> C{"epoch closed?"}
  C -- no --> Z["no-op"]
  C -- yes --> L["WithEpochSettlementLock (advisory lock)"]
  L --> CA["build candidates from live registry"]
  CA --> G{"gates: attested · uptime >= 90% · healthy · work-proven · linked account"}
  G -- fail --> X["excluded (earns 0)"]
  G -- pass --> UP["uptime from provider_sessions (interval union)"]
  UP --> AL["AllocateDraws: <= pool, workhorse-protected, minus already-settled"]
  AL --> S["SettleProviderFloorDraw — idempotent per (provider_key, epoch_id)"]
  S --> LG["ledger credit + provider_floor_draws audit row"]
Loading

Data model — new tables + the idempotency fix on the existing earnings table:

erDiagram
  provider_floor_draws {
    string provider_key
    string epoch_id "UNIQUE(provider_key, epoch_id)"
    int64  amount_micro_usd
    int64  floor_micro_usd
    int64  earned_micro_usd
    float  uptime_frac
    int    memory_gb
  }
  provider_probe_results {
    string provider_key
    bool   success
    int64  latency_ms
  }
  provider_earnings {
    string job_id "now UNIQUE (idempotent ON CONFLICT)"
    string provider_key
    int64  amount_micro_usd
  }
Loading

What's included

  • Phase 0 — bounded floor + restart-safe idempotent settlement: coordinator/payments/baserewards (floor table by verified-memory tier, avail ≥90%-uptime ramp, k-draw, pool water-fill protecting the 48–96GB workhorse tier, SettleEpoch from durable provider_sessions); store layer (provider_floor_draws, UNIQUE(job_id) + ON CONFLICT no-op that fixes a latent double-credit path, session-overlap uptime, cross-instance WithEpochSettlementLock); GET /v1/admin/base-rewards.
  • Phase 1 — verified capacity + correctness probe: HardwareModel→max-memory downward cap (mdm/mac_models.go; a self-reported tier can only be lowered, never raised); a coordinator correctness prober (encrypts like a consumer, records willingness-to-serve); work-gate = a billed other-account job OR a passed probe (self-route excluded).
  • Phase 2/3 — taper.go (absolute-revenue taper, calendar glide, 30% month-over-month cliff guard) implemented as pure, tested functions; settlement runs at taper = 1 pending activation. Fee-pool handoff is design-only (platform fee is 0% in alpha).
  • console-ui: provider-facing BaseRewardsPanel on /earn (honest 64GB+ anchor, never the word "guarantee") + an admin proxy route.
  • Convention: AGENTS.md + CLAUDE.md now require before/after mermaid diagrams in every PR (this PR dogfoods it).

Honest caveats (carried from the design)

  • This reduces to $0 per machine, but the per-machine crossover is well above alpha demand — budget it as a flat ~$8k/mo line for many months; the reduction guarantees it ends, not that it's cheap now.
  • "Pay for your Netflix" is honestly true only for 64GB+ machines (~top 18% of a realistic Apple fleet); the UI anchors to that class and shows sub-48GB as usage-only.
  • The Phase-1 probe verifies liveness + SE-signature, not byte-exact known-answer (a documented follow-up).

Testing

  • go build ./... ✅, Linux cross-compile ✅, go vet ./... ✅, gofmt clean ✅.
  • Unit tests: payments/baserewards (floor boundaries, k-draw, pool water-fill determinism, taper/cliff, settlement idempotency/restart-safety, memory-cap anti-overclaim, self-route-excluded, blue-green double-open, empty-fleet no-NaN), store/base_rewards_test.go (double-credit no-op, idempotent floor-draw, overlap union — memory always + postgres behind DATABASE_URL), mdm (model→memory cap), api suite green, console-ui BaseRewardsPanel vitest.
  • Quality gate: reviewed by the Codex rescue subagent + an independent Claude subagent. Codex caught 4 real Postgres-path issues (a partial-index/ON CONFLICT mismatch that would have errored every earning insert, a cross-run pool-cap gap, an empty-account settlement, one wrong memory-cap entry) — all fixed in this PR and re-verified.

Note: Postgres-backed store tests require DATABASE_URL (skipped in CI without it); the ON CONFLICT/advisory-lock SQL was reviewed by hand.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

…floor))

Replaces the rejected additive stream-payments design with a bounded,
self-extinguishing earnings FLOOR. Payout = earned + max(0, floor − k·earned)
(k=1 ⇒ max(earned, floor)): the floor is reduced dollar-for-dollar by what a
provider earns, so the subsidy targets idle machines and shrinks to $0 as real
demand grows. Total draw is hard-capped by a fixed monthly pool.

Off by default behind EIGENINFERENCE_BASE_REWARDS — zero behavior change unset.

Phase 0 — bounded floor + restart-safe idempotent settlement:
- coordinator/payments/baserewards: floor table by verified-memory tier, avail
  (≥90% uptime ramp), k-draw, pool water-fill protecting the 48–96GB workhorse
  tier, monthly-epoch SettleEpoch from durable provider_sessions.
- store: provider_floor_draws (UNIQUE(provider_key,epoch_id)); UNIQUE(job_id)
  on provider_earnings + ON CONFLICT no-op (fixes a latent double-credit path);
  session-overlap uptime query; cross-instance WithEpochSettlementLock so the
  pool cap holds across concurrent settlers.
- GET /v1/admin/base-rewards.

Phase 1 — verified capacity + correctness probe:
- mdm HardwareModel → max-memory downward cap (mac_models.go); a self-reported
  memory tier can only be lowered, never raised.
- coordinator correctness prober (encrypts like a consumer, records willingness);
  work-gate = billed other-account job OR passed probe. Self-route excluded.

Phase 2/3 — taper.go (absolute-revenue taper, calendar glide, 30% cliff guard)
implemented as pure, tested functions; settlement runs at taper=1 pending
activation.

console-ui: provider-facing BaseRewardsPanel on /earn (honest 64GB+ anchor, no
"guarantee"); admin proxy route.

docs/base-rewards.md: full design + economics. AGENTS.md/CLAUDE.md now require
before/after mermaid diagrams in every PR.
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Jun 7, 2026 3:42am
d-inference-console-ui-dev Ready Ready Preview Jun 7, 2026 3:42am
d-inference-landing Ready Ready Preview Jun 7, 2026 3:42am

Request Review

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

This PR adds a base-rewards settlement engine and correctness prober; no security-relevant changes are made to the files mapped to T-032 or T-038, and no existing mitigations are weakened.


Trust boundaries touched

  • TB-001 (Consumer → Coordinator API) — new HTTP handler(s) registered via api.NewProber and base-rewards endpoints in the 35 uncovered files
  • TB-008 (Coordinator → Payments) — new baserewards engine writes to the ledger/store on a settlement loop

Threat analysis

Threat Finding
T-032 (Solana mnemonic exfiltration) ℹ️ Neutral. The new code path reads from billingCfg.EncryptionMnemonic (already present) and from the BaseRewards config block. No new handling of the mnemonic is introduced; the coordinator X25519 key derivation block immediately below is unchanged.
T-038 (Missing ReadHeaderTimeout / MaxHeaderBytes) ℹ️ Neutral. The http.Server literal at line ~485 is not touched by this diff. SEC-027 remains open.

New attack surface not covered by an existing threat

  1. Base-rewards balance manipulation (maps closest to T-030 / TB-008). baserewards.NewEngine is wired directly to st (the store) and reg (the provider registry). The settlement loop (br.Run(ctx)) presumably credits provider balances periodically. The diff does not show the baserewards package internals, but the same atomicity risk documented for Stripe webhooks (SEC-012 / T-030) applies here: if settlement reads uptime fractions and writes credits in non-atomic steps, concurrent runs or a restart during settlement could double-credit providers. Verify that Run() uses a database transaction (or advisory lock) that makes each settlement epoch idempotent.

  2. Correctness prober as new unauthenticated surface (maps closest to T-006 / TB-002). api.NewProber(srv) is a new API component not described in the threat model. The prober runs as a background goroutine (saferun.Go) but if it opens any inbound HTTP routes, those need to be reviewed for authentication and rate limiting. The diff does not show prober.go's route registrations, which are listed among the 35 uncovered files. Ensure Prober endpoints require admin or internal authentication and are not reachable from the public API surface.

  3. Config fields logged at startup. main.go lines ~300–305 log reduction_k, pool_micro_usd, and min_uptime at Info level. These are operational values, not secrets, so this is low risk — but confirm brc.FloorPoolB is not seeded from the same env as the Solana mnemonic or any other secret.


Open findings resolved by this PR

None. SEC-027 (T-038) and SEC-012 (T-030) remain open and are not addressed here.


🔐 Threat model: docs/threat-model.yaml · Updates on each push to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03398bc9a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coordinator/api/prober.go Outdated
Comment on lines +125 to +126
SelfRouteOnly: true, // pin + never fall back to a paid provider
FreeSelfRoute: true, // settles free — no earning row, no payout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow probes to reserve the pinned provider

With SelfRouteOnly set here and no OwnerAccountID, every candidate is filtered out before the serial pin is considered: providerOwnedBy explicitly returns false for an empty account id, so ReserveProviderEx returns nil for all probes. In the zero-demand case this means the new probe work-gate can never record a success, so otherwise eligible providers cannot qualify for base rewards unless they already have organic billed jobs. Use the serial restriction without self-route ownership filtering, or stamp the provider's real owner account.

Useful? React with 👍 / 👎.

Comment on lines +167 to +171
remainingBudget := e.cfg.PoolBudgetMicroUSD - settledSum
if remainingBudget < 0 {
remainingBudget = 0
}
allocs := AllocateDraws(pureCands, remainingBudget, e.cfg.WorkhorseReserveFrac, e.cfg.PerAccountCapFrac)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce per-account caps across re-settlement runs

This allocation only subtracts the prior epoch draw total; it does not seed the allocator with amounts already paid per account. If an epoch is settled once and later a new machine from the same account becomes eligible, AllocateDraws starts that account at zero and grants up to a fresh per-account cap (computed from the remaining budget), letting that account exceed the intended 5% monthly concentration cap across idempotent re-runs. Existing provider_floor_draws need to count toward accountGranted or the candidate budget before allocating new rows.

Useful? React with 👍 / 👎.

Comment on lines +48 to +49
ModelLoaded: p.CurrentModel != "" || len(p.WarmModels) > 0,
CurrentModel: probeModel(p),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use backend slots for base-reward warm checks

For Swift providers BackendCapacity.Slots is the authoritative loaded-model state, but this snapshot treats any non-empty CurrentModel or WarmModels as loaded and also picks the probe target from those fallback fields. When a provider reports a slot as crashed, reloading, or idle_shutdown (or has no slot for the model) while those legacy fields remain populated, buildCandidates can mark it eligible and the prober can target it even though the scheduler would not consider that model warm/serving. Derive ModelLoaded and the probe model from backend slots (running/idle) whenever BackendCapacity is present.

Useful? React with 👍 / 👎.

Comment thread coordinator/api/prober.go
Comment on lines +145 to +148
defer func() {
provider.RemovePending(requestID)
s.registry.SetProviderIdle(provider.ID)
}()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel timed-out probe requests on the provider

This deferred cleanup removes the pending request and marks the provider idle, but it never sends a cancel frame. When awaitCompletion returns on probeTimeout or context cancellation after the request was written, the provider can keep generating the probe while the coordinator has already made it schedulable for more work, wasting capacity and potentially overlapping a new request with the abandoned probe. Use the same cancel cleanup as other timed-out dispatches.

Useful? React with 👍 / 👎.

Comment on lines +89 to +91
INSERT INTO ledger_entries (account_id, entry_type, amount_micro_usd, balance_after, reference, created_at)
SELECT d.account_id, $9, d.amount_micro_usd, c.balance_micro_usd, $3, NOW()
FROM draw d CROSS JOIN credit c WHERE d.amount_micro_usd > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record floor draws in provider earnings summaries

Settling a positive floor draw only creates the balance/ledger credit here; it never inserts a provider_earnings row or updates earnings_summary, which is what /v1/provider/account-earnings returns for the provider dashboard totals and recent history. After monthly settlement, a provider's withdrawable balance increases but their displayed lifetime/recent earnings stay unchanged, making the new floor payout effectively invisible in the earnings UI except as an unexplained balance jump.

Useful? React with 👍 / 👎.

Comment thread coordinator/mdm/mac_models.go Outdated
"Mac16,13": 32, // MacBook Air 15" (M4, 2025)

// --- Mac Studio (2025): M4 Max + M3 Ultra ---
"Mac16,9": 512, // Mac Studio (M4 Max 128GB / M3 Ultra 512GB, 2025) — Ultra → 512

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disambiguate Mac16,9 before granting the 512GB tier

This identifier is documented in the comment as covering both 128GB M4 Max and 512GB M3 Ultra Mac Studio machines, but the cap is set to 512GB for all of them. Because the engine only clamps self-reported memory downward and the current probe does not verify a tier-sized model, a 128GB M4 Max Mac Studio can self-report 512GB and receive the top $40 floor instead of its real tier. Use a more specific hardware signal or avoid the 512GB cap for ambiguous identifiers until tier verification exists.

Useful? React with 👍 / 👎.

- P1 prober: drop SelfRouteOnly on probe requests. With no OwnerAccountID it
  made ReserveProviderEx filter out every provider (providerOwnedBy=false), so
  the probe work-gate could never record a success. Pin via the serial allowlist
  + FreeSelfRoute (settles free) instead — the Phase-1 probe path now works.
- P2 per-account cap across re-runs: AllocateDraws now takes the full pool as the
  cap basis and a priorByAccount seed, so an account's 5% cap holds cumulatively
  across idempotent re-settlement runs (no fresh full cap for a new same-account
  machine). Engine seeds it from already-settled draws.
- P2 warm check: derive ModelLoaded + the probe target from authoritative
  BackendCapacity slots (running/idle, matching the scheduler) when present, not
  from legacy CurrentModel/WarmModels — a crashed/reloading slot no longer marks
  a provider eligible or probe-targetable.
- P2 probe cancel: send a cancel frame to the provider on probe timeout/error so
  it stops generating instead of overlapping later real work.
- P2 earnings visibility: floor settlement now records a base_reward earning row
  (+ summary) so the draw shows in the provider's earnings history/summary
  instead of an unexplained balance jump — excluded from organic gating by Model.
- P2 Mac16,9: cap conservatively at 128GB (ambiguous M4 Max 128 / M3 Ultra 512)
  so a 128GB Studio cannot self-report 512GB for the top floor before the
  tier-sized probe exists.

Regression tests added: cross-run per-account cap, floor-draw earnings
visibility (excluded from organic), memory-cap overclaim already covered.
golangci-lint v2.1.6 clean; coordinator + console-ui suites green.
An operator running N real, attested, serving Macs contributes N machines of
capacity and should earn N floors. The per-account concentration cap penalized
exactly those honest multi-machine operators (the supply we want), added nothing
to cost control (the pool is already bounded), and was dodgeable by splitting
machines across free accounts. Attestation is the real Sybil defense.

Default PerAccountCapFrac to 0 (per-machine). Kept as an optional knob
(EIGENINFERENCE_BASE_REWARDS_ACCOUNT_CAP, default 0); when set it still binds on
the Stripe payout identity and enforces cumulatively across re-settlement runs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7f828bf30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +288 to +289
if capGB, known := mdm.ModelMaxMemoryGB(p.HardwareModel); known && capGB > 0 && memGB > capGB {
memGB = capGB

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cap unknown hardware models before paying top tiers

When ModelMaxMemoryGB returns known == false (for example if hw.model reports Unknown or a new/missing identifier), this leaves memGB entirely self-reported, while the current probe only asks for an 8-token completion and does not verify a tier-sized model. In that case a small attested Mac with an uncatalogued or failed hardware-model string can report MemoryGB=512 and receive the 512GB floor instead of being conservatively capped until real tier verification exists.

Useful? React with 👍 / 👎.

Comment thread coordinator/api/prober.go
Comment on lines +202 to +204
// Success = a valid, SE-signed, non-empty completion arrived in time.
// (Byte-exact known-answer matching is a documented follow-up.)
success := awaitErr == nil && seSig != "" && len(text) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify probe signatures before marking success

For the base-reward work gate, any non-empty se_signature makes the probe successful; handleComplete only copies msg.SESignature/ResponseHash onto the pending request and does not verify the signature. An attested but modified provider can therefore satisfy the probe half of eligibility with a bogus signature and non-empty text, even though the comment and reward gate require a valid SE-signed completion.

Useful? React with 👍 / 👎.

Comment thread coordinator/api/server.go

// Metrics snapshot (admin only)
s.mux.HandleFunc("GET /v1/admin/metrics", s.handleAdminMetrics)
s.mux.HandleFunc("GET /v1/admin/base-rewards", s.handleAdminBaseRewards)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Authenticate the Privy-backed admin status route

The new console proxy forwards a Privy bearer token to /v1/admin/base-rewards, but this coordinator route is registered without requireAuth, and the global handler stack does not populate auth.UserFromContext. As a result isAdminAuthorized can only accept the raw admin key here, so Privy admins using the UI endpoint get 403 instead of the base-rewards status.

Useful? React with 👍 / 👎.

Comment thread coordinator/api/prober.go
Comment on lines +123 to +130
ConsumerKey: "base-rewards-probe",
AllowedProviderSerials: []string{snap.SerialNumber}, // pin to this machine
// FreeSelfRoute makes the dispatch settle free (no earning row, no payout).
// We deliberately do NOT set SelfRouteOnly: that flag means "owned-only"
// and, with no OwnerAccountID, ReserveProviderEx would filter out every
// provider (providerOwnedBy == false), so the probe could never reserve
// the pinned machine. The serial allowlist alone does the pinning.
FreeSelfRoute: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep probes out of paid usage accounting

Setting FreeSelfRoute here does not actually make the probe a free internal request because handleComplete only sets its freeSelfRoute flag when the serving provider is owned by ConsumerKey; this synthetic key owns no provider. Successful probes therefore enter the paid path, fail a debit, and still persist a zero-cost RecordUsageFull row because freeSelfRoute remains false, inflating public usage/request stats with synthetic probe traffic.

Useful? React with 👍 / 👎.

"Mac14,15": 24, // MacBook Air 15" (M2, 2023)
"Mac14,7": 24, // MacBook Pro 13" (M2, 2022)
"Mac14,5": 96, // MacBook Pro 14" (M2 Max, 2023)
"Mac14,9": 96, // MacBook Pro 14" (M2 Pro/Max, 2023) — Max → 96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cap shared Pro/Max identifiers conservatively

For model identifiers this table labels as shared Pro/Max, using the Max memory ceiling lets the lower-memory Pro variant self-report the Max tier because the current probe does not verify a tier-sized model. For example, a Mac14,9 M2 Pro machine can claim 96GB and receive the 96GB floor even if its physical ceiling is 32GB; apply the same conservative treatment used for Mac16,9 until a stronger hardware signal or tier probe exists.

Useful? React with 👍 / 👎.

warm := warmServingModel(p)
out = append(out, ProviderSnapshot{
ID: p.ID,
ProviderKey: p.PublicKey,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a stable machine key for reward identity

This makes the base-rewards provider identity the X25519 encryption public key, but the Swift provider creates that key with NodeKeyPair.generate() at ProviderLoop startup. Because sessions, earnings, probe successes, and floor draws are all matched by provider_key, a normal provider restart or update splits one machine into multiple identities; the current live key may have only a fraction of the month's uptime/work and can miss the 90% gate despite the same serial being online all month. Key rewards on a stable attested machine identity, or persist the X25519 key across restarts.

Useful? React with 👍 / 👎.

Comment on lines +176 to +180
remainingBudget := e.cfg.PoolBudgetMicroUSD - settledSum
if remainingBudget < 0 {
remainingBudget = 0
}
allocs := AllocateDraws(pureCands, remainingBudget, e.cfg.PoolBudgetMicroUSD, e.cfg.WorkhorseReserveFrac, e.cfg.PerAccountCapFrac, priorByAccount)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the workhorse reserve across re-settlement

If an epoch is first settled before some 48–96GB machines become eligible (for example, their probe lands after the first run), already-settled non-workhorse draws are frozen and this rerun reserves only a fraction of the remaining budget. A first run containing only large machines can therefore consume the whole pool, leaving later eligible workhorses with $0 even though a one-shot allocation with the same final candidate set would have protected the workhorse tier.

Useful? React with 👍 / 👎.

…range Macs

The fleet skews ~27GB average, and 24/32GB Macs can serve the gpt-oss-20B
baseline plus specialist work (STT, embeddings) — but the floor started at 48GB,
so the bulk of useful supply earned nothing. Add 24GB ($10/mo) and 32GB ($12/mo)
tiers; threshold drops from 48GB to 24GB (sub-24GB still usage-only). They earn
only while actually serving (the work gate), so the floor never rewards an idle
small machine. Updated floor table, console-ui panel, docs, and tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 531e5e4a2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +41 to +46
`SELECT EXISTS(
SELECT 1 FROM provider_earnings
WHERE provider_key = $1
AND created_at >= $2
AND amount_micro_usd > 0
AND model NOT IN ('base_reward', 'probe'))`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject self-dealt jobs from the work gate

When a provider creates a paid request from another account and pins its own machine via provider_serial, the consumer path accepts that routing hint and the scheduler restricts dispatch to that serial; the resulting provider_earnings row has no consumer-account provenance, so this predicate treats any positive non-base_reward/probe payout as organic work. That lets a provider buy one tiny request to itself and satisfy the billed-job half of base-reward eligibility without real external demand; record/check the consumer account or otherwise exclude pinned/self-dealt jobs here before clearing the gate.

Useful? React with 👍 / 👎.


workSince := e.now().Add(-e.cfg.WorkWindow)
out := make([]candidate, 0)
for _, p := range e.reg.ListProviders() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Settle closed epochs from durable provider state

Because candidates are built only from e.reg.ListProviders(), settlement for a closed month depends on who is connected at the moment this hourly job runs, not on the durable session/earning rows for that epoch. A machine that stayed online and served work for ≥90% of April but is offline or restarting during the May 1 settlement tick is skipped entirely until it reconnects, and may receive nothing if the pool is allocated before then; use the persisted provider/session state for the epoch instead of the live registry as the candidate source.

Useful? React with 👍 / 👎.

Comment on lines +407 to +410
cur, ok := best[ps.ProviderKey]
if !ok || ps.ConnectedAt.After(cur.when) {
best[ps.ProviderKey] = pick{account: ps.AccountID, when: ps.ConnectedAt}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prorate floor payouts when ownership changes

This picks the most recent session's account and then credits the entire epoch's floor for the provider key to that account. If a machine is linked to account A for most of the month and then relinked or sold to account B shortly before settlement, B receives the whole monthly top-up while A's uptime/earnings can still be part of the same provider-key calculation; split candidates by session account/time or otherwise attribute the floor to the accounts that actually owned the machine during the epoch.

Useful? React with 👍 / 👎.

Comment on lines +241 to +243
// Gate 4: healthy + advertised model loaded for routing.
if !p.Online || !p.ModelLoaded {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the scheduler's runtime and challenge gates

For currently connected providers this treats Online plus ModelLoaded as loaded for routing, but ReserveProviderEx/snapshotProviderLocked also requires runtime verification, private-text support, and a fresh LastChallengeVerified before public inference can route. If a provider served a billed job earlier and then fails runtime verification or lets challenge freshness lapse while staying online with an idle slot, it still passes settlement here and can receive the floor despite being unroutable to users; mirror the scheduler's structural gates before adding the candidate.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant