feat(sasquatch-pro): enforce the paid tier; make AI Pro-only

- useSasquatchPro: derive Pro expiry from what was ACTUALLY paid (largest tier
  the bolt11 covers, run from the receipt time), instead of trusting the
  user-signed `pro-expiry` tag — a 7-day payment could previously self-assert a
  year. Provider-authored receipt + amount checks unchanged.
- SasquatchGuide: AI answers are now Pro-only. Free users get the static FAQ,
  glossary, trip planner and local relay lookups (all no-cost), plus a nudge to
  Pro when a question would otherwise need AI.
- AGENTS.md updated.
This commit is contained in:
SkateSpots 2026-09-27 13:56:52 +07:00
parent 087f490acc
commit 670a33123d
2 changed files with 15 additions and 5 deletions

View file

@ -961,7 +961,9 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
let usedCVM = false;
let liveTs = 0; // timestamp of the streaming bubble (0 = none)
let streamSuppressed = false; // output looks like a JSON tool call — never render it
try {
// AI answers are a Pro feature. Free users skip this entirely and fall
// through to the static FAQ, glossary and local relay lookups below.
if (isPro) try {
// The system prompt goes in a real `system` message rather than being
// smuggled into a user turn — the model then treats the relay/derived
// text below as lower-trust input, blunting prompt-injection via event
@ -1340,7 +1342,10 @@ Instructions:
const fallback =
staticAnswer ??
answerFromGlossary(question) ??
`My AI brain's catching some air right now 🦧 — but I've still got you:\n\n• 🗺️ [Map](/) — spots, shops, events near you\n• 🧪 [The Lab](/lab) — coaches, filmers, editors\n• 📖 [Glossary](/glossary) — 150+ tricks & terms\n• 👥 [Crews](/crews) — find your people\n\nOr just tell me a **city** ("spots in Barcelona") or a **trick** ("what's a nollie") and I'll answer straight up. 🛹`;
(!isPro
? // Free tier: no AI. Nudge to Pro, but point at what still works.
"That one needs AI reasoning — a **Sasquatch Pro** feature ⚡🦧\n\nI can still find **spots, events, coaches, crews, shops, tours and gear** straight from the relays — ask me e.g. \"spots in Barcelona\" — and the [Glossary](/glossary) covers 150+ tricks & terms. Tap **⚡ Go Pro** up top to unlock AI answers."
: `My AI brain's catching some air right now 🦧 — but I've still got you:\n\n• 🗺️ [Map](/) — spots, shops, events near you\n• 🧪 [The Lab](/lab) — coaches, filmers, editors\n• 📖 [Glossary](/glossary) — 150+ tricks & terms\n• 👥 [Crews](/crews) — find your people\n\nOr just tell me a **city** ("spots in Barcelona") or a **trick** ("what's a nollie") and I'll answer straight up. 🛹`);
appendMessage({ role: 'sasquatch', text: fallback, timestamp: Date.now() });
}
} catch (err) {

View file

@ -30,7 +30,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import { getZapProviderPubkey } from '@/lib/lightningAddress';
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
import { PRO_LIGHTNING_ADDRESS, PRO_TIERS } from '@/lib/sasquatchPro';
import { PRO_LIGHTNING_ADDRESS, PRO_TIERS, DAY_SECONDS } from '@/lib/sasquatchPro';
import type { NostrEvent } from '@nostrify/nostrify';
const PLATFORM_PUBKEY = PLATFORM_OWNER_PUBKEYS[0];
@ -103,8 +103,13 @@ function expiryFromZapReceipt(
}
if (paidSats < MIN_PRO_SATS) return null;
const exp = Number(tagVal(zapReq.tags, 'pro-expiry'));
return Number.isFinite(exp) && exp > 0 ? exp : null;
// Enforce the tier from what was ACTUALLY paid. Never trust the user-signed
// `pro-expiry` tag — that let a 7-day payment self-assert a year. Pick the
// largest tier the payment covers and run the pass from when the receipt was
// issued (so paying later genuinely extends access).
const tier = [...PRO_TIERS].sort((a, b) => b.sats - a.sats).find((t) => paidSats >= t.sats);
if (!tier) return null;
return receipt.created_at + tier.days * DAY_SECONDS;
}
export function useSasquatchPro(): ProStatus {