Newlay's Blossom enabled on the relay box (blobs inside the data volume); Caddy + DNS + TLS wired. The app now uploads there first, keeping blossom.primal.net and blossom.band as mirrors, and includes it in BLOSSOM_MIRROR_HOSTS for read fallback.
Verified the legacy relay held the same 115 events as the new one (0 diff) before removing it, so no data is lost. Drops wss://goskatespots.nostr1.com from the default pools, the DM/CVM/zap relay arrays, and the cvm-server + push-service defaults.
Point SKATE_RELAY, the default relay pools, the DM/CVM/zap relay arrays, the cvm-server + push-service defaults, and the archive default at wss://relay.skate-spots.org, keeping the legacy hosted relay as a fallback during the client-update window. Restored 115/115 archived events; the relay runs behind Caddy with TLS. Docs + AGENTS.md updated.
New data-driven Lab craft, mirroring the artist role: three skill
sections (What You Make [required] / How You Make It / Materials) plus
minimum order, turnaround, size range, samples and customisation extras,
inheriting the shared Lab extras (rate, payment, gallery, reel,
languages, availability, booking).
Wired through types.ts (kind, label/description maps, parse, collective-
capable), labelKeys, useLabProfiles (kind list + dispatch), becomeRoles
(role card), craftFields, LabPage (tab, filters, offer button, empty
state, counts, deep links), LabCrewsStrip, NIP.md, and en/es i18n
(labOptions, becomeRolesUi, labUi). /become/clothing added to the smoke
route list. tsc, eslint, check-i18n, vitest 101/101, build and smoke all
pass.
Finishes the escrow-mint work: advertiseEscrowKey now also publishes
cashu_mint (the seller's preferred mint from Wallet > Escrow) on kind 0,
documented in NIP.md. The buyer's EscrowRequestDialog reads the seller's
cashu_mint, includes it in the mint picker even if it is not in the buyer's
trust list, and prefers it (falling back to the buyer's preference, then
first trusted mint). Funds locked at a mint the seller actually uses are
the ones they can redeem. Still open: seller-side receive from a mint they
do not already have. New marketUi.escrowRequest.sellerMintHint (en/es).
- usePayPaymentPointer now checks the seller's committed payment_hash (from
the accepted offer) against the invoice actually paid; a swapped pointer
is rejected before any payment.
- PayWithSats no longer offers an in-app sats Pay button for a non-SATS
offer: a fiat amount is never paid as if it were sats. New
marketUi.pay.nonSats (en/es).
- ListingOffersPanel passes paymentHash + currency through.
Docs updated (still open: NWC->WebLN/Cashu fallback, empty preimage).
The escrow mint was silently the buyer's preferred/first trusted mint
(useCashuEscrow: prefs.preferredMintUrl || mints[0].url) with no UI, even
though the seller redeems there. Now:
- the escrow dialog shows the mint and lets the buyer pick from their
trusted mints (defaults to their preference);
- a mint whose NUT-06 info advertises no NUT-11 (P2PK) support is blocked
with a clear message before minting (cashuEscrow.mintSupportsP2PK);
- the create call uses the selected mint.
Still open (scoped separately): consult a seller-advertised mint and
seller-side receive from an unfamiliar mint. Recorded in the audit.
Next batch from MARKETPLACE_FLOW_AUDIT.md:
- bolt11.ts: getBolt11Expiry / isBolt11Expired; usePayPaymentPointer refuses
an expired invoice before paying (a lapsed offer invoice used to fail deep
in the wallet with a confusing error).
- PayWithSats shows unsupported pointer kinds (creq/clink) as unavailable up
front instead of enabling a button that fails after the click.
- useTradeAttestations: verify events (onlyVerified) and keep the newest per
(author, kind, d), so a newer s=revoked supersedes a stale valid copy;
useTradeReputation no longer trusts unverified relay events.
- EscrowRequestDialog persists the minted token locally before publishing
(lib/pendingEscrow.ts), so a failed send cannot strand the locked funds.
Deep audit (MARKETPLACE_FLOW_AUDIT.md): full trade/escrow/offers lifecycle,
severity-ranked findings, and a state-coverage matrix.
Fixes:
- Grid Mark as sold now opens MarkSoldDialog, so the kind-31871 attestation
(buyer, method) is recorded from the grid too, not just the detail page.
- Offers accept attaches a payment pointer only when the accepting user is the
listing owner (seller); previously the buyer could publish their own lud16 as
the payee and misdirect payment.
- advertiseEscrowKey refuses to publish kind 0 before the profile loads, so it
cannot overwrite name/picture/lud16 with a cashu_escrow-only profile.
- useListingOffers passes requireAny, so the offers panel error state actually
fires on an outage (previously an outage looked like no offers).
- Offer messages invalidate the offer queries, so accept/counter/decline update
immediately instead of after the stale window.
P0 fixes from UX_AUDIT_2026-10-09.md:
- useMarketplaceItem now filters admin-hidden listings and skips
showcase-only (not-for-sale) items, so a direct /listing/... URL cannot
surface removed or non-sale content.
- Listing detail hides Make an offer for logged-out users and for
fixed-price listings (Buy is the single primary CTA there).
Borrowed from the Plebeian market review: a /marketplace-guidelines page
(peer-to-peer, buyer-beware, no checkout, prohibited items, soft
moderation), linked from the marketplace hub; new marketTerms en/es keys.
Records the remaining borrowables in the audit doc.
A lone friend-of-a-friend no longer reads as trust. useViewerGraph now
counts distinct follows per friend-of-a-friend (fofCounts), and
deriveLocalScore returns verified only at FOF_MIN_RATERS (2) or more.
Provider assertions are unaffected; the full GrapeRank confidence model
stays provider-side. Docs updated.
Fifth slice of TRUST_SCORE.md. useNetworkAlerts reads the viewer's NIP-85
assertions for the accounts they follow and surfaces those trusted people
reported or muted. New /alerts page lists them with Ignore / Unfollow /
Mute / Report (reusing useFollowActions, useMuteUser, ReportDialog) —
inform, never auto-act. Settings -> Trust links to it with a live count;
ignored accounts are device-local. /alerts added to the smoke list.
Completes the safety loop. useMuteList reads the viewer's kind-10000 mute
list plus optimistic local mutes (lib/localMutes.ts); useMuteUser
republishes the list with the target added, records it locally and
invalidates content queries. useAdminModeration carries a new
mutedPubkeys set, and the shared filterAdminHidden/filterAdminHiddenMixed
drop muted authors — so every list hook filters them with no per-hook
changes (admin tooling with includeHidden still sees them). The flagged
notice gains a Mute action.
TrustBadge called useTrustScore per card, which fetched the viewer's
follow list plus up to 100 friends' follow lists for EVERY card. The
graph does not depend on the target, so useViewerGraph computes it once
per viewer (one shared React Query) and deriveLocalScore returns the same
100/90/60/0 ladder (the NIP-05 30 tier classified as unknown anyway).
TrustBadge now costs no per-card network work on top of the batched
assertion read. Behaviour preserved; logged-out users still use the house
graph. Also records the ANALYZE=1 bundle findings in AGENTS.md.
Settings -> Trust is now functional, not read-only. useSetTrustProvider
publishes a kind-10040 designation for a custom provider (npub or 64-hex +
wss relay, the five 30382:<type> rows) and can clear it to fall back to the
community provider. The card validates input, prompts the signer, and
invalidates the provider + assertion queries so scores refresh. Docs
updated.
- Settings -> Trust section (?section=trust, TrustSettingsCard): a
show/hide-marks toggle (useTrustDisplay; Flagged is never hidden) and the
active provider + relay (useTrustProvider), linked to /trust.
- TrustBadge respects the display preference.
- Name AGPL-3.0 on the License page by updating its existing copy
(licenseLabel + body) in en/es.
- Settings + card i18n keys; /settings?section=trust added to the smoke
list. Docs updated.
Second pass of TRUST_SCORE.md.
- useTrustAssertions now batches: every TrustBadge registers its subject into
a shared per-viewer union and ONE query fetches the union (no per-card
query), with a debounced refetch when a list grows. Same hook API.
- A direct follow (or self) is no longer shown as Verified - only a
friend-of-friend or a provider assertion counts; Flagged is never
suppressed.
- useUserReports reads public NIP-56 reports for an account, and
TrustFlaggedNotice shows a flagged counterparty with the reporters grouped
by the viewer's network plus Report / Dismiss. Wired into the listing
seller card and the coach detail page.
- New walletUi.trust.* keys (en/es).
First slice of TRUST_SCORE.md.
- lib/nip85.ts: parse kind-10040 provider designations, resolve a provider
with a house fallback, parse kind-30382 assertions (rank/followers/
reporters/muters/hops; absent hops means no path, absence is not
distrust), and author-check the provider.
- hooks/useTrustAssertions.ts: batch read from the resolved provider,
signature-verified via onlyVerified + the author check.
- lib/trustLadder.ts: three buckets and thresholds (flagged = trusted
reporters >= 2; verified rank >= 2).
- hooks/useTrustBucket.ts: prefers a NIP-85 assertion (verify + flag),
falls back to the local WoT ladder (verify only).
- TrustBadge.tsx: now renders Verified / Unknown / Flagged with glyph,
colour and tooltip; Unknown hidden by default, Flagged always shown.
Reaches the map/tour/event cards, marketplace card, listing detail and
coach detail through the shared component.
Open follow-ups (in TRUST_SCORE.md): batch the bucket at list level, a
'who reported' breakdown, a provider-choice setting, alerts, confidence
scoring.
A NIP-09 kind-5 delete is only honoured by relays and was never applied
locally, so on refresh the persisted React Query cache and the Dexie spot
cache re-rendered a just-deleted spot for a few seconds until fresh data
(or the admin hidden-label query) filtered it out.
Add src/lib/localDeletions.ts: a device-local tombstone keyed by coord
and event id, written optimistically by useDeleteEntity on the owner path
(rolled back if signing/publishing fails). The shared filterAdminHidden/
filterAdminHiddenMixed now also drop tombstoned items, so every list hook
filters them from first render with no per-hook changes. useSkateSpot
returns null for a tombstoned spot, and cacheSpots/cacheSpot refuse and
evict tombstoned rows. Prune by tombstone only (callers use limits 8-2000,
so pruning by 'not in fresh result' would wipe valid cached spots).
- getBtcRates()/useBtcRates() read every BTC→fiat rate from the ONE Coinbase
exchange-rates call the app already made, plus sats (1e8). No new endpoint.
- The calculator's currency control is now a searchable Popover: sats + every
fiat Coinbase reports, with a search box. Selecting a currency CONVERTS every
money field (labour, rent, utilities, subscriptions, equipment cost, material
order/shipping, packaging materials) by the live ratio; unitless fields
(overhead factor, board counts, minutes, margins) are untouched. Falls back to
relabel-only if rates aren't loaded.
- Currency symbols/formats for the common fiats.
New /diy hub: curated resources for making your own gear, with cross-links into
the rest of the app.
- src/lib/diyGuides.ts: seed data — categories (Boards, Ramps & wooden
obstacles, Concrete & DIY spots, Repairs & tuning, Tools & plans), resource
cards (Open Source Skateboards / SK8CAD / Instructables / YouTube), and an
actions block (find a Builder, find a DIY crew, map a DIY spot, sell builds,
find a coach).
- src/pages/DiyPage.tsx renders it (category chips + card grid + actions);
external links go through sanitizeUrl and credit the source.
- Route /diy (lazyWithRetry) + a nav-catalog entry (id diy) so it appears in the
desktop "More" menu and the mobile burger; also linked from the site footer
and the map links strip. Smoke test walks /diy.
The desktop footer is hidden on full-bleed map routes, leaving the
Newsletter/License/Privacy/Collaborators links (and the "not sponsored by
OpenSats" note) unreachable from the homepage. MapFooterStrip overlays them
inside the map container without stealing map height:
- Desktop: a slim full-width bottom bar.
- Mobile: a tiny collapsed "Links" chip that expands to a compact, scrollable
row.
- Dismissible (useMapFooterHidden, device-local, same-tab synced) and
restorable from Settings -> Appearance.
- The map's OSM attribution moved up (md:bottom-7) so the bar doesn't cover it.
The /lab profile card and /stats Top Mappers were overflowing off the right
edge: Tailwind `grid md:grid-cols-2` (no base column) renders one implicit
`auto` track on mobile that can exceed the container when a name is long, so the
whole card got pushed off-screen. Added the base `grid-cols-1` repo-wide (plus
`min-w-0` on truncating flex/grid children).
/stats also gained a "Top Shops — most shops added" leaderboard (kind 30017)
beside Top Mappers (spots): `topShops` in useGrowthStats, a generalized
MapperRow (count + unit), and EN/ES copy. PERSIST_BUSTER → v6 (new persisted
field).
- useEscrowPrefs (device-local): default lock window, preferred escrow mint,
require-escrow default for new listings.
- EscrowSettingsCard on the Wallet (native-only, beside EscrowKeyCard).
- EscrowRequestDialog now defaults its lock window + funding mint from the prefs.
- Listings gain an optional ['escrow','required'] tag: a toggle on
CreateListingPage (defaulted from the pref) and a 🔒 badge on the card.
Addresses "the floating Sasquatch flashing is annoying":
- Pulse is now state-driven: the animate-ping halo shows only when there's an
unread tip (message), never perpetually; the unread dot matches.
- Honors prefers-reduced-motion (motion-reduce:hidden).
- Map-aware: Layout passes isFullBleed, so on the map the bubble runs compact
(smaller, no glow, no pulse) instead of competing with map controls.
- Hideable: a ✕ on the FAB hides it (useSasquatchHidden, localStorage
sasquatch-hidden, same-tab synced via an event), with a "Show Sasquatch
assistant" toggle in Settings → Appearance to bring it back.
- useLabDraft persists the form snapshot to localStorage (content-debounced)
and only offers a saved draft as resumable when it differs from the current
state, so opening an existing profile never shows a spurious resume banner and
a draft can't silently clobber a freshly loaded profile.
- LabDraftBanner offers Resume / Discard; wired into CraftProfileForm (8 crafts)
and the coach + filmer/editor/photographer/builder pages.
- The draft clears on a successful publish.
Every provider lived at /coach/<pubkey>/<id> regardless of craft (a barber got
a "coach" URL). New src/lib/labRoutes.ts maps craft kind → segment and builds
the canonical path; the router adds /lab/:craft/:pubkey/:identifier (the craft
segment is cosmetic — the page still resolves by pubkey + identifier) and keeps
/coach/... working for old links. All generated links updated: offer-form
redirects, Lab cards + "Your Lab profiles", cross-links, spot/event/admin links,
the CVM client URL, the share path, and the SPA clean-link shim.
- New `useBatchSpotCommunityScores(spots)` — ONE query for many spots (same
signals/weights as the single-spot hook; counts distinct accounts, drops the
mapper, caps per account; skip WoT/activity weighting at this scale).
- SpotCard takes an optional `community` prop and shows a small "Community"
(green) or "Seen" chip in the footer.
- MapPage scores the visible spot cards (shops excluded — different kind) and
passes each card its score.
- useSpotCommunityScore now also reads crews (35006, `a` ref, weight 3) and Lab
profile links (35001-35014, `a` ref, weight 2) as effortful signals — a whole
crew listing the spot, or a Lab pro linking it, is strong evidence. A crew
ref alone reaches "community". (Tours/events use geohash, not a spot coord,
so they're not counted.)
- Spot page: on an unconfirmed spot, a logged-in non-owner now sees a "Be the
first to confirm this spot" nudge with Add-photo + "I've skated here".
- Breakdown line/tooltip include crew + Lab counts.
New `useSpotCommunityScore(spot)` — ONE batched relay query covering photos,
status reports, comments, reactions, zaps, collections and tag suggestions for
a spot, then a distinct-account, tier-weighted score:
- counts DISTINCT accounts and never the mapper's own signals;
- weights costly signals higher (zap 3, photo/status 2, comment/reaction/
collection/tag 1) and caps each account's contribution at 3;
- multiplies by the actor's web-of-trust + real-activity (reusing
useBatchTrustScores/useBatchActivity) so a ring of empty accounts can't move it;
- returns level (unverified | seen | community) + a breakdown.
SpotDetailPage now derives the Community-verified badge from this (not just
photos+status) and shows a small "why" line (e.g. "2 photos · 1 zap · 3
comments") under the badges. No new event kinds.
A NIP-05 name maps to one key, so "who owns it" decides where it lives:
- Crew: repeatable nip05 tags (up to 5), verified against the founder key.
- Shop: same on kind 30017, verified against the owner key (new field).
- Profile: kind-0 has ONE standard nip05; extra personal handles live in a
separate NIP-78 record (kind 30078, d=skatespots:profile-handles) so another
client re-saving the profile can't wipe them.
Shared pieces:
- components/Nip05Badges.tsx — generic verify-and-render chip row (any owner).
- components/Nip05HandlesEditor.tsx — shared add/remove editor with live
verified/mismatch status; used by profile, crew and shop forms.
- CrewNip05Badge now delegates to Nip05Badges; CrewForm uses the shared editor.
- Profile page shows primary + extra handles, and each crew/shop chips its own
verified handle.
- lib/profileHandles.ts + hooks/useProfileHandles.ts (read/publish the record).
- types: SkateShop gains nip05/nip05s; parseSkateShop parses repeatable nip05s.
- NIP.md + AGENTS.md + i18n (EN/ES) updated.
H14 (NWC double-pay): after the 15s pay() timeout, useNWC now polls
lookupInvoice to confirm settlement before reporting failure; if the status
stays unknown it throws a coded error (PAYMENT_STATUS_UNKNOWN) and the callers
that used to fall through to another rail (useZaps, usePurchasePro,
PlatformFeePayment) now STOP and tell the user to check their wallet — no
automatic second payment.
H1 (spot claim): claim-from now stores the ORIGINAL COORDINATE
(30818:pubkey:d) rather than an ambiguous bare pubkey; SkateSpot parses
claimedFrom/claimReason; a claimed listing shows a 'Claimed listing' notice so
the duplicate is understandable. NIP.md updated. Full reader-side supersede is
documented as the remaining design item (a wrong rule could hide real spots).
A ['featured', expiry] tag is self-asserted, so anyone could mint one and buy
top placement (the badge verified it, ordering didn't).
- New src/lib/featuredVerification.ts resolves the provider pubkey (cached) and
batch-checks provider-authored kind-9735 zap receipts for the featured coords.
- useTours / useSkateEvents / useUpcomingSkateEvents / useLabProfiles now rank
only receipts-backed featured items first.
- Availability fallback: if the provider can't be resolved or the query fails,
the self-asserted ordering is kept, so a provider outage never blanks paid
placement.
- H13 offer threads: the seller is ALWAYS the listing author (derived from the
event pubkey), never a self-declared role tag — a non-owner can no longer
appear as the seller with a payment pointer.
- H12 trade reputation: a preimage/deposit proof counts at most once, so a real
payment can't be reused across attestations to inflate payment-verified.
- Crew alerts: listed crews are filtered against the user's own membership
confirmation labels (one batched query), so a confirmed member no longer gets
a phantom 'confirm to join' badge/notification when the crew is edited.
- Error boundaries (app + route) and DesktopNavManager are translated; the
class boundary renders a translated functional ErrorFallback. New batch4
namespaces errorsUi.* / desktopNav.*.
- 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.
- useMarketplaceItem / useSkateEvents now pass requireAny, so a total outage
throws (isError) instead of silently returning [].
- ListingDetailPage and EventsPage render the shared ErrorState with a retry
instead of a false 'Listing not found' / 'No upcoming events'.
- H2 useAdminModeration now fetches the 'cleared' label too, so an admin Undo
actually propagates (newest-verdict-wins) instead of only working on-device.
- H3 admin takedowns publish the NIP-32 'hidden' label (useAdminHide) instead of
an author-only kind-5 relays ignore; the cross-app hide is real now.
- H5 the /stats outage banner works: queryCanonical reports relay failures via
onError, so 'partial' can be true and zeros are never presented as truth.
- H6 useShops got its own cache namespace (was colliding with useSkateShops).
- H7 listing NIP-05 is verified against the user's key, not merely self-asserted.
- H8 NIP-22 spot comments/transfer now use uppercase root tags (A/K/P); readers
accept legacy #a; NIP.md corrected.
- H9 Lab profile edits preserve foreign spot refs (crew link = 35006 only).
- H10 collection edits preserve foreign NIP-51 tags.
- H11 LAB_KINDS = all 13 provider kinds (was 35001-35005).
- H16 skip-to-content is a button that focuses <main> (was href=#main, which
HashRouter treated as a route → 404).
- H17 CustomizeNavSheet is translated; useLocalStorage re-reads on key change and
keeps stateRef in sync cross-tab.
- Backup 'confirmed' flag is now per-account (backup-confirmed:<pubkey>).