diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx
index 871bd59..6d8ba86 100644
--- a/src/components/SasquatchGuide.tsx
+++ b/src/components/SasquatchGuide.tsx
@@ -1474,7 +1474,7 @@ Instructions:
- 🦧 Sasquatch
+ Sasquatch
{isPro ? (
- ⚡ {aiStatus.data.balanceSats.toLocaleString()} sats
+ ⚡ AI {aiStatus.data.balanceSats.toLocaleString()}
)}
diff --git a/src/lib/proAuth.ts b/src/lib/proAuth.ts
index fc4326d..54f098b 100644
--- a/src/lib/proAuth.ts
+++ b/src/lib/proAuth.ts
@@ -6,8 +6,9 @@
* • `x-sasquatch-auth` — a base64 NIP-98 (kind 27235) event proving the caller;
* • `x-sasquatch-pro` — base64 of the Pro zap receipt proving an active pass.
*
- * The NIP-98 event is signed ONCE per session and reused, so an interactive
- * signer (Amber) doesn't pop on every message.
+ * The NIP-98 event is signed ONCE and reused for its whole validity window —
+ * persisted to localStorage so an interactive signer (Amber) does not pop again
+ * on every app load. (It expires with the 12 h window the proxy enforces.)
*/
import type { NostrEvent } from '@nostrify/nostrify';
import { sasquatchAiBase } from '@/lib/sasquatchAi';
@@ -23,6 +24,32 @@ type SignEvent = (event: {
let cachedAuthHeader: string | null = null;
let cachedAuthPubkey: string | null = null;
+/** Persisted NIP-98 auth (keyed to the pubkey, valid while inside the window). */
+const AUTH_STORAGE_KEY = 'skatespots:sasquatch-nip98';
+/** Reuse for 11 h to stay safely inside the proxy's 12 h acceptance window. */
+const AUTH_MAX_AGE_SECONDS = 11 * 3600;
+
+function loadStoredAuth(pubkey: string): string | null {
+ try {
+ const raw = localStorage.getItem(AUTH_STORAGE_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw) as { pubkey?: string; header?: string; createdAt?: number };
+ if (parsed?.pubkey !== pubkey || !parsed.header || !parsed.createdAt) return null;
+ if (Math.abs(Date.now() / 1000 - parsed.createdAt) > AUTH_MAX_AGE_SECONDS) return null;
+ return parsed.header;
+ } catch {
+ return null;
+ }
+}
+
+function storeAuth(pubkey: string, header: string, createdAt: number): void {
+ try {
+ localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ pubkey, header, createdAt }));
+ } catch {
+ /* private mode — the in-memory cache still works for this session */
+ }
+}
+
function toBase64(value: string): string {
// Event JSON is ASCII (hex pubkeys/ids, tags), so btoa is safe here.
return btoa(value);
@@ -39,17 +66,27 @@ export async function buildProAuthHeaders(opts: {
if (!pubkey || !signEvent) return headers;
if (cachedAuthPubkey !== pubkey || !cachedAuthHeader) {
- const event = await signEvent({
- kind: 27235,
- created_at: Math.floor(Date.now() / 1000),
- tags: [
- ['u', sasquatchAiBase()],
- ['method', 'POST'],
- ],
- content: '',
- });
- cachedAuthHeader = toBase64(JSON.stringify(event));
- cachedAuthPubkey = pubkey;
+ // Reuse a stored signature first — otherwise Amber pops on every app load
+ // for a signer that would happily reuse the same NIP-98 for 12 h.
+ const stored = loadStoredAuth(pubkey);
+ if (stored) {
+ cachedAuthHeader = stored;
+ cachedAuthPubkey = pubkey;
+ } else {
+ const createdAt = Math.floor(Date.now() / 1000);
+ const event = await signEvent({
+ kind: 27235,
+ created_at: createdAt,
+ tags: [
+ ['u', sasquatchAiBase()],
+ ['method', 'POST'],
+ ],
+ content: '',
+ });
+ cachedAuthHeader = toBase64(JSON.stringify(event));
+ cachedAuthPubkey = pubkey;
+ storeAuth(pubkey, cachedAuthHeader, createdAt);
+ }
}
headers['x-sasquatch-auth'] = cachedAuthHeader;
diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx
index f348507..154e21c 100644
--- a/src/pages/MapPage.tsx
+++ b/src/pages/MapPage.tsx
@@ -1603,7 +1603,11 @@ export default function MapPage() {
)}
{/* Main Content */}
-
+ {/* `isolate` creates a stacking context so the map's internal z-indexes
+ (Leaflet panes, our overlays up to z-[2000]) stay INSIDE the map. Without
+ it they competed with the root and the © OpenStreetMap attribution
+ (z-999) painted on top of the Sasquatch panel (z-40). */}
+
{viewMode === 'map' ? (
<>
-