- Tour sponsor URL/logo sanitized before href/src (SponsorSlot) — same XSS
guard EventSponsors already uses.
- Official SkateSpots badge only shows when the NIP-05 actually verifies
(NIP19Page), not on a self-asserted handle.
- Community stats reads are null-guarded on persisted-cache fields
(monthly/cumulativeSpots/topMappers) — no more crash on an old cache.
- Overpass response no longer assumes .elements (food layer).
- user-listings query key is memoized (was Date.now() → refetch storm).
- Pro grant requires the l=pro label (was accepted with none).
- Verify card RSVP maps accepted/tentative (was going/interested → showed
'No RSVP' on a valid ticket).
- Expired listings can't be bought via a stale/shared URL.
- Batch spot photo/status hooks include the relay suffix and use
querySkateContent (relay-policy parity with useSpotReports).
- Offline queue requeues rows stuck in 'publishing' after a crash.
- querySkateContent/queryCanonical dedupe replaceable events by author+kind
(not id) so a stale profile/relay list can't win.
- Route offline cache key includes the origin (was destination+mode only).
- Internal links use <Link> (MarketplacePage); EventCountdown reacts to
language changes.
- First-visit language auto-detect (Spanish devices land in es; stored choice wins).
- Sasquatch AI replies in the app language (skate trick names kept English).
- i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t()
key missing from en/es or any en/es key drift.
- ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and
~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace,
trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters.
- Policy: only skate TRICK names stay English; user-generated content untouched.
A pure swap has no invoice, so ListingTradeActions now offers a 'Good-faith
deposit' input for price_type=trade listings. Paying it stores a deposit-slot
receipt (saveReceipt(coord, r, 'deposit')); the buyer's confirm attaches it as
['proof','deposit',preimage,payment_hash] with method=trade, so a barter carries
a verifiable COST without pretending the goods were proven.
- getPaymentHashFromBolt11 (bolt11.ts, dependency-free; matches the BOLT11 spec
test vector) + verifyPreimage (sha256(preimage) === payment_hash).
- usePayPaymentPointer captures the preimage from NWC/WebLN/melt and returns a
PaymentReceipt; PayWithSats.onPaid carries it; stored device-locally so the
later confirm step attaches it; publishTrade writes
['proof','lnpreimage',preimage,hash] (and ['proof','deposit',...] for barter).
- useTradeReputation verifies and counts it in paymentVerified.
- Honest scope: proves PAYMENT, not delivery; money legs only - a pure hardware
swap has no invoice, stays attested; a deposit receipt gives barter real cost.
- HandoffHelper suggests the 3 nearest public skate spots to the listing as
one-tap DM meetup templates (public data only; never the viewer's location).
- New addressable kind 34967 + useShipmentStatus + ShippingStatusCard: seller
marks shipped, buyer received, either issue. p-tagged so the push service
notifies (added to MARKET_KINDS); d=ship:<listing-d>:<author> so a status
replaces only its author's.
- Honest framing: a self-reported claim, NOT proof - 'received' does not release
escrow; no address stored; parcel photos with an address discouraged.
- ['shipping','buyer'|'seller'|'split'] tag + create-page selector.
- Distance from the viewer's last known location (cache only, no GPS prompt) on
both gear cards, and a 'Nearest (local)' sort on GearSwapPage.
- HandoffHelper (buyer-only, in ListingTradeActions) opens the trade DM
pre-filled: suggest a time & place / share delivery details / use a locker,
with anti-scam copy. Details stay in the NIP-17 DM; the app never sees them.
- ships deals now nudge safety on all platforms, not just when escrow is ready.
- Listings carry optional ["fulfilment","pickup"|"ships"|"both"] (selling only):
a create-page selector (default pickup), a card badge, and a Pickup/Ships filter
on GearSwapPage. The app NEVER collects an address - a ships deal exchanges it
privately in the NIP-17 trade chat, and ListingTradeActions nudges escrow for
shipped deals. No KYC rail, no labels, no carrier API.
- TradeReputationCard adds a low-diversity ring warning (verified >= 5 across <= 2
people) beyond the single-partner one.
A price_type=fixed listing now shows a direct 'Buy with sats' button in
ListingTradeActions (card + detail) - no negotiation needed. The price resolves
to sats (SATS directly, USD via useUsdToSats) and pays through PayWithSats (NWC ->
WebLN -> Cashu melt, falling back to the seller's lud16). The sheet mounts only
while open so the wallet hooks stay off every grid card. Reuses the accepted-offer
Pay rail.
- Mints page: 'Online only' toggle + result count ('N of M mints')
- Wallet page: 'Browse Cashu mints' link to /mints
- ListingTradeActions now renders the EscrowPanel for a viewer's active escrow,
so escrow status shows on both the grid card and the detail page
- AGENTS.md project map: marketplace detail page + shared actions
Gate: 101 tests, 0 errors/warnings, boot smoke green.
- New '/listing/:pubkey/:identifier' page: large gallery + lightbox, full
description, details, seller trust + trade reputation card, and full actions
(shared) for buyers and owners (mark sold/relist/delete)
- Extract ListingTradeActions (confirm/escrow request/ask-to-confirm/refund/
contact + escrow dialog) and use it in BOTH the grid card and the detail page
so there is one implementation
- useMarketplaceItem(pubkey, identifier) fetch hook
- Cards link their image + title to the detail page
- Smoke test covers /listing/:pubkey/:identifier
Gate: 101 tests, 0 errors/warnings, boot smoke green.