feat(earnings): count earnings from join date (verified_at floor) (#978)#984
Conversation
Only inflows with block_time >= the recipient agent's registration (agents.verified_at) count as earnings. "Earnings" = earned as an agent, not lifetime personal on-chain history. - Closes the agent_peer scoping gap: a pre-platform transfer between two addresses that only LATER registered would otherwise be counted as earnings (and is trivially gameable). inbox/bounty were already post-join by construction (they match platform DB records). - Bounds backfill: the sweep stops once it pages past registration instead of walking to genesis — cheaper Hiro usage. - fetchAgentPage now returns verified_at parsed to unix seconds (0 = no floor / all-time fallback when missing or unparseable). - indexAgent applies the floor in collect() and terminates backfill on the first pre-join page. Flippable to all-time later via a re-backfill (reset earnings_index_state); free to change now while the indexer is dormant.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
landing-page | d848b05 | Commit Preview URL Branch Preview URL |
Jun 08 2026, 08:53 AM |
arc0btc
left a comment
There was a problem hiding this comment.
Scopes earnings to post-registration inflows — closes a real semantic gap where pre-platform agent_peer transfers between later-registered addresses would have inflated earnings. Clean implementation.
What works well:
- The
verifiedAtSec: 0fallback correctly degrades to all-time for agents withoutverified_at— no floor applied, no data loss. reachedPreJointerminates backfill early rather than walking to genesis — correct and cheaper Hiro usage.- Field alignment verified:
reachedPreJoinreadsr.tx?.burn_block_timeandextractInboundTransfersmaps the sametx.burn_block_timetot.blockTime— the two checks are consistent. - Tests cover ISO parse, null, and unparseable cases. Good edge-case discipline.
- Clean separation from #983 (Phase 3 API PR) — different files, no entanglement.
[nit] transfersFound semantic shift (lib/earnings/indexer.ts)
The original code did transfersFound += transfers.length (all extracted transfers). The new code only increments for accepted transfers, so transfersFound now means "earnings-eligible transfers counted" rather than "transfers found on-chain". If this counter feeds logs or metrics, the label might become misleading. Harmless for correctness — worth a rename (earningsRows or transfersCounted) if it surfaces in dashboards.
[nit] Epoch-0 corner case (lib/earnings/d1.ts)
Math.floor(Date.parse(r.verified_at) / 1000) || 0 — if verified_at is exactly "1970-01-01T00:00:00Z", Date.parse returns 0, and 0 || 0 = 0, treating a valid epoch-0 registration as "no floor". Irrelevant in practice, but clarifying with a comment or an explicit Number.isFinite(s) && s > 0 guard would make the intent unambiguous. Current behavior is fine.
Code quality notes:
- Multi-line parameter comment on
verifiedAtSecinindexAgentis verbose but the 0-means-all-time invariant is non-obvious enough to justify it. reachedPreJoinas a named inner closure reads well — cleaner than inlining.
Operational note: We track Hiro API row-read costs and have seen unbounded backfill walks add up quickly (CF DO free tier is 5M rows/day). The early-termination via reachedPreJoin will meaningfully reduce costs once the indexer is enabled — good call.
Scopes earnings to money earned as an agent, not lifetime personal on-chain history. Only inflows with
block_time >= agents.verified_at(registration) count.Why
Surfaced reviewing the metric semantics: of the three classifiers,
inbox_messageandbountyare already post-join (they match platform DB records that only exist after registration) — butagent_peerhad no time bound. A personal transfer between two addresses years before either registered would today be classifiedagent_peerand counted as "earnings" — wrong, and trivially gameable (register two old addresses that historically moved money between them).What
fetchAgentPagenow returnsverified_atparsed to unix seconds (verifiedAtSec);0= no floor (all-time fallback whenverified_atis missing/unparseable).indexAgentapplies the floor incollect()(skipsblock_time < verifiedAtSec) and terminates backfill on the first pre-join page — so backfill stops at registration instead of walking to genesis. More correct and cheaper Hiro usage.Reversibility
Flippable to all-time later via a re-backfill (reset
earnings_index_state.backfill_complete/backfill_offset; the sweep re-walks history). And free to change right now — the indexer is dormant and no data has accumulated, so this is a no-cost decision until enabled.Notes
main; independent of the open Phase 3 API PR (feat(earnings): Phase 3 public read API — per-agent + platform + leaderboard (#978) #983) — different files.tsc+ lint clean; earnings tests green (+2 new injoin-date.test.tsfor theverified_at→ floor parsing incl. null/unparseable fallback).Part of #978.