- useCVMClient: add findCrews (queries kind 35006 across data relays, filters by
city/country/vibe/search, returns CrewResult with member_count, vibes, url);
add crews to getCommunityStats + CrewResult type
- SasquatchGuide: register find_crews tool in system prompt + switch handler,
add crew keywords to needsLiveData, update app-facts (crews are searchable +
mention cross-linking and Zap-the-Crew)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useCrewZap hook: splits a total evenly across recipients, resolves each
member's Lightning address (batched kind-0 query), fetches an invoice per
recipient and auto-pays via NWC → WebLN; per-recipient status
(resolving/paying/success/skipped/failed) with manual-invoice fallback
- splitAmount util distributes remainder fairly (no lost sats)
- ZapCrewDialog: amount presets + custom, per-head preview, live progress rows,
copy-invoice fallback when no wallet; excludes the current user
- CrewDetailPage: "Zap the Crew" button (recipients = founder + confirmed
members via useCollectiveMembers); shown only when >1 recipient
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Nutzap:
- Gate sending behind NUTZAP_SENDING_ENABLED=false with clear docs; sending
would require P2PK-locking proofs (fund-loss risk if wrong), so it's disabled
until implementable/auditable. Receiving + reading unchanged. No dead-end
button exists (hook was never wired to UI).
Cross-linking crews ↔ Lab collectives:
- types: parseAddrCoord/parseCrewRef/parseLabRefs helpers + LAB_KINDS; add
crewRef to all 5 Lab interfaces/parsers and labRefs[] to SkateCrew
- CrewLinkField (pick one of your own crews) added to all 5 Become-* forms,
shown only for collectives; publishes an a-tag on the Lab profile
- LabLinkField (pick your own Lab profiles) added to CrewForm; publishes a-tags
- CrossLinkCards: LinkedCrewCard on Lab detail ("Part of a Crew"), LinkedLabCard
on crew detail ("Hire This Crew"), resolving coordinates via useCrew/useLabProfile
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Phase 1 CLINK integration (recognize & display only):
- types.ts: optional noffer field + isValidNoffer guard on SkateCoach,
SkateFilmer, SkateEditor, SkatePhotographer, SkateShop; parsers read it
- NofferField: reusable opt-in input with live validation, added to all 4
Become-* Lab forms and both Create/Edit shop forms
- ClinkPayButton: secondary "Pay via CLINK" action (QR + copy + nostr: deep
link), rendered on CoachDetailPage and ShopDetailPage only when a valid
noffer is present; never replaces the existing zap / Lightning Address flow
- useQRCodeScanner: classify scanned noffer1/ndebit1 CLINK codes
- NIP.md: document the noffer tag on Lab kinds + shops and a CLINK section
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
New Photographers category (blue-indigo) — separate from Filmers:
- New addressable kind 35004 + SkatePhotographer type + parseSkatePhotographer.
- Rich photographer taxonomy as queryable `t` tags: services (action,
sequence, editorial, product, events…), camera bodies, lenses, lighting
(Profoto/Elinchrom/Godox/triggers…), support/rig, and shooting style,
plus a freeform "gear" tag and a portfolio link.
- BecomePhotographerPage form with chip-grid selectors for all gear
categories + "what's in the bag" textarea, wired to /become-photographer.
- The Lab: 4th "Photo" tab (indigo accent) with its own filters, cards,
empty state; OfferFAB gains "Offer Photography"; useLabProfiles fetches
35004; CoachDetailPage renders photographers (and editors) properly with
per-type accent, gear + portfolio display.
- NIP.md: documented the whole Lab (kinds 35001-35004) incl. photographer.
Filmer improvements (now video-focused, photography split out):
- Removed the token "Action Photography" service; added follow/line filming.
- Richer equipment list: cinema camera, HVX, action cam, gimbal, 4K/HFR.
- Filmers gain a freeform "what's in the bag" gear field + a reel/portfolio
link, both surfaced on the detail page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Sasquatch panel no longer auto-opens on the map (/) or The Lab (/lab).
Added an `autoShow` flag to GuideMessage; only opt-in messages (onboarding)
auto-open. The bubble stays minimized until tapped, and context messages
still populate the panel when opened manually.
- Map CARTO/OSM attribution now sits above the mobile bottom nav (safe-area
aware) instead of overlapping the Shops & Spots buttons; made it smaller
and more subtle. Desktop keeps it in the bottom-left corner.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Architecture:
- usePlatformFee hook: central fee config (featuredEvent/labProfile/featuredShop)
5,000 sats each, 30-day validity, localStorage persistence, owner bypass
- PlatformFeePayment component: generic reusable payment gate
auto-pay via NWC/WebLN, QR fallback, active/expired state display
all fees route to skatespots@minibits.cash
Events (CreateEventPage):
- New optional "Entry Fee" field
- Triggers 5,000 sat platform fee when filled
- Stores entry-fee + paid tags on published event
- Free events: zero friction unchanged
The Lab (Become{Coach,Filmer,Editor}Page):
- Triggers 5,000 sat fee when "Rate" field is filled
- Submit disabled with clear CTA until fee paid
- Free/volunteer profiles: zero friction unchanged
Rule: "pay only when you earn"
- No rate set → publish instantly, no fee
- Rate set → 5,000 sats to SkateSpots, valid 30 days
- Platform owners always bypass
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- FABs (add spot/shop): switch from fixed bottom-[88px] to
calc(3.5rem + env(safe-area-inset-bottom, 0px) + 0.75rem) so they
respect device safe-area and always clear the bottom nav on all
Android/iOS Capacitor builds
- Right-side filter pills: same fix, switched from absolute to fixed
with the same safe-area-aware bottom value
- useSasquatchGuide: /map page now returns undefined message, meaning
Sasquatch never auto-opens when navigating to Shops & Spots — user
must tap the floating bubble to open it manually
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NUser takes (method, pubkey, signer) as separate args.
Was passing {pubkey, signer} as first arg so pubkey was always undefined.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NLoginOther requires type x-${string} to be a valid NLoginType.
Using bare 'amber' meant pubkey was never properly stored/rehydrated
from localStorage — it was always undefined/empty.
Using NLogin('x-amber', pubkey, ...) correctly stores pubkey through
the NLoginBase interface and survives localStorage serialization.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NLoginType doesn't expose pubkey field directly — amber login is cast
as unknown as NLoginType so l.pubkey was undefined. Use getPubkey()
helper that casts to extract pubkey safely for all login types.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
WebSocket connections take longer to establish on first load in
Capacitor. Increase timeout from 8s to 15s and retries from 3 to 5.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add CapacitorAmberSigner: a full NostrSigner implementation backed by
the native nostr-signer-capacitor-plugin (login, signEvent, nip04,
nip44 all go through Amber via native Android intents)
- LoginDialog: on Capacitor, call getAmberPublicKeyViaPlugin() directly
instead of the intent:// URL redirect — returns a Promise, no
callback URL or polling needed. Shows clear error if Amber not installed.
- useCurrentUser: on Capacitor amber logins use CapacitorAmberSigner
so every publish/sign goes through the plugin, not window.location.href
- Web/PWA path unchanged — still uses URL redirect + AmberSigner
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The entire nostrconnect relay approach was wrong for a native Android/Capacitor app.
Amber on Android uses NIP-55 intents (nostrsigner: URI scheme), not NIP-46 relays.
New flow (same as Amethyst):
1. openAmberForLogin() fires nostrsigner:?type=get_public_key&callbackUrl=...
2. Amber opens, user taps approve
3. Amber calls back to org.skatespots.app://amber-callback?event=<pubkey>
4. AmberCallbackPage writes pubkey to localStorage('amber_pending_pubkey')
5. LoginDialog polls localStorage every 500ms and calls login.amber(pubkey)
6. Done — no relays, no waiting, no spinning
Also added login.amber(pubkey) method to useLoginActions.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Completely replaces the broken NIP-55 intent approach with NIP-46:
- Amber tab: generates nostrconnect:// URI, opens Amber, waits for
Amber to connect back via relay using NLogin.fromNostrConnect()
(same pattern as Ditto/soapbox-pub)
- Bunker tab: paste bunker:// URI from Amber or nsec.app
- Extension tab: NIP-07 browser extension (desktop)
- useLoginActions: updated to match Ditto's implementation with
nostrconnect(), generateNostrConnectParams, generateNostrConnectURI
- Clean mobile-first UI — no clipboard hacks, no intent tricks
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Raise QUERY_TIMEOUT in useSkateSpots from 8s to 15s
- Raise AbortSignal.timeout in useLabProfiles and useSkateEvents from 10s to 15s
- Add useSkateRelay to AppConfigSchema so it's preserved when reading from localStorage
On a fresh phone (no warm WebSocket, no TanStack Query cache), the app
needs to: load JS bundle, boot React providers, open a WebSocket to
goskatespots.nostr1.com, and receive EOSE — all before the timeout.
On slow mobile connections this easily exceeded the old 8-10s limits,
causing /home to show empty sections while /map worked (the connection
was already warm by then). 15s gives cold connections enough headroom.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
New useGrowthStats hook:
- Queries wss://goskatespots.nostr1.com directly (authoritative source)
- Fetches all 7 content types in parallel: spots, shops, events,
coaches, filmers, editors, tours
- Deduplicates addressable events (keep newest per pubkey+kind+d)
- Buckets events by month with gap-filling for continuous X axis
- Computes cumulative spots line chart data
- Extracts top 10 mappers by spot count
- 15 min stale time — stats don't need to be live
New useGeyserStats hook:
- Queries Geyser GraphQL for supporter count + total sats raised
- Reuses same API already used in SupportersPage
Rewritten CommunityStatsPage:
- Section 1: 8 hero stat cards (spots, shops, events, coaches,
filmers, editors, tours, mappers) with per-type accent colours
- Section 2: 4 charts (cumulative spots area, monthly stacked bar,
active mappers bar, content mix donut)
- Section 3: Top 10 mappers leaderboard with avatars + 🥇🥈🥉
- Section 4: Supporters banner with live Geyser count + sats raised
- Footer: data source + verifiability note
- Full skeleton loading for each section independently
- Error state for relay connectivity issues
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- AppRouter: lazy load all 40+ pages (only Index is eager)
→ initial JS bundle no longer includes code for pages never visited
- MapPage: lazy load SpotMap/Leaflet (~150KB) behind Suspense
→ Leaflet only downloads when user navigates to /map
- useSkateSpots/useShops/useMarketplace/useTours:
→ timeout: 30s → 8s (fail fast, show error sooner)
→ staleTime: 30s → 5min (trust cache, fewer relay round-trips)
→ gcTime: 5min → 30min (keep data in memory longer)
→ refetchOnMount: 'always' → true (use cache when data is fresh)
- Remove console.log/warn/debug spam from production hooks
- vite.config: manual chunk splitting for leaflet, nostr-tools, bitcoin
libs, radix-ui, react-router — each chunk cached independently
by browser, only re-downloaded when that library updates
- vite.config: esbuild drops console.log/warn/debug in prod build
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root causes found:
1. syncedUsers blocked re-sync permanently even on failure — first attempt
fires before WebSocket connections are ready on mobile, fails silently,
gets marked done, profile stays anonymous forever that session
2. useAuthor had 5min staleTime — empty {} result from pre-login query
was cached and not refetched after login
3. NostrSync did not force-remove the stale useAuthor cache entry
Fixes:
- Rewrite NostrSync with proper retry logic (up to 5 attempts, backoff)
- Tracks success/failure per pubkey, only stops on confirmed success
- Re-triggers on user.pubkey change (catches Amber polling completing)
- Forces queryClient.removeQueries on author cache before each attempt
- Tries Citrine → bootstrap relays → main pool in order
- useAuthor: reduce staleTime 5min→1min, expand fallback relay list
- Both files now use same BOOTSTRAP_RELAYS set for consistency
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The encrypted CVM request/response cycle (kind 21000/21001) was too unreliable
in the browser — timeouts on every call. Replace with direct Nostr relay queries
using nostr.group() which is already proven to work in the app. Same filtering
logic as the server. No dependency on CVM server being up for data queries.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
nostr-tools v2 SimplePool.subscribeMany() callbacks (onevent/oneose) no
longer fire reliably in browser context, causing the CVM client to always
time out. Switch to raw Relay.connect() which uses the v2 API correctly.
Race across all CVM relays and resolve on first valid response.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Rich system prompt that explains the CVM architecture: it aggregates
across multiple relays, deduplicates, and filters by features inside
events (complex queries that plain Nostr filters can't do)
- Switched tool call format from fragile regex (TOOL:name {...}) to
clean JSON {"tool":"name","args":{...}} that's easier for the LLM
- More robust JSON parsing — scans the full AI response for the tool call
- Better second-pass prompt: AI formats results with bullet points + links
- Updated suggested questions to showcase real CVM queries
- CVM failure gives a clear message explaining what the server does
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Removed:
- [NostrProvider] Routing query to read relays (fired on EVERY query)
- [NostrProvider] Publishing kind X to relays
- [NostrSync] Starting sync / querying profile / profile query returned / sync completed
- [NostrSync] Profile not found on main relays
- [NostrSync] Profile synced and cached
- [Shakespeare] Fetching / Model / Response status debug logs
Kept all warn/error logs for actual failure cases.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
proxy.shakespeare.diy was returning NetworkError on all requests.
Direct calls to ai.shakespeare.diy work fine from a real browser.
Kept debug logging to confirm correct URL and pubkey.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Removed from: useCVMClient, NostrSync, useLoggedInAccounts, useAuthor, SettingsPage
Replaced with relay.damus.io where a fallback was still needed.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use goskatespots.nostr1.com as primary (matches CVM server config),
plus relay.nostr.band and relay.damus.io as fallbacks.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
All fetch calls to ai.shakespeare.diy now go through proxy.shakespeare.diy
so they work from inside the iframe.diy preview sandbox.
The NIP-98 token is still signed against the original URL (not the proxy
URL) so the server-side signature validation passes correctly.
Affects: sendChatMessage, sendStreamingMessage, getAvailableModels
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Previous attempts used 'shakespeare' and 'claude-sonnet-4-5' — both invalid.
The correct format matches the recentlyUsedModels in ai.json config:
provider/model-name with dots not dashes.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The model name 'shakespeare' does not exist on the API — it was returning:
"Model 'shakespeare' is not supported" (400 invalid_request_error)
Updated both sendChatMessage calls in SasquatchGuide and the default
model in useShakespeare hook (sendChatMessage + sendStreamingMessage).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Previously, the catch block around response.json() would swallow the
actual error body and replace it with a generic "Invalid request" message.
Now:
- Read response as text() first (never fails)
- Log the raw body to console as [Shakespeare API 400] raw body: <text>
- Parse JSON from the text string separately
- Fallback error message includes the raw body so it's visible in console
This will make it easy to diagnose exactly what the Shakespeare API
is rejecting (model name, message format, missing field, etc.)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add useShakespeare.ts hook (Shakespeare AI via NIP-98 auth)
- Add useCVMClient.ts hook — calls SkateSpots CVM server tools over
Nostr directly from the browser (NIP-44 encrypted request/response,
no @contextvm/sdk bundle needed, pure nostr-tools)
- Rewrite SasquatchGuide to use Shakespeare AI + live CVM data:
* Static FAQ answers for protocol/app questions (fast, no API)
* Shakespeare AI for natural language understanding
* AI detects TOOL: calls in its response and executes them live
* CVM server queried for find_spots, find_events, find_coaches,
find_gear, get_community_stats
* Streaming "thinking..." and "Searching Nostr..." indicators
* Live/offline status badge shows CVM server connectivity
* Graceful fallback when CVM server unreachable
* Login nudge for unauthenticated users
CVM server pubkey: e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530
CVM server host: 185.18.221.2 (LNVPS, Dublin)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The drawCardToCanvas function had an old version of the card rendering logic
that leaked outside the function body (lines 505-731 were orphaned code).
This code referenced local variables (ctx, p, data, qrX, qrSize, PAD, etc.)
that only existed inside the function, causing ReferenceError at module load time
and crashing the entire app with a blank screen.
Also added missing lucide-react imports (Printer, Loader2, Share2, Download,
Check, Copy, Award, Zap, Info, Palette, Shield, ChevronUp, ChevronDown) that
were used in the JSX but not imported, causing the secondary Printer is not
defined ReferenceError.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
@noble/curves v2 dropped the ./secp256k1 subpath export. The manual
getEventHash + schnorr.sign + bytesToHex pattern is replaced with
nostr-tools' finalizeEvent() which handles id computation and signing
internally, with no external subpath dependencies. Also removes the
now-unused getEventHash import and bytesToHex helper.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
@noble/hashes v2 dropped the ./utils subpath export, breaking the import
in useDirectMessages.ts. Replaced with a simple inline implementation
that has zero new dependencies and works correctly with any build tool.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.
Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)
Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>