feat(profile): show a holder what was taken from them and what they e… - #4
Merged
Samuel1-ona merged 1 commit intoAug 30, 2026
Merged
Conversation
…arned Terreno's loop is buy → get raided → get paid → take it back, and the contract has always done all four. The app only ever showed the first. Nothing tells a holder that somebody took their pixel, that they were paid 95% of double what they spent, or that they could take it back — so the one moment that proves the game works is invisible to the person it happened to. It is worse than absent. `useActivityFeed` polls every 10s and surfaces OTHER people's buys as social proof, and explicitly skips the viewer's own. The app pushes strangers' activity and stays silent about the viewer's payout. Everything is pull; nothing pulls anyone back. This adds the losing side of a purchase, which nothing else reads. `Purchase.previousOwner` has been indexed since the subgraph shipped — its schema comment already calls it "the raid record" — so this is a query and a surface, with no contract change and no subgraph change. What changes: - `lib/subgraph.ts` — `fetchRaidsAgainst()`, the only query here that reads a purchase from the seller's side. Bounded at the query, so a much-raided wallet cannot pull an unbounded set through the route. - `app/api/raids` — groups by BATCH (one buyer taking four pixels in one transaction is one event, not four notifications), reconstructs the per-pixel figure, and nets the resale fee. - `hooks/useRaids` — one fetch per wallet/map change, no polling. The deed is read on arrival, not watched. - `components/Profile/RaidLedger` — the record, written as a payout rather than a loss. Two things are computed server-side and both matter. The fee: the subgraph credits `totalEarned` with gross `perPixelCost` while the seller receives `price − fee`, so amounts are netted exactly as `/api/pnl` does — and `resaleFee.ts` imports the server-only logger, so it could not run in the browser anyway. The per-pixel split: `Purchase.pricePaid` is exact only for single-pixel batches, so a multi-pixel batch is split evenly, matching how `mapping.ts` already credits `totalEarned`. Verified against the live Goldsky subgraph, not only against fixtures. For `0xa2acf…8022` on map 0 the route returns three raids whose per-raid GROSS sums to 344885 — byte-for-byte the `OwnerMapStat.totalEarned` the indexer had credited that wallet. The even split reproduces the indexer's arithmetic rather than approximating it. Real batches come back with `pricePaid: null`, so the even-split branch is the production path, and the raider in that data has no profile, so the generated-name fallback is one too. Netting per raid drifts from `/api/pnl`'s aggregate netting by at most one microcent per raid (2 for that wallet) because integer truncation lands differently. Both render as $0.33, so it is invisible at the two decimals `formatUSDT` shows. Documented and pinned by a test using those mainnet numbers rather than "corrected" — apportioning an aggregate back across cards would make every card depend on every other one. Why it's safe: nothing on the money path reads any of this. It is a read-only surface; buying is untouched. "No raids" and "cannot tell" are distinct — without the subgraph there is no way to read the losing side, and rendering "nobody has taken anything from you" would state something unverified as fact. Deliberately not included: no push notification (needs infrastructure this repo does not have) and no TAKE IT BACK deep link yet — the pixel ids are returned so it is a UI change when wanted. Verification: `tsc --noEmit` clean, `next lint` clean, full suite passes (65 files), coverage floors held. 26 new tests. Mutation-tested: reporting gross turns 5 red, ignoring `pricePaid` turns 1 red, claiming `available` without the subgraph turns 1 red. Fixed one real bug the tests caught — a missing `mapId` fell through to map 0, because `Number(null)` is 0. Not verified automatically: the ledger's appearance on a phone. Needs a look on a preview deployment at mobile width. Merge order: stacked on samuel1-ona/nim-address-unlinkable-on-deed, which also edits `app/profile/page.tsx`. Merge that first and this retargets cleanly; happy to rebase onto main if it lands the other way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ckb8yB9X3p41mVvfmfQdpo
|
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.
…arned
Terreno's loop is buy → get raided → get paid → take it back, and the contract has always done all four. The app only ever showed the first. Nothing tells a holder that somebody took their pixel, that they were paid 95% of double what they spent, or that they could take it back — so the one moment that proves the game works is invisible to the person it happened to.
It is worse than absent.
useActivityFeedpolls every 10s and surfaces OTHER people's buys as social proof, and explicitly skips the viewer's own. The app pushes strangers' activity and stays silent about the viewer's payout. Everything is pull; nothing pulls anyone back.This adds the losing side of a purchase, which nothing else reads.
Purchase.previousOwnerhas been indexed since the subgraph shipped — its schema comment already calls it "the raid record" — so this is a query and a surface, with no contract change and no subgraph change.What changes:
lib/subgraph.ts—fetchRaidsAgainst(), the only query here that reads a purchase from the seller's side. Bounded at the query, so a much-raided wallet cannot pull an unbounded set through the route.app/api/raids— groups by BATCH (one buyer taking four pixels in one transaction is one event, not four notifications), reconstructs the per-pixel figure, and nets the resale fee.hooks/useRaids— one fetch per wallet/map change, no polling. The deed is read on arrival, not watched.components/Profile/RaidLedger— the record, written as a payout rather than a loss.Two things are computed server-side and both matter. The fee: the subgraph credits
totalEarnedwith grossperPixelCostwhile the seller receivesprice − fee, so amounts are netted exactly as/api/pnldoes — andresaleFee.tsimports the server-only logger, so it could not run in the browser anyway. The per-pixel split:Purchase.pricePaidis exact only for single-pixel batches, so a multi-pixel batch is split evenly, matching howmapping.tsalready creditstotalEarned.Verified against the live Goldsky subgraph, not only against fixtures. For
0xa2acf…8022on map 0 the route returns three raids whose per-raid GROSS sums to 344885 — byte-for-byte theOwnerMapStat.totalEarnedthe indexer had credited that wallet. The even split reproduces the indexer's arithmetic rather than approximating it. Real batches come back withpricePaid: null, so the even-split branch is the production path, and the raider in that data has no profile, so the generated-name fallback is one too.Netting per raid drifts from
/api/pnl's aggregate netting by at most one microcent per raid (2 for that wallet) because integer truncation lands differently. Both render as $0.33, so it is invisible at the two decimalsformatUSDTshows. Documented and pinned by a test using those mainnet numbers rather than "corrected" — apportioning an aggregate back across cards would make every card depend on every other one.Why it's safe: nothing on the money path reads any of this. It is a read-only surface; buying is untouched. "No raids" and "cannot tell" are distinct — without the subgraph there is no way to read the losing side, and rendering "nobody has taken anything from you" would state something unverified as fact.
Deliberately not included: no push notification (needs infrastructure this repo does not have) and no TAKE IT BACK deep link yet — the pixel ids are returned so it is a UI change when wanted.
Verification:
tsc --noEmitclean,next lintclean, full suite passes (65 files), coverage floors held. 26 new tests. Mutation-tested: reporting gross turns 5 red, ignoringpricePaidturns 1 red, claimingavailablewithout the subgraph turns 1 red. Fixed one real bug the tests caught — a missingmapIdfell through to map 0, becauseNumber(null)is 0.Not verified automatically: the ledger's appearance on a phone. Needs a look on a preview deployment at mobile width.
Merge order: stacked on samuel1-ona/nim-address-unlinkable-on-deed, which also edits
app/profile/page.tsx. Merge that first and this retargets cleanly; happy to rebase onto main if it lands the other way.Claude-Session: https://claude.ai/code/session_01Ckb8yB9X3p41mVvfmfQdpo
The hole, and the fix
What this does NOT do / residual risk
Judgement calls
Issues
Closes #
Refs #
Stacking / conflicts
Verification evidence
Remaining ops steps
Checklist
mainlint/typecheck/testpass locally on the current headClosesissue — all met.env.example/ examples / error strings updated for the world this createsmain, lockfile regenerated (never hand-resolved)money-path-checklist.mdrun; payout logic compared against the other side