Skip to content

feat(profile): show a holder what was taken from them and what they e… - #4

Merged
Samuel1-ona merged 1 commit into
mainfrom
samuel1-ona/raid-payouts-invisible-to-the-raided
Aug 30, 2026
Merged

feat(profile): show a holder what was taken from them and what they e…#4
Samuel1-ona merged 1 commit into
mainfrom
samuel1-ona/raid-payouts-invisible-to-the-raided

Conversation

@Samuel1-ona

Copy link
Copy Markdown
Contributor

…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.tsfetchRaidsAgainst(), 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.

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

  • none

Checklist

  • Title is the commit message I want on main
  • Ships the test that fails on pre-fix code; fixtures are real captured data; fakes honour their arguments
  • Covers the seam, not only pure functions; any stated guarantee tested on its failure path
  • Error responses audited for what they leak; reads on write paths bounded at the query
  • Judgement calls / bundled product changes flagged above (or "none")
  • lint / typecheck / test pass locally on the current head
  • Re-read acceptance criteria of every Closes issue — all met
  • Development sidebar links match Closes/Refs above
  • README / runbook / .env.example / examples / error strings updated for the world this creates
  • Lockfile touched → rebased on current main, lockfile regenerated (never hand-resolved)
  • Wallet/provider tree touched → loaded in a normal browser, not only Nimiq Pay
  • Money/security path → money-path-checklist.md run; payout logic compared against the other side
  • No secrets in the diff
  • Questions for the maintainer marked clearly at the end (or "none")

…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
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
terreno-web Building Building Preview Aug 30, 2026 3:39pm

Request Review

@Samuel1-ona
Samuel1-ona merged commit f24b325 into main Aug 30, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant