A prize sent to an account key is destroyed, not delayed - #227
Merged
Conversation
A player who signs in with Google is ranked under an address derived from their
account id — SHA-256 of it, per lib/authIdentity.ts. It looks exactly like a
wallet, it can RECEIVE USDT, and it can never send any, because no private key
for it exists anywhere in the world. Transfer a season prize there and the money
is gone: not stuck, not recoverable, gone.
Nothing distinguished that address from a real wallet. /api/player/seen recorded
`guest`, which comes from useWallet's isGuest — and isGuest is FALSE for an
account player, because WalletProvider stops minting a guest id once an account
address exists. So the one field that was supposed to separate people from
browsers quietly answered "real wallet" for the case that matters most.
This has been true since the Firebase sign-in shipped. The Privy gate did not
create it; it made it likely, because the talent.app judges will arrive through
Google rather than a wallet.
Three parts:
RECORD WHAT THE ID IS. players/{id} gains `kind` — wallet | account | guest —
written from the same call that already reported `guest`, which keeps being
written unchanged so /api/stats is untouched. Unrecognised values are stored as
unknown rather than guessed at.
RECORD WHERE MONEY CAN GO. Privy hands a Google player an embedded wallet they
actually control. That address is now kept as `payout`, and is deliberately NOT
used as their key: keying on it would grant account-level access to an address
the app cannot verify a signature for, and would orphan every save already
stored under the derived one.
REFUSE TO BURN IT. prepareSeason resolves each winner's payable address while
the ranking is being frozen and stores the answer in the snapshot, so the list
reviewed on Monday is the list paid on Tuesday. payoutCommands then:
· emits NO transfer for a winner with nowhere to be paid, and names them so
the owner can go and ask;
· withholds the ENTIRE on-chain half when a podium winner cannot claim — the
contract pays by claim, so funding the pool for someone who cannot sign
locks the deposit away, which is worse than a failed transfer because the
treasury has already paid;
· pays everyone else at their embedded wallet, saying so on the line;
· and marks winners ranked before any of this existed as UNVERIFIED rather
than either paying them silently or blocking them. Unknown is not the same
as safe — it means nobody has checked — and treating it as unpayable would
strand legitimate winners who predate the field.
One blocked winner never stops the others.
docs/OWNER-RUNBOOK.md gains the three warning shapes and what to do about each.
Verified: 128/128 test-season-close (14 of them new and covering exactly this),
test-season-routes, test:privy, test:season-ui, tsc clean, build unchanged at
142/242 kB, check:copy, check:cssvars, check:market, audit, lint 35.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A764RdnwpyWnG7uCNhMiQ
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A player who signs in with Google is ranked under an address derived from their account id — SHA-256 of it, per
lib/authIdentity.ts. It looks exactly like a wallet, it can receive USDT, and it can never send any, because no private key for it exists anywhere in the world. Transfer a season prize there and the money is gone: not stuck, not recoverable, gone.Nothing distinguished that address from a real wallet.
/api/player/seenrecordedguest, which comes fromuseWallet'sisGuest— andisGuestis false for an account player, becauseWalletProviderstops minting a guest id once an account address exists. The one field meant to separate people from browsers quietly answered "real wallet" for the case that matters most.This has been true since the Firebase sign-in shipped. The Privy gate did not create it; it made it likely, because the talent.app judges will arrive through Google rather than a wallet.
Three parts
Record what the id is.
players/{id}gainskind—wallet|account|guest— written from the same call that already reportedguest, which keeps being written unchanged so/api/statsis untouched. Unrecognised values are stored as unknown rather than guessed at.Record where money can go. Privy hands a Google player an embedded wallet they actually control. That address is now kept as
payout, and is deliberately not used as their key: keying on it would grant account-level access to an address the app cannot verify a signature for, and would orphan every save already stored under the derived one.Refuse to burn it.
prepareSeasonresolves each winner's payable address while the ranking is being frozen and stores the answer in the snapshot, so the list reviewed on Monday is the list paid on Tuesday.payoutCommandsthen emits no transfer for a winner with nowhere to be paid, withholds the entire on-chain half when a podium winner cannot claim, pays everyone else at their embedded wallet, and marks pre-existing winners UNVERIFIED rather than silently paying or blocking them.Unknown is not the same as safe — it means nobody has checked — and treating it as unpayable would strand legitimate winners who predate the field. One blocked winner never stops the others.
What the owner actually sees
Once rank 3 supplies an address, the same season emits the full on-chain half again, with rank 2 and 3 published at their embedded wallets rather than the keys they are ranked under.
Why the on-chain half is withheld, not just flagged
The reward contract pays by claim. A winner who cannot sign can never claim, so funding the pool for them locks the deposit in a contract with no way out — strictly worse than a failed transfer, because the treasury has already paid.
Testing
docs/OWNER-RUNBOOK.mdgains the three warning shapes and what to do about each.No contract changes.
guestand/api/statsare untouched; both new fields are additive and nothing already stored changes meaning.Generated by Claude Code