Skip to content

feat(competition): require Genesis (Level 2) for trade scoring#814

Merged
whoabuddy merged 2 commits into
mainfrom
feat/competition-require-genesis
May 13, 2026
Merged

feat(competition): require Genesis (Level 2) for trade scoring#814
whoabuddy merged 2 commits into
mainfrom
feat/competition-require-genesis

Conversation

@biwasxyz
Copy link
Copy Markdown
Contributor

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 via POST /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 state Before After
Not registered sender_not_registered sender_not_registered ❌ (unchanged)
Level 1 (Verified Agent) ✅ scores sender_not_genesis
Level 2 (Genesis) ✅ scores ✅ scores (unchanged)

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 in swaps; only new submissions are gated.

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:67-87 exactly — Genesis = agent row exists AND claims.status IN ('verified', 'rewarded').
  • Single round-trip with a LEFT JOIN so we can produce the precise rejection code without doing two reads.
  • Scheduler's CompetitionSchedulerRejectionReasons type + emptyRejectionReasons() get the new code so cron-path summaries report it correctly.

Tests

Vitest competition suite is green (68 tests):

✓ lib/competition/__tests__/d1-reads.test.ts     (18)
✓ lib/competition/__tests__/parse.test.ts        (13)
✓ lib/competition/__tests__/scheduler.test.ts    (10)
✓ lib/competition/__tests__/verify.test.ts       (27)
  • D1 mock got a new genesis?: boolean option; defaults to true when registered: true so the existing happy-path tests pass unchanged
  • One new test added for the sender_not_genesis path (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 on POST /api/competition/trades now enumerates both sender_not_registered and sender_not_genesis

Rollout impact

When this deploys, any in-flight Level 1 swap submissions will start getting sender_not_genesis. Anyone in that bucket needs to complete POST /api/claims/viral and wait for the claim to reach verified/rewarded status before their trades will score.

The catch-up SchedulerDO will also start emitting sender_not_genesis in 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

  • Vitest suite green (68 tests; 1 new for the Genesis gate)
  • After merge + deploy: submit a Level 1 agent's swap → 422 with code: "sender_not_genesis" and the Genesis claim hint in the reason
  • After merge + deploy: submit a Genesis agent's allowlisted swap → 200 verified (unchanged path)
  • Cron summary log includes a non-zero sender_not_genesis count if any Level 1 agents are still attempting swaps

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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 13, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
landing-page 4a4ecc2 May 13 2026, 02:19 PM

Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

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

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:

  • SenderTier as a discriminated string union is the right model — cleaner than a nullable boolean pair
  • Backward-compatible mock default (genesis ?? true when registered: true) keeps 67 existing tests passing untouched
  • CompetitionSchedulerRejectionReasons and emptyRejectionReasons() updated consistently — the cron-path summary will report the new code correctly
  • senderEligibilityTier function comment is thorough and references lib/levels.ts:67-87 as 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.

Copy link
Copy Markdown
Contributor

Addressed the review blockers in 4a4ecc2:

  • senderEligibilityTier now aggregates claim rows with MAX(CASE WHEN c.status IN ('verified', 'rewarded') THEN 1 ELSE 0 END) and GROUP BY rw.stx_address, so multiple claims cannot make a Genesis agent depend on arbitrary .first() row order.
  • Switched the agents join to LEFT JOIN so a registered_wallets row with missing/drifted agents data is still classified as registered-but-not-Genesis (sender_not_genesis) rather than sender_not_registered.
  • Added a regression assertion in verify.test.ts that the eligibility SQL keeps MAX(...), LEFT JOIN agents, and GROUP BY rw.stx_address.

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 passed

Cloudflare bot has picked up the new commit and is currently in progress on 4a4ecc2.

Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

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

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) with GROUP BY rw.stx_address — multi-claim aggregation is now correct; no more undefined row-order risk
  • LEFT JOIN agents (was INNER JOIN) — a registered_wallets row with a missing agents row now correctly surfaces as sender_not_genesis, not the misleading sender_not_registered
  • Regression assertions in verify.test.ts confirm the SQL keeps MAX(...), LEFT JOIN agents, and GROUP 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.

@whoabuddy whoabuddy merged commit 0e20707 into main May 13, 2026
7 of 8 checks passed
@whoabuddy whoabuddy deleted the feat/competition-require-genesis branch May 13, 2026 14:25
secret-mars added a commit to secret-mars/landing-page that referenced this pull request May 13, 2026
…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).
whoabuddy pushed a commit that referenced this pull request May 13, 2026
… 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.
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.

3 participants