feat(sasquatch): Pro receipt DM + local Pro FAQ + tougher free-tier answers

1. Pro purchase confirmation: on a paid pass, send the buyer an encrypted
   NIP-17 receipt to their own inbox (tier, expiry, unlocked perks). Best-effort
   so it never blocks the purchase. Success toast now points to the DM.

2. Frictionless Pro answers: add local static-FAQ entries for "sasquatch pro",
   "pro perks/version", pricing, "go pro", "upgrade" — so those questions are
   answered instantly and never dead-end when the free AI model is rate-limited
   (the exact failure seen when asking "what perks has the pro version?").

3. FAQ matching now prefers the longest matching key so specific phrases win
   over broad ones ("sasquatch pro" > "pro").

4. Refresh the free-model fallback list (add deepseek-v3, gemini-2.0-flash-exp,
   qwen-2.5-72b) for better free-tier AI availability.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-30 00:21:34 +07:00
parent 1c18163a90
commit d6b50de349
4 changed files with 63 additions and 6 deletions

View file

@ -438,17 +438,31 @@ const STATIC_FAQ: Record<string, string> = {
'what relay': "SkateSpots uses: 🛹 goskatespots.nostr1.com (primary, allowlisted for quality) + relay.ditto.pub (global) + purplepag.es (profile aggregator). Your data is replicated across all of them for redundancy.",
'trip planner': "Tell me a city and I'll build a day-by-day skate trip 🗺️ — spots, events, coaches and local crews, weather-aware (rainy day = crew tip for covered spots). Try 'plan me 3 days in Lisbon'. I can save it all to a Collection, and hand you off to flights/stays on the real sites (no KYC, no middleman). 🛹",
'plan a trip': "Just say 'plan me 3 days in Barcelona' (or any city) and I'll map out spots, sessions, coaches and local crews for each day. Save it as a Collection and go shred. 🗺️🛹",
// ── Sasquatch Pro — answered locally so it never depends on the AI brain ──
'sasquatch pro': "**Sasquatch Pro** 🦧⚡ is a Lightning supporter pass that unlocks the personal side of me:\n\n• 🧠 **Personal assistant** — I know your Stash, crews, spots & listings\n• ✨ **Action drafts** — list gear, post a spot status, plan trips (you always confirm before anything publishes)\n• 🚀 **Priority AI** — better model, higher limits, longer memory\n• 🌤️ **Proactive nudges** — weather + spot tips (opt-in)\n• 🏅 **Supporter badge** + Pro-only skins\n\nFree Sasquatch stays free: public spot/event/coach/crew search, the glossary & trip planning all work without Pro. Pro just adds the *personal* stuff and keeps SkateSpots ad-free. Tap **⚡ Go Pro** up top. Tiers: 7 days ~2k sats, 1 month ~5k, 3 months ~12k, 1 year ~36k.",
'pro perks': "Pro perks 🦧⚡: personal assistant (reads your Stash/crews/spots/listings), action drafts (list gear, post status, plan trips — you confirm), priority AI model with higher limits & longer memory, opt-in weather/spot nudges, and a supporter badge + Pro skins. Free stays free — Pro adds the personal layer & keeps the app ad-free. Tap **⚡ Go Pro** up top.",
'pro version': "The Pro version of me 🦧⚡ adds a **personal assistant**: I can read your Stash, crews, spots & listings, and draft actions for you (list gear, post a status, plan a trip — you always confirm first). Plus a priority AI model, higher limits, longer memory, opt-in weather nudges, and a supporter badge. Free Sasquatch (spot/event/coach/crew search, glossary, trip planner) stays free. Tap **⚡ Go Pro** up top.",
'perks': "Pro perks 🦧⚡: personal assistant (your Stash/crews/spots/listings), action drafts you confirm, priority AI + higher limits + longer memory, opt-in weather/spot nudges, and a supporter badge + Pro skins. Tap **⚡ Go Pro** up top.",
'go pro': "Tap the **⚡ Go Pro** button up top to open the upgrade sheet. Pro unlocks my personal assistant (Stash/crews/spots/listings), action drafts, priority AI and a supporter badge. Pay with Lightning — 7 days ~2k sats up to 1 year ~36k sats. Free features stay free. 🦧",
'pro pricing': "Pro tiers (pay with Lightning ⚡): **7 days** ~2,000 sats (~$1.20), **1 month** ~5,000 sats (~$3), **3 months** ~12,000 sats (~$7, 20% off), **1 year** ~36,000 sats (~$21, best value). Tap **⚡ Go Pro** up top.",
'how much is pro': "Pro tiers: 7 days ~2,000 sats, 1 month ~5,000 sats, 3 months ~12,000 sats (20% off), 1 year ~36,000 sats (best value). Pay with Lightning ⚡. Tap **⚡ Go Pro** up top.",
'upgrade': "To upgrade to Sasquatch Pro, tap the **⚡ Go Pro** button up top. Pay with Lightning (7 days ~2k sats up to 1 year ~36k). Pro unlocks my personal assistant, action drafts, priority AI and a supporter badge. 🦧⚡",
};
function getStaticAnswer(question: string): string | null {
const q = question.toLowerCase().trim();
// Exact match first (for greetings like "hi", "hey")
if (STATIC_FAQ[q]) return STATIC_FAQ[q];
// Then partial match
// Partial match — prefer the LONGEST matching key so specific phrases
// ("sasquatch pro", "pro pricing") win over broad ones ("pro", "perks").
let best: { key: string; answer: string } | null = null;
for (const [key, answer] of Object.entries(STATIC_FAQ)) {
if (q.includes(key)) return answer;
if (q.includes(key) && (!best || key.length > best.key.length)) {
best = { key, answer };
}
return null;
}
return best?.answer ?? null;
}
// ─── CVM connection status indicator ─────────────────────────────────────────

View file

@ -75,7 +75,7 @@ export function SasquatchProDialog({
// Toast + close on success.
useEffect(() => {
if (purchase.state === 'paid') {
toast({ title: 'Welcome to Sasquatch Pro! 🦧⚡', description: 'Your supporter pass is active. Thanks for keeping the streets free.' });
toast({ title: 'Welcome to Sasquatch Pro! 🦧⚡', description: 'Your pass is active — a receipt just landed in your DMs. Thanks for keeping the streets free.' });
const t = setTimeout(() => { onOpenChange(false); purchase.reset(); }, 1400);
return () => clearTimeout(t);
}

View file

@ -21,6 +21,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNWC } from '@/hooks/useNWCContext';
import { useWallet } from '@/hooks/useWallet';
import { useAppContext } from '@/hooks/useAppContext';
import { useSendDirectMessage } from '@/hooks/useDirectMessages';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import {
getZapProviderPubkey,
@ -47,9 +48,45 @@ export function usePurchasePro() {
const { getActiveConnection, sendPayment } = useNWC();
const { webln } = useWallet();
const { config } = useAppContext();
const { mutateAsync: sendDM } = useSendDirectMessage();
const [result, setResult] = useState<PurchaseResult>({ invoice: null, state: 'idle', error: null });
/**
* Send the buyer an encrypted (NIP-17) confirmation to their own inbox — a
* durable, private receipt of what they bought. Best-effort: a failure here
* never blocks the purchase (they already paid + got the on-relay receipt).
*/
const sendConfirmationDM = useCallback(
async (tier: ProTier, expiry: number) => {
if (!user?.pubkey || !user.signer?.nip44) return;
const until = new Date(expiry * 1000).toLocaleDateString(undefined, {
year: 'numeric', month: 'long', day: 'numeric',
});
const content = [
`🦧⚡ Sasquatch Pro — you're in!`,
``,
`Pass: ${tier.label} (${tier.sats.toLocaleString()} sats)`,
`Active until: ${until}`,
``,
`What you unlocked:`,
`• Personal assistant — I read your Stash, crews, spots & listings`,
`• Action drafts — list gear, post a spot status, plan trips (you confirm)`,
`• Priority AI model, higher limits & longer memory`,
`• Opt-in weather + spot nudges`,
`• ⚡ Supporter badge + Pro-only skins`,
``,
`Thanks for keeping SkateSpots free & ad-free. Now go shred. 🛹`,
].join('\n');
try {
await sendDM({ recipients: user.pubkey, content, subject: 'Sasquatch Pro receipt' });
} catch {
/* best-effort — don't fail the purchase over a receipt DM */
}
},
[user, sendDM],
);
/** Publish the self-signed fallback receipt for a paid tier. */
const publishReceipt = useCallback(
async (tier: ProTier, expiry: number) => {
@ -125,6 +162,7 @@ export function usePurchasePro() {
try {
await sendPayment(nwc, invoice);
await publishReceipt(tier, expiry);
await sendConfirmationDM(tier, expiry);
setResult({ invoice: null, state: 'paid', error: null });
return;
} catch {
@ -137,6 +175,7 @@ export function usePurchasePro() {
if (typeof webln.enable === 'function') await webln.enable();
await webln.sendPayment(invoice);
await publishReceipt(tier, expiry);
await sendConfirmationDM(tier, expiry);
setResult({ invoice: null, state: 'paid', error: null });
return;
} catch {
@ -155,7 +194,7 @@ export function usePurchasePro() {
});
}
},
[user, config, getActiveConnection, sendPayment, webln, publishReceipt],
[user, config, getActiveConnection, sendPayment, webln, publishReceipt, sendConfirmationDM],
);
/** Called after a manual (QR) payment to record the pass locally. */
@ -164,6 +203,7 @@ export function usePurchasePro() {
const expiry = Math.floor(Date.now() / 1000) + tier.days * DAY_SECONDS;
try {
await publishReceipt(tier, expiry);
await sendConfirmationDM(tier, expiry);
setResult({ invoice: null, state: 'paid', error: null });
} catch (err) {
setResult({
@ -173,7 +213,7 @@ export function usePurchasePro() {
});
}
},
[publishReceipt],
[publishReceipt, sendConfirmationDM],
);
const reset = useCallback(() => setResult({ invoice: null, state: 'idle', error: null }), []);

View file

@ -14,6 +14,9 @@ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
// rate-limited (429) we back off briefly then move to the next model.
export const FREE_MODELS = [
'meta-llama/llama-3.3-70b-instruct:free',
'deepseek/deepseek-chat-v3-0324:free',
'google/gemini-2.0-flash-exp:free',
'qwen/qwen-2.5-72b-instruct:free',
'google/gemma-2-9b-it:free',
'mistralai/mistral-7b-instruct:free',
'qwen/qwen-2.5-7b-instruct:free',