[#1242] Add X handle + follow activation endpoints#1280
Conversation
Three SIWE-authenticated activation endpoints: - verify-x-handle: lookupXUser preview without DB write - confirm-x-handle: server-side re-lookup + UPSERT pl_activations with UNIQUE conflict → 409, R16 graceful degrade on API outage, auto-generates referral code on first confirm - x-follow-click: records x_follow_at, sets activated_at when handle confirmed + follow click complete activation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
realproject7
left a comment
There was a problem hiding this comment.
@re2 review — APPROVE ✅
Checked against issue #1242 acceptance criteria:
| Criterion | Status |
|---|---|
| All 3 endpoints reject invalid/expired sigs with 401 | ✅ via verifySiweRequest |
verify-x-handle returns lookup data without persisting |
✅ no DB imports |
confirm-x-handle server-side re-lookup (ignores client x_user_id) |
✅ RE1 Critical 2 |
| UNIQUE conflict → 409 | ✅ code 23505 check |
| R16 graceful degrade: twitterapi.io down → pending row | ✅ try/catch, null confirmedAt |
x-follow-click sets activated_at when conditions met |
✅ |
| Version 1.31.0 → 1.32.0 (feature) | ✅ |
Endpoint-by-endpoint review:
verify-x-handle — Pure lookup, no DB. lookupXUser error propagates as 500 (acceptable for preview — no degradation needed unlike confirm).
confirm-x-handle — Security-correct flow:
- Server calls
lookupXUser(username)directly — never trusts client-suppliedx_user_id - R16 path: twitterapi.io failure → writes row with
x_handle_confirmed_at = null, which avoids acquiring the partial UNIQUE index lock - UPSERT on
addressconflict, 23505 catches cross-wallet handle conflicts → 409 - Auto-generates referral code via
nanoid(8)on first confirm
x-follow-click — Correct activation gate:
- Requires existing activation row (400 if missing)
- Sets
activated_atonly when handle confirmed + not already activated - Idempotent: re-clicking updates
x_follow_atbut doesn't re-setactivated_at
Security notes:
- All endpoints SIWE-authenticated
- Username lowercased before storage
- No raw user input in queries (Supabase client parameterizes)
- No sensitive data leaked in responses
No blocking issues found.
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Summary
The endpoint structure is in place, but confirm-x-handle conflates a normal username-not-found response with the R16 outage path and the PR has no tests for the required endpoint behavior. The package lock metadata is also stale after the version bump.
Findings
-
[high]
confirm-x-handlewrites a pending activation row whenlookupXUser()returnsnull. R16 only allows the pending row when twitterapi.io throws/down; a 404/not-found username should not be persisted as a pending handle. As written, any signed wallet can reserve arbitrary nonexistent handles inpl_activationswithx_handle_confirmed_at = NULL, and later behavior depends on stale pending data.- File:
src/app/api/airdrop/confirm-x-handle/route.ts:35 - Suggestion: Distinguish
nullfrom thrown lookup errors. Return 404/no write fornull; only write pending state in the catch/outage path.
- File:
-
[medium] The PR adds no tests for the #1242 acceptance cases. These routes have important mocked boundaries: SIWE auth, twitterapi lookup success/null/throw, Supabase upsert/unique conflict, and activation update. Without route tests, regressions in invalid-signature 401s, verify-without-persisting, bogus
x_user_idignore, 409 conflict handling, R16 pending behavior, andx-follow-clickactivation would pass unnoticed.- File:
src/app/api/airdrop/verify-x-handle/route.ts:5 - Suggestion: Add route tests for all three endpoints with mocked
verifySiweRequest,lookupXUser, and Supabase client covering the acceptance checklist.
- File:
-
[medium]
package-lock.jsonwas not updated afterpackage.jsonmoved to1.32.0; the lockfile root still reports1.31.0.- File:
package-lock.json:3 - Suggestion: Regenerate or update the lockfile root version to match
package.json.
- File:
Decision
Requesting changes until not-found handles are not persisted, the acceptance behavior is covered by route tests, and the lockfile metadata is consistent. CI was still pending at review time.
confirm-x-handle now returns 404 for nonexistent X users (null) and only writes pending rows on API outage (throw). 12 tests cover: invalid sig → 401, verify without persist, server re-lookup ignores bogus x_user_id, 404 for ghost users, 409 UNIQUE conflict, R16 pending row, follow-click activation, no re-activation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Summary
The prior behavioral issues are addressed: nonexistent X users now return 404 without an upsert, route tests were added for the #1242 acceptance cases, and the lockfile version is fixed. One cleanup remains in the updated route before approval.
Findings
- [medium]
apiDownis assigned in the twitterapi.io catch path but never read. This leaves dead code in the route and is likely to fail the repo lint/typecheck job via unused-variable rules.- File:
src/app/api/airdrop/confirm-x-handle/route.ts:34 - Suggestion: Remove
apiDownentirely, or use it in the response if the pending/outage path needs to expose that state.
- File:
Decision
Requesting changes until the unused variable is removed or used. CI was still pending at review time, and local lint/typecheck could not be run because this checkout does not have dependencies installed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Summary
The requested changes are addressed. confirm-x-handle now returns 404 without persisting when lookup returns null, only writes pending rows on lookup throw, route tests cover the #1242 acceptance paths, and package-lock metadata matches the package version.
Findings
- None.
Decision
Approved. lint-and-typecheck and e2e are passing on the latest head.
realproject7
left a comment
There was a problem hiding this comment.
@re2 re-review — APPROVE ✅
Verified fixes for @re1's findings:
- [high] null vs throw — Fixed:
lookupXUserreturningnullnow correctly returns 404 with no DB write. Only the catch path (API throw) writes a pending row for R16 degradation. ✅ - [medium] Tests — 12 route tests added across all 3 endpoints. ✅
- [medium] Lockfile — Synced to 1.32.0. ✅
- Dead code — Unused
apiDownvariable removed. ✅
CI is fully green (lint-and-typecheck + e2e both pass). No remaining issues.
Summary
Three new SIWE-authenticated endpoints for the activation flow:
POST /api/airdrop/verify-x-handle— callslookupXUser(username), returns preview data (display name, avatar, followers, bio). No DB write.POST /api/airdrop/confirm-x-handle— server-side re-lookup (ignores client-supplied x_user_id per RE1 Critical 2), UPSERTspl_activationswith handle + user ID + confirmed timestamp. UNIQUE index conflict → 409. R16 graceful degrade: twitterapi.io down → writes pending row without UNIQUE lock. Auto-generatespl_referral_codesentry on first confirm.POST /api/airdrop/x-follow-click— recordsx_follow_at, setsactivated_atwhen handle confirmed + follow click complete the activation.Auth
All endpoints use
verifySiweRequestfromlib/airdrop/siwe-verify.ts. Invalid/expired signatures → 401.Version
1.31.0 → 1.32.0 (feature)
🤖 Generated with Claude Code