Your landing page, rewritten for each kind of visitor. Crawl once, approve every variant, paste one tag. Self-hosting needs your own Vaaya API key
See the codeYour landing page, rewritten for each kind of visitor. Give it a URL: it reads the page once, suggests visitor personas, and writes each persona's version of your headline, subhead, button and feature blurbs. You approve every block, then paste one tag in your page head.
Self-hosting needs your own Vaaya API key with balance. Every model call and rendered-crawl fallback is billed to that key at Vaaya's per-call prices. It is not free to run. Get a key at https://vaaya.ai. If you would rather not host it, the hosted version is at https://vaaya.ai/personalized-landing-pages.
utm_content, utm_campaign, utm_source, then the referring site. A
bare visit with none of these sees your original page. A model is asked only when your site
passes free text (a site search, a chat opener), at most once per visit| Name | Required | What |
|---|---|---|
VAAYA_API_KEY | Yes | Your Vaaya API key. Every model call and rendered read is billed to it |
VAAYA_URL | No | Vaaya base URL. Default https://vaaya.ai |
ADMIN_TOKEN | Yes | The token you type at /login. Long and random. Changing it signs every browser out |
SESSION_SECRET | Yes | 32 or more random characters. Seals the session cookie and salts the hashed visitor IPs |
BLOB_READ_WRITE_TOKEN | Yes | From the Vercel Blob store connected to the project |
APP_URL | No | This app's public address, no trailing slash, used in the install tag. Default: the address the dashboard was opened on |
MODELS | No | Comma-separated model ids, tried in order. Replaces every default model list |
CLASSIFY_PER_SITE_DAY | No | Visitor classifications per site per UTC day. Default 500. 0 turns classification off |
CLASSIFY_PER_IP_DAY | No | Visitor classifications per visitor IP per UTC day, across every site. Default 20 |
pnpm install
cp .env.example .env.local # then fill it in
pnpm dev # http://localhost:3000, sign in with ADMIN_TOKEN
pnpm test # unit tests
Local development still needs a Blob store token. vercel env pull .env.local fetches it once
the project is linked.
Deploy to Vercel: import the repository, connect a Blob store to the project (Storage, then
Blob; this sets BLOB_READ_WRITE_TOKEN), add the other variables and deploy. The routes that
crawl and generate ask for up to 300 seconds (maxDuration), so your plan must allow that.
The Install step in the dashboard shows your exact tag. It goes first in the page head,
before any other script or stylesheet, and must stay synchronous (no async, defer or
type="module"), because an async script cannot swap text before the first paint:
<head>
<script src="https://your-app.example.com/api/s/pv_yoursitekey.js"></script>
...
</head>
The dashboard also gives you a prompt to paste into a coding agent, with the right file for plain HTML, Vite or Lovable, and the Next.js App and Pages routers.
If your site has a search box or a chat opener, pass the visitor's first query or message:
window.vaayaPersona?.classify(text) // resolves to a persona id, or null
Preview any persona on your live site with ?vaaya_persona=<persona id>, and your original with
?vaaya_persona=none. A preview never sets the cookie.
Hosted and self-hosted are priced differently. The hosted version has product prices and gives some steps away; a self-hosted copy has no product price, and every call it makes is billed to your own key.
Persona suggestions are free, and so is UTM and referrer routing on every pageview. Generation is charged once per site when the variants are ready, nothing if it fails. Classification of a visitor's free text is charged per visit, and routing keeps working when the balance runs out. The current prices are on https://vaaya.ai/personalized-landing-pages.
Your Vaaya key pays per call, at Vaaya's per-call prices, including the steps that are free on the hosted version:
Each step tries the next model in its list only when one fails, and a failed generation can be run again.
Free on Vaaya either way: the direct page fetch, UTM and referrer routing, and serving the tag. Self-hosted, Vercel bills hosting and Blob storage under your plan.
The caps bound what visitors can spend. CLASSIFY_PER_SITE_DAY and CLASSIFY_PER_IP_DAY are
counted in one blob per UTC day (counts/<YYYY-MM-DD>.json). A classification is counted when it
is attempted, before the model call, so a failed call still counts. Visitor IPs are stored only
as salted hashes. On top of that, each server instance allows at most 5 classifications per
minute from one visitor on one site and 120 per minute per site. After Vaaya refuses the key
(401) or the balance (402), classification pauses for a minute on that instance and visitors
keep plain UTM and referrer routing.
ifMatch, a weak W/ prefix stripped)
and retried when another write got there first
sites/<id>.json: one site with its crawl, personas, routing rules and every variantlive/<site_key>.json: what the public endpoints read, approved variants only, rewritten
after generation and after every review or routing changecounts/<YYYY-MM-DD>.json: the daily classification counters/login checks ADMIN_TOKEN in constant time and sets one httpOnly cookie, AES-GCM
sealed with SESSION_SECRET. Dashboard writes also refuse a request whose Origin is another sitelib/vaaya.ts with authorization: Bearer <VAAYA_API_KEY> and
x-vaaya-agent: personalized-landing-pages:
POST {VAAYA_URL}/api/llm/v1/chat/completions, OpenAI-compatible, with a forced
tool call so every answer is one JSON objectPOST {VAAYA_URL}/api/run/vaaya/onescrape with one URL, format: "html" and
a cost capgoogle/gemini-2.5-flash-lite, openai/gpt-5-nano, anthropic/claude-haiku-4.5;
generation uses anthropic/claude-sonnet-5, then openai/gpt-5.2. MODELS replaces both lists
(suggestions and classification then use its first two)lib/snippet-runtime.ts), never compiled, so a
bundler cannot change what ships. The tests evaluate those exact bytesmax-age=60, s-maxage=30, stale-while-revalidate=30, and each server
instance keeps a site's live copy in memory for 10 secondsapp/page.tsx dashboard when signed in, a short intro otherwise
app/dashboard.tsx the four steps: personas, generate, review, install
app/login/page.tsx sign in with ADMIN_TOKEN
app/api/login, app/api/logout set and clear the session cookie
app/api/sites/... add, list, personas, generate, review, routing, remove
app/api/s/[file] public: the tag, /api/s/<site_key>.js
app/api/classify public: a visitor's free text to a persona
lib/store.ts sites, live copies and daily counters
lib/blob.ts JSON documents in Vercel Blob with ETag checks
lib/crawl.ts the fetch, the address checks, the rendered fallback
lib/html.ts, lib/extract.ts the parser and block extraction
lib/suggest.ts persona suggestions
lib/generate.ts the variants
lib/classify.ts visitor classification
lib/facts.ts the facts guard
lib/snippet.ts, snippet-runtime.ts the served tag
lib/install.ts the tag and the coding-agent prompt
lib/public.ts the two public endpoints
lib/vaaya.ts the only file that calls Vaaya
lib/session.ts, lib/env.ts sign-in and settings
lib/__tests__/ vitest
MIT, see LICENSE.
Your landing page, rewritten for each kind of visitor. Crawl once, approve every variant, paste one tag. Self-hosting needs your own Vaaya API key
See the codeYour landing page, rewritten for each kind of visitor. Give it a URL: it reads the page once, suggests visitor personas, and writes each persona's version of your headline, subhead, button and feature blurbs. You approve every block, then paste one tag in your page head.
Self-hosting needs your own Vaaya API key with balance. Every model call and rendered-crawl fallback is billed to that key at Vaaya's per-call prices. It is not free to run. Get a key at https://vaaya.ai. If you would rather not host it, the hosted version is at https://vaaya.ai/personalized-landing-pages.
utm_content, utm_campaign, utm_source, then the referring site. A
bare visit with none of these sees your original page. A model is asked only when your site
passes free text (a site search, a chat opener), at most once per visit| Name | Required | What |
|---|---|---|
VAAYA_API_KEY | Yes | Your Vaaya API key. Every model call and rendered read is billed to it |
VAAYA_URL | No | Vaaya base URL. Default https://vaaya.ai |
ADMIN_TOKEN | Yes | The token you type at /login. Long and random. Changing it signs every browser out |
SESSION_SECRET | Yes | 32 or more random characters. Seals the session cookie and salts the hashed visitor IPs |
BLOB_READ_WRITE_TOKEN | Yes | From the Vercel Blob store connected to the project |
APP_URL | No | This app's public address, no trailing slash, used in the install tag. Default: the address the dashboard was opened on |
MODELS | No | Comma-separated model ids, tried in order. Replaces every default model list |
CLASSIFY_PER_SITE_DAY | No | Visitor classifications per site per UTC day. Default 500. 0 turns classification off |
CLASSIFY_PER_IP_DAY | No | Visitor classifications per visitor IP per UTC day, across every site. Default 20 |
pnpm install
cp .env.example .env.local # then fill it in
pnpm dev # http://localhost:3000, sign in with ADMIN_TOKEN
pnpm test # unit tests
Local development still needs a Blob store token. vercel env pull .env.local fetches it once
the project is linked.
Deploy to Vercel: import the repository, connect a Blob store to the project (Storage, then
Blob; this sets BLOB_READ_WRITE_TOKEN), add the other variables and deploy. The routes that
crawl and generate ask for up to 300 seconds (maxDuration), so your plan must allow that.
The Install step in the dashboard shows your exact tag. It goes first in the page head,
before any other script or stylesheet, and must stay synchronous (no async, defer or
type="module"), because an async script cannot swap text before the first paint:
<head>
<script src="https://your-app.example.com/api/s/pv_yoursitekey.js"></script>
...
</head>
The dashboard also gives you a prompt to paste into a coding agent, with the right file for plain HTML, Vite or Lovable, and the Next.js App and Pages routers.
If your site has a search box or a chat opener, pass the visitor's first query or message:
window.vaayaPersona?.classify(text) // resolves to a persona id, or null
Preview any persona on your live site with ?vaaya_persona=<persona id>, and your original with
?vaaya_persona=none. A preview never sets the cookie.
Hosted and self-hosted are priced differently. The hosted version has product prices and gives some steps away; a self-hosted copy has no product price, and every call it makes is billed to your own key.
Persona suggestions are free, and so is UTM and referrer routing on every pageview. Generation is charged once per site when the variants are ready, nothing if it fails. Classification of a visitor's free text is charged per visit, and routing keeps working when the balance runs out. The current prices are on https://vaaya.ai/personalized-landing-pages.
Your Vaaya key pays per call, at Vaaya's per-call prices, including the steps that are free on the hosted version:
Each step tries the next model in its list only when one fails, and a failed generation can be run again.
Free on Vaaya either way: the direct page fetch, UTM and referrer routing, and serving the tag. Self-hosted, Vercel bills hosting and Blob storage under your plan.
The caps bound what visitors can spend. CLASSIFY_PER_SITE_DAY and CLASSIFY_PER_IP_DAY are
counted in one blob per UTC day (counts/<YYYY-MM-DD>.json). A classification is counted when it
is attempted, before the model call, so a failed call still counts. Visitor IPs are stored only
as salted hashes. On top of that, each server instance allows at most 5 classifications per
minute from one visitor on one site and 120 per minute per site. After Vaaya refuses the key
(401) or the balance (402), classification pauses for a minute on that instance and visitors
keep plain UTM and referrer routing.
ifMatch, a weak W/ prefix stripped)
and retried when another write got there first
sites/<id>.json: one site with its crawl, personas, routing rules and every variantlive/<site_key>.json: what the public endpoints read, approved variants only, rewritten
after generation and after every review or routing changecounts/<YYYY-MM-DD>.json: the daily classification counters/login checks ADMIN_TOKEN in constant time and sets one httpOnly cookie, AES-GCM
sealed with SESSION_SECRET. Dashboard writes also refuse a request whose Origin is another sitelib/vaaya.ts with authorization: Bearer <VAAYA_API_KEY> and
x-vaaya-agent: personalized-landing-pages:
POST {VAAYA_URL}/api/llm/v1/chat/completions, OpenAI-compatible, with a forced
tool call so every answer is one JSON objectPOST {VAAYA_URL}/api/run/vaaya/onescrape with one URL, format: "html" and
a cost capgoogle/gemini-2.5-flash-lite, openai/gpt-5-nano, anthropic/claude-haiku-4.5;
generation uses anthropic/claude-sonnet-5, then openai/gpt-5.2. MODELS replaces both lists
(suggestions and classification then use its first two)lib/snippet-runtime.ts), never compiled, so a
bundler cannot change what ships. The tests evaluate those exact bytesmax-age=60, s-maxage=30, stale-while-revalidate=30, and each server
instance keeps a site's live copy in memory for 10 secondsapp/page.tsx dashboard when signed in, a short intro otherwise
app/dashboard.tsx the four steps: personas, generate, review, install
app/login/page.tsx sign in with ADMIN_TOKEN
app/api/login, app/api/logout set and clear the session cookie
app/api/sites/... add, list, personas, generate, review, routing, remove
app/api/s/[file] public: the tag, /api/s/<site_key>.js
app/api/classify public: a visitor's free text to a persona
lib/store.ts sites, live copies and daily counters
lib/blob.ts JSON documents in Vercel Blob with ETag checks
lib/crawl.ts the fetch, the address checks, the rendered fallback
lib/html.ts, lib/extract.ts the parser and block extraction
lib/suggest.ts persona suggestions
lib/generate.ts the variants
lib/classify.ts visitor classification
lib/facts.ts the facts guard
lib/snippet.ts, snippet-runtime.ts the served tag
lib/install.ts the tag and the coding-agent prompt
lib/public.ts the two public endpoints
lib/vaaya.ts the only file that calls Vaaya
lib/session.ts, lib/env.ts sign-in and settings
lib/__tests__/ vitest
MIT, see LICENSE.