feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: auto-publish soldiers ranking + prize zaps from results podium - #52

Merged
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation
Jul 1, 2026
Merged

feat: auto-publish soldiers ranking + prize zaps from results podium#52
agustinkassis merged 1 commit into
mainfrom
feat/prize-zap-podium-and-ranking-automation

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Two related pieces of hackathon-results tooling, plus supporting fixes.

1. Ranking auto-publish

  • New scripts/publish-soldiers-ranking.mjs — a headless equivalent of the /soldados"Recrear ranking" admin button. It self-signs the authorization with LACRYPTA_NSEC and republishes the ranking Nostr snapshot via POST /api/soldiers/ranking, so new hackathon results propagate to /soldados without a manual browser click.
    • No-ops when LACRYPTA_NSEC isn't set; best-effort (never fails the reports build).
    • Chained into build-hackathon-reports.mjs (skip with SKIP_RANKING_PUBLISH=1); also pnpm run ranking:publish.
    • ⚠️ Publishes a real event to public Nostr relays — see the CLAUDE.md note.
  • Bug fix in build-hackathon-reports.mjs: it was silently dropping the identity/commerce hackathon entries from reports.json on every run (their data predates the script and has no .md source in-tree). Now it preserves an existing entry when a hackathon has no .md files, instead of wiping it.

2. Prize zaps from the results podium

  • Wire the existing PrizeZapButton into VotingHero's closed-results podium + runner-up rows, keyed by each winner's recipientPubkey (already frozen at voting close in results.winners). Moved PrizeZapButton to components/voting/ since it's now shared by the home and hackathon pages.
  • New GET /api/prize-recipient: resolves the recipient's Lightning address only from their kind-0 profile lud16 — never nip05 (that's an identity handle, not a payout destination). Server-side cached lookup, reliable where the client-side single-shot relay fetch was flaky.
  • handlePay resolves the destination up front, so payment no longer depends on the flaky client-side findZapEndpoint (the source of the "No encontré el perfil Nostr del ganador" error).

Supporting fixes

  • Admin gate:PrizeZapButton now gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB) like every other admin control, not the publisher key — so the button actually shows for the admin.
  • Stuck-disabled buttons: the receipt-check effect depended on the inline target object (new every render), pinning checking → permanently disabled. Now depends on target's primitive fields.
  • Signer flooding:lib/auth.ts dedupes probeSignerAvailable per session, so many useAuth consumers (one button per podium slot) share a single window.nostr.getPublicKey() call.

Testing

  • tsc --noEmit clean.
  • /api/prize-recipient verified to resolve winners' real lud16 addresses (blink.sv, primal.net) — all valid LNURL-pay endpoints with allowsNostr: true.
  • Ranking script verified to run and no-op/skip correctly; reports.json regenerates byte-identical.
  • Verified in the browser preview (anonymous + reasoning through the admin path). No actual prize payment was triggered (would send real sats on real relays).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prize recipients can now be resolved automatically for prize payouts, enabling zap payments to selected winners.
    • Closed hackathon results now show a zap payment action for podium finishers and runners-up when a prize is available.
  • Bug Fixes

    • Improved payout destination lookup and fallback handling so prize payments are more reliable.
    • Hackathon report generation now keeps existing entries when source files are missing instead of dropping them.

Ranking automation:
- scripts/publish-soldiers-ranking.mjs: headless version of the /soldados
"Recrear ranking" admin action — self-signs with LACRYPTA_NSEC and
republishes the ranking Nostr snapshot. No-ops without the secret; never
fails the reports build. Chained into build-hackathon-reports.mjs (skip
with SKIP_RANKING_PUBLISH=1) and exposed as `pnpm run ranking:publish`.
- build-hackathon-reports.mjs: stop silently dropping hackathons whose report
dir has no .md source (identity/commerce data predates the script) — preserve
their existing reports.json entry instead of wiping it.
Prize zaps on the results podium:
- Wire the existing PrizeZapButton into VotingHero's ClosedHero podium +
runner-up rows, keyed by each winner's recipientPubkey (frozen at close in
results.winners). Moved PrizeZapButton to components/voting/ since it's now
shared by the home and hackathon pages.
- New GET /api/prize-recipient: resolves the recipient's Lightning address
from their kind-0 profile lud16 ONLY (never nip05 — that's an identity
handle, not a payout address). Server-side cached lookup, which is reliable
where the client-side single-shot relay fetch was flaky.
- PrizeZapButton gates on the admin pubkey (NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB),
matching every other admin control, instead of the publisher key.
- Resolve the destination in handlePay before paying so it no longer depends
on the flaky client-side findZapEndpoint (the "No encontré el perfil" error).
- Fix stuck-disabled pay buttons: the receipt-check effect depended on the
inline `target` object, re-running every render and pinning `checking`.
Depend on target's primitive fields instead.
Signer probe dedupe:
- lib/auth.ts: dedupe probeSignerAvailable per session so many useAuth
consumers (one PrizeZapButton per podium slot) share a single
window.nostr.getPublicKey() call instead of flooding the extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 1, 2026 7:05pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a /api/prize-recipient route resolving Lightning payout info from a Nostr pubkey; integrates it and /api/lacrypta-pubkeys into PrizeZapButton and VotingHero's closed-results UI. Caches the auth signer probe. Adds a soldiers-ranking republish script wired into the hackathon reports build, plus docs and package scripts.

Changes

Prize Zap Recipient Resolution

Layer / File(s)Summary
Prize recipient resolution API
app/api/prize-recipient/route.ts
Validates a 64-char hex pubkey, fetches cached Nostr profile, resolves lud16 into a Lightning address and LNURLp zapEndpoint, and returns them as JSON (with graceful failure).
PrizeZapButton admin pubkey and destination resolution
components/voting/PrizeZapButton.tsx
Fetches admin pubkeys via /api/lacrypta-pubkeys instead of local resolution, fetches recipientPaymentInfo from /api/prize-recipient when not pre-provided, narrows effect dependencies to target identity fields, and computes an effectiveTarget with resolved destination before calling payPrizeZap.
VotingHero podium/runner integration
components/voting/VotingHero.tsx, app/hackathons/[id]/page.tsx
Builds a recipientByProject map from winners and passes hackathonId/recipientPubkey into PodiumCard/RunnerRow, which now conditionally render PrizeZapButton in place of the static prize chip; updates the PrizeZapButton import to the shared component path.

Auth Signer Probe Caching

Layer / File(s)Summary
Cached signer probe and invalidation
lib/auth.ts
Adds a module-level signerProbeCache keyed by auth method/pubkey to deduplicate the NIP-07 signer probe across useAuth() consumers, clearing it on setAuth()/clearAuth() and on probe failure/rejection.

Soldiers Ranking Republish Pipeline

Layer / File(s)Summary
publish-soldiers-ranking.mjs script
scripts/publish-soldiers-ranking.mjs
New CLI that loads .env.local, reads LACRYPTA_NSEC, derives the signing pubkey, builds and signs a kind 27235 Nostr event, POSTs it to /api/soldiers/ranking, and logs success/failure.
Reports build wiring, preservation, and docs
scripts/build-hackathon-reports.mjs, package.json, CLAUDE.md
build-hackathon-reports.mjs preserves prior reports.json entries when .md sources are missing and invokes the ranking-publish script via spawnSync unless SKIP_RANKING_PUBLISH=1; package.json adds reports:build/ranking:publish scripts; CLAUDE.md documents the preservation and publish behavior.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
participant VotingHero
participant PrizeZapButton
participant PrizeRecipientAPI
participant LacryptaPubkeysAPI
participant payPrizeZap
VotingHero->>PrizeZapButton: render with hackathonId, recipientPubkey, sats
PrizeZapButton->>LacryptaPubkeysAPI: fetch admin pubkeys
LacryptaPubkeysAPI-->>PrizeZapButton: adminPubkey
PrizeZapButton->>PrizeRecipientAPI: GET /api/prize-recipient?pubkey=...
PrizeRecipientAPI-->>PrizeZapButton: lightningAddress, zapEndpoint, source
PrizeZapButton->>PrizeZapButton: handlePay computes effectiveTarget
PrizeZapButton->>payPrizeZap: pay(effectiveTarget)
Loading
sequenceDiagram
participant BuildScript as build-hackathon-reports.mjs
participant ReportsJSON as reports.json
participant PublishScript as publish-soldiers-ranking.mjs
participant RankingAPI as /api/soldiers/ranking
BuildScript->>ReportsJSON: load previous entries
BuildScript->>BuildScript: regenerate or preserve per-hackathon entries
BuildScript->>ReportsJSON: write updated reports.json
BuildScript->>PublishScript: spawnSync (unless SKIP_RANKING_PUBLISH=1)
PublishScript->>PublishScript: sign kind 27235 event with LACRYPTA_NSEC
PublishScript->>RankingAPI: POST signed event
RankingAPI-->>PublishScript: soldier count, generated timestamp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title captures the two main changes: automatic soldiers ranking publishing and prize zaps added to the results podium.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prize-zap-podium-and-ranking-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit cf2bf6b into mainJul 1, 2026
2 of 3 checks passed

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/prize-recipient/route.ts`:
- Around line 17-20: Harden Lightning-address handling in
lightningAddressToLnurlpEndpoint and the zapEndpoint flow by parsing the address
as a real hostname instead of relying on split("@"), and reject malformed or
unsafe hosts before constructing the LNURL URL. Update the /api/lnurl-invoice
validation to block private/reserved IP ranges and path-bearing or otherwise
unsafe domains, not just localhost/loopback/.local, so only safe destinations
can be returned. Apply the same validation logic in app/hackathons/[id]/page.tsx
where the Lightning address is used to build the endpoint.
In `@components/voting/PrizeZapButton.tsx`:
- Around line 160-162: The payout-source comments in PrizeZapButton are out of
sync with the `/api/prize-recipient` contract and still mention a submission
nip05 fallback. Update the nearby comments around the recipient resolution logic
to state that the API resolves profile lud16 only and that this is the supported
payout source, keeping the description aligned with the server-side rule and
removing any fallback wording. Match the wording in both comment locations tied
to the recipient resolution flow so future changes aren’t misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6175ed37-4966-4954-b3c4-40be42234de4

📥 Commits

Reviewing files that changed from the base of the PR and between 45d6030 and 6e5d2e0.

📒 Files selected for processing (9)
  • CLAUDE.md
  • app/api/prize-recipient/route.ts
  • app/hackathons/[id]/page.tsx
  • components/voting/PrizeZapButton.tsx
  • components/voting/VotingHero.tsx
  • lib/auth.ts
  • package.json
  • scripts/build-hackathon-reports.mjs
  • scripts/publish-soldiers-ranking.mjs

Comment on lines +17 to +20
function lightningAddressToLnurlpEndpoint(address: string): string | null {
const [name, domain] = address.split("@");
if (!name || !domain) return null;
return `https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify the downstream invoice route validates endpoint scheme/host before fetch.
rg -n -C4 'lnurl-invoice|endpoint|fetch\\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 272


🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n -C3 'lnurl-invoice|lightningAddressToLnurlpEndpoint|payPrizeZap|recipientZapEndpoint|fetch\(' app lib components

Repository: lacrypta/lacrypta-dev

Length of output: 39782


🏁 Script executed:

#!/bin/bashset -euo pipefail
sed -n '1,220p' app/api/lnurl-invoice/route.ts

Repository: lacrypta/lacrypta-dev

Length of output: 4707


Harden Lightning-address parsing before building the LNURL endpoint.split("@") still turns malformed lud16 values into fetchable URLs, and /api/lnurl-invoice only blocks localhost/loopback/.local, not private/reserved IPs or path-bearing domains. Parse the address as a real hostname and reject unsafe hosts before returning zapEndpoint. Also applies to app/hackathons/[id]/page.tsx:128-131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/prize-recipient/route.ts` around lines 17 - 20, Harden
Lightning-address handling in lightningAddressToLnurlpEndpoint and the
zapEndpoint flow by parsing the address as a real hostname instead of relying on
split("@"), and reject malformed or unsafe hosts before constructing the LNURL
URL. Update the /api/lnurl-invoice validation to block private/reserved IP
ranges and path-bearing or otherwise unsafe domains, not just
localhost/loopback/.local, so only safe destinations can be returned. Apply the
same validation logic in app/hackathons/[id]/page.tsx where the Lightning
address is used to build the endpoint.

Comment on lines +160 to +162
// Otherwise (e.g. the results podium only knows the pubkey), resolve it
// server-side: profile lud16 → submission nip05 fallback (see
// /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the payout-source comments aligned with the API contract.

/api/prize-recipient resolves profile lud16 only, but these comments still describe a submission nip05 fallback. That contradicts the route’s payout safety rule and can mislead future changes.

Suggested comment fix
- // server-side: profile lud16 → submission nip05 fallback (see- // /api/prize-recipient). Fills "Destino" and gives the pay step an endpoint.+ // server-side: profile lud16 only (see /api/prize-recipient). Fills+ // "Destino" and gives the pay step an endpoint.
...
- // Resolve the destination reliably from the server (profile lud16 →- // submission nip05) instead of depending on the display effect having+ // Resolve the destination reliably from the server (profile lud16 only)+ // instead of depending on the display effect having

Also applies to: 214-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/voting/PrizeZapButton.tsx` around lines 160 - 162, The
payout-source comments in PrizeZapButton are out of sync with the
`/api/prize-recipient` contract and still mention a submission nip05 fallback.
Update the nearby comments around the recipient resolution logic to state that
the API resolves profile lud16 only and that this is the supported payout
source, keeping the description aligned with the server-side rule and removing
any fallback wording. Match the wording in both comment locations tied to the
recipient resolution flow so future changes aren’t misled.

Sign up for freeto 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

@agustinkassis