Learn how to create an AI chat connected to OpenRouter (Claude AI) with trial requests after user login.
- Create paywall. Follow this guide:
- Create an API provider to handle tokenized requests:
- Enable and set up paywall tokenization:
- Now the paywall and API provider are ready.
We are planning to use three models of Claude AI: Sonnet, Opus, and Haiku. So, we have created three API providers, one for each model. You can get the API Provider settings for Claude 3 Haiku from the API Provider documentation. In our case, it’s OpenRouter Docs. Because we have different API pricing, we can choose different query price levels. We chose Standard for Haiku and Advanced for Sonnet and Opus. We also specified that 10,000 queries will be deposited into the user’s account after the subscription purchase, and 10 trial requests will be allowed after user login.
API Provider settings:
Let’s code
const model2url = { haiku: `https://onlineapp.pro/api/v1/api-gateway/29?paywall_id=197&test_mode=1`, sonnet: `https://onlineapp.pro/api/v1/api-gateway/26?paywall_id=197&test_mode=1`, opus: `https://onlineapp.pro/api/v1/api-gateway/30?paywall_id=197&test_mode=1` }; const model2type = { haiku: 'standard', sonnet: 'advanced', opus: 'advanced' }; export default function AIChat() { const [user, setUser] = useState(); const [messages, setMessages] = useState([]); const [answer, setAnswer] = useState(''); const [isLoading, setIsLoading] = useState(false); const [prompt, setPrompt] = useState(''); const abortSignal = useRef<{ abort: () => void }>({}); const getUser = async () => { const user = await paywall.getUser(); setUser(user); }; useEffect(() => { getUser(); }, []); const onSendMessage = async () => { if (isLoading) { abortSignal.current?.abort(); setIsLoading(false); return; } if (user?.error) { await paywall.open({ resolveEvent: 'signed-in' }); getUser(); return; } setAnswer(''); setIsLoading(true); setPrompt(''); const newMessages = [ ...messages, { role: 'user', content: prompt } ] setMessages(newMessages); let fullAnswer = ''; try { for await (const chunk of paywall.makeStreamRequest(model2url.sonnet, { method: 'POST', body: JSON.stringify({ messages: newMessages, stream: true }), abortSignal.current })) { try { const lines = chunk .split('\n') .filter((line) => line.startsWith('data: ')); lines.forEach((line) => { const data = line.replace('data: ', '').trim(); try { const parsed = JSON.parse(data); const content = parsed.choices[0].delta.content; fullAnswer += content; setAnswer((prev) => prev + content); } catch (error) { console.error('Failed to parse SSE data:', error); } }); } catch (error) { console.error('Failed to parse SSE data:', error); } } setMessages((prev) => [ ...prev, { role: 'assistant', content: fullAnswer } ]); // Charge user queries setUser((user) => ({ ...user, balances: user?.balances?.map((balance) => balance.type === model2type[selectedModelValue] ? { type: balance.type, count: balance.count - 1 } : balance ) })); } catch (error) { if (error === 'not-enough-queries') { // User queries have run out, ask to renew subscription to top up again setIsLoading(false); await paywall.renew(); // Update user balances after success payment await getUser(); } else if (error === 'Unauthorized' || error === 'access-denied') { setIsLoading(false); await paywall.open({ resolveEvent: 'signed-in' }); // Update user balances after success login await getUser(); } } setIsLoading(false); }; return <div> {messages.map((message) => message.text)} {isLoading && (answer || "Loading answer...")} <textarea onChange={setPrompt} value={prompt} placeholder="Write your question" /> <button onClick={onSendMessage}>Send message</button> {user?.balances && ( <div> Your balance: { user?.balances?.find( (balance) => balance.type === model2type.sonnet )?.count }{' '} Queries </div> ) )} </div> }