feat(competition): require Genesis (Level 2) for trade scoring#814
Conversation
Verifier now rejects trades from any sender that isn't at Genesis
level. Previous behavior accepted any address present in
registered_wallets (effectively Level 1, BTC+STX dual-sig). Going
forward, only agents with a verified or rewarded viral claim
(POST /api/claims/viral, status IN ('verified','rewarded')) can score.
Implementation:
- New VerifyFailureCode value `sender_not_genesis`
- `senderEligibilityTier` returns 'not_registered' | 'registered' | 'genesis'
via a single D1 read joining registered_wallets + agents + claims.
Predicate matches `computeLevel()` in lib/levels.ts exactly so the
verifier and the agent-level system agree on the Genesis bar.
- Verifier rejects with `sender_not_genesis` (clear reason mentioning
/api/claims/viral) for Level 1 addresses; existing `sender_not_registered`
still applies for unregistered addresses.
- Scheduler's rejection-reasons enum + emptyRejectionReasons() include the
new code so cron-path summaries report it correctly.
- Test fixture defaults `genesis: true` when `registered: true` so existing
happy-path tests don't need changes; explicit `genesis: false` exercises
the new rejection path (one new test added).
Docs updated in the same PR:
- app/llms.txt: adds an "Eligibility" callout to the Trading Competition
section explaining Genesis is required.
- app/llms-full.txt: new "Eligibility — Genesis Required" subsection
under ## Trading Competition; 422 rejection-code description expanded
to list both sender_not_registered and sender_not_genesis.
Applies to both ingestion paths (agent-submit POST and SchedulerDO
catch-up). Pre-existing swaps that were ingested under the old rules
stay in the table — no retroactive purge. Once an agent reaches
Genesis, subsequent terminal-status, allowlisted swaps score normally.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
landing-page | 4a4ecc2 | May 13 2026, 02:19 PM |
arc0btc
left a comment
There was a problem hiding this comment.
Adds the Genesis gate cleanly — upgrading from a single boolean to a 3-way tier enum is the right abstraction, the SQL is a single round-trip as promised, the test mock's genesis ?? true default keeps the existing suite clean, and the docs in both llms.txt files are unusually thorough. Agents consuming these endpoints will understand exactly what they need to do.
Two issues before merge:
[blocking] Multi-claim agent gets wrong tier (lib/competition/verify.ts:89–103)
The LEFT JOIN claims c ON c.btc_address = a.btc_address without GROUP BY or LIMIT returns one row per claim. If an agent has multiple rows in claims (e.g., one pending and one verified), .first() returns whichever row D1 picks — undefined order. An agent who IS Genesis could be classified as registered if a non-qualifying claim row appears first, silently blocking their trades.
Fix — aggregate with MAX() so you get exactly one row regardless of how many claims exist:
SELECT
1 AS registered,
MAX(CASE WHEN c.status IN ('verified', 'rewarded') THEN 1 ELSE 0 END) AS genesis
FROM registered_wallets rw
INNER JOIN agents a ON a.stx_address = rw.stx_address
LEFT JOIN claims c ON c.btc_address = a.btc_address
WHERE rw.stx_address = ?1
GROUP BY rw.stx_address
Alternatively, replace the LEFT JOIN with a correlated EXISTS subquery — same semantics, no multi-row risk, and arguably clearer intent. If claims has a unique constraint on (btc_address) that prevents duplicates, this is moot — but document that invariant, or be defensive either way.
[question] INNER JOIN on agents shifts the error code for partial-registration states (lib/competition/verify.ts:89)
The old check was SELECT 1 FROM registered_wallets WHERE stx_address = ?1. The new query INNER JOINs agents. An address present in registered_wallets but missing from agents (e.g., after a partial registration failure or data drift) previously passed the registered check and would now get a sender_not_genesis rejection — informative. Under the new query it returns null → tier = "not_registered" → sender_not_registered — misleading.
If both rows are always populated atomically by the registration flow, this can't happen. But if that invariant isn't enforced at the DB level, a LEFT JOIN agents would make the error message accurate for anyone in that state:
FROM registered_wallets rw
LEFT JOIN agents a ON a.stx_address = rw.stx_address
LEFT JOIN claims c ON c.btc_address = a.btc_address
[nit] Cloudflare deploy just failed — the bot posted a failure comment seconds after the PR was opened. The code changes look syntactically valid and the SQL is well-formed, so this is probably infra/binding, but worth verifying before merge given the rollout impact described in the PR.
What looks good:
SenderTieras a discriminated string union is the right model — cleaner than a nullable boolean pair- Backward-compatible mock default (
genesis ?? truewhenregistered: true) keeps 67 existing tests passing untouched CompetitionSchedulerRejectionReasonsandemptyRejectionReasons()updated consistently — the cron-path summary will report the new code correctlysenderEligibilityTierfunction comment is thorough and referenceslib/levels.ts:67-87as the canonical source — the WHY is documented
Operational note: We process competition trades in production. The INNER JOIN adds two table reads per submission, but D1 query latency isn't a bottleneck at this call site. The real operational concern is the rollout: any Level 1 agents actively submitting will hit sender_not_genesis immediately on deploy. The PR's rollout section is accurate — the hint message pointing to POST /api/claims/viral is the right UX for that case.
|
Addressed the review blockers in
Verification run in a clean PR worktree using the repo toolchain: npm run test -- lib/competition/__tests__/verify.test.ts lib/competition/__tests__/scheduler.test.ts
# 2 files passed, 38 tests passedCloudflare bot has picked up the new commit and is currently in progress on |
arc0btc
left a comment
There was a problem hiding this comment.
Both blockers from cycle 1 are resolved. Approving.
What changed in 4a4ecc2:
MAX(CASE WHEN c.status IN ('verified', 'rewarded') THEN 1 ELSE 0 END)withGROUP BY rw.stx_address— multi-claim aggregation is now correct; no more undefined row-order riskLEFT JOIN agents(wasINNER JOIN) — aregistered_walletsrow with a missingagentsrow now correctly surfaces assender_not_genesis, not the misleadingsender_not_registered- Regression assertions in
verify.test.tsconfirm the SQL keepsMAX(...),LEFT JOIN agents, andGROUP BY rw.stx_address— if the query drifts back, the test breaks
Remaining item (not blocking):
The Cloudflare bot posted a failure on the prior commit; whoabuddy's comment says a new build is in progress on 4a4ecc2. The code changes are syntactically valid and the SQL is well-formed — this is an infra/binding issue to resolve post-merge, not a code correctness issue.
On the docs:
The llms.txt / llms-full.txt updates are the right call — the 422 response matrix now enumerates both rejection codes and the eligibility prerequisites. Agents consuming the API will understand exactly what they need to do without reading source code.
This is clean work. The tier model ("not_registered" | "registered" | "genesis") is more honest than the old boolean and the single D1 round-trip is efficient. Ready to merge once the CF build green-lights.
…y-mint flow Per operator's Telegram review at 17:31Z: llms.txt was missing the third trade-eligibility requirement (ERC-8004 NFT mint), so agents reading the docs literally would complete only 1+2 (Verified + Genesis claim) and hit `sender_not_registered` from `senderEligibilityTier` despite passing both visible-from-docs gates. Likely the proximate cause of the 19 ungated senders observed on /leaderboard at 17:03Z (per secret-mars/drx4/daemon/comp-participants-2034v320c.json) — they hit steps 1+2 then assumed they were done. Two doc files updated: - app/llms.txt: competition Eligibility section rewritten from "two prerequisites" to "all three one-time onboarding steps" enumerating Verified Agent + Genesis + ERC-8004 NFT, with the missing-NFT rejection code explicitly named (`sender_not_registered`, because the registered_wallets view JOINs through agents on erc8004_agent_id). Inline minting steps added showing both `identity_register` (the recommended higher-level MCP helper) and `call_contract` with `register-with-uri` (the lower-level path documented in /docs/identity.txt). - app/llms-full.txt: same fix to the Trading Competition > Eligibility subsection. Section title updated from "Eligibility — Genesis Required" to "Eligibility — Genesis + On-Chain Identity Required" to match the new three-step framing. Same inline mint flow + cross-link to the canonical /identity page and /docs/identity.txt. Both changes match the actual senderEligibilityTier predicate in `lib/competition/verify.ts:106-127` and the canonical rules at aibtcdev#815 §1. Refs: aibtcdev#815 §1 (canonical rules), aibtcdev#814 (Genesis gate), secret-mars/drx4/daemon/comp-participants-2034v320c.json (empirical evidence of agents stuck at steps 1+2). No code paths touched; no test changes (full vitest suite stays green at 1189 passed / 5 skipped).
… Part 2) (#825) Trading competition starts at COMP_START_TIMESTAMP=1778700600 (2026-05-13T19:30Z, set by #819). The verifier rejects future swaps with burn_block_time below that as `before_comp_start`, but `swaps` already contains pre-launch rows ingested by the catch-up cron during the development window — including: - Level-1-only senders' rows that pre-date #814's Genesis gate (deployed 14:25Z) and were preserved by the no-retroactive-purge policy - Genesis-tier senders' development-period trades that the campaign should not retroactively count Per #823 Part 2: clean these so the leaderboard renders empty at launch and accumulates only fresh post-launch trades. Boundary semantics: rows at exactly burn_block_time = 1778700600 are preserved, matching `verify.ts:318` `>=` and the vitest "boundary accepted" assertion at `verify.test.ts:268-275`. Forward-only — no rollback for deleted audit rows. Same trade-off as #818's no-retroactive-purge choice, inverted; here we explicitly purge so the competition window is the canonical scoring window from launch. Idempotent: re-running on an already-clean table is a no-op DELETE. Sanity-tested in sqlite: before: [('a', 1778600000), ('b', 1778700600), ('c', 1778800000)] after: [('b', 1778700600), ('c', 1778800000)] after re-run (idempotency): same Closes #823 Part 2. Pairs with #824 (Part 1 SSR Genesis filter) for full launch readiness. Refs: #815, #819, #814, #821, #823.
Summary
Verifier now rejects competition trades from any sender below Genesis (Level 2). Previously the only sender-side check was "address in
registered_wallets," which is effectively Level 1 (BTC+STX dual-sig viaPOST /api/register). Going forward, agents also need a verified or rewarded viral claim (POST /api/claims/viral) to score.The intent: agents on the trading-comp leaderboard should be ones the network has already vetted twice — cryptographic identity proof and a public on-record claim. Bumps the noise floor and aligns the trading-comp gate with what the rest of the level system already calls "Genesis."
Behavior change
sender_not_registered❌sender_not_registered❌ (unchanged)sender_not_genesis❌Applies to both ingestion paths: agent-submit (
POST /api/competition/trades) and the SchedulerDO catch-up cron. No retroactive purge — pre-existing rows from Level 1 senders stay inswaps; only new submissions are gated.Implementation
VerifyFailureCodevalue:sender_not_genesissenderEligibilityTierreturns'not_registered' | 'registered' | 'genesis'via a single D1 read joiningregistered_wallets+agents+claims. Predicate matchescomputeLevel()inlib/levels.ts:67-87exactly — Genesis = agent row exists ANDclaims.status IN ('verified', 'rewarded').LEFT JOINso we can produce the precise rejection code without doing two reads.CompetitionSchedulerRejectionReasonstype +emptyRejectionReasons()get the new code so cron-path summaries report it correctly.Tests
Vitest competition suite is green (68 tests):
genesis?: booleanoption; defaults totruewhenregistered: trueso the existing happy-path tests pass unchangedsender_not_genesispath (registered: true, genesis: false → rejected with the expected code + reason mentioning Genesis)Docs updated in the same PR
app/llms.txt— new "Eligibility" callout in the Trading Competition section, plus the catch-up sweep note now correctly says "any Genesis-level registered wallet"app/llms-full.txt— new "Eligibility — Genesis Required" subsection under## Trading Competition; the 422 response description onPOST /api/competition/tradesnow enumerates bothsender_not_registeredandsender_not_genesisRollout impact
When this deploys, any in-flight Level 1 swap submissions will start getting
sender_not_genesis. Anyone in that bucket needs to completePOST /api/claims/viraland wait for the claim to reach verified/rewarded status before their trades will score.The catch-up SchedulerDO will also start emitting
sender_not_genesisin its rejection-reason summaries — useful for spotting "how many Level 1 agents would have been counted under the old rules" at a glance.Test plan
code: "sender_not_genesis"and the Genesis claim hint in the reasonsender_not_genesiscount if any Level 1 agents are still attempting swaps