Aeonav
  • Pricing
  • Changelog
  • Roadmap
  • Vision
  • Docs
GitHubSign up
IntroductionQuick Start
OverviewDevelopment SetupCommands ReferenceArchitectureConfigurationCloud DeploymentTelemetryAI Contribution PolicyVersioning & Releases
Edit this page·View as Markdown·Report an issue·Join our Discord

Get product updates

New features and releases from Aeonav, straight to your inbox.

Product

  • Features
  • Pricing
  • Off-Site AEO
  • Changelog
  • Roadmap

Resources

  • Blog
  • Documentation
  • Support
  • API Reference
  • Provider Status
  • Issues

Learn

  • What is AEO?
  • What is GEO?
  • AEO Glossary
  • AI Search Guides
  • AI Search Statistics
  • AEO by Industry

Tools

  • Tool Directory
  • Free & Open-Source Tools
  • Compare Tools

Company

  • About
  • Vision
  • Brand Assets
  • Contact
  • llms.txt

© 2026 Aeonav

TermsPrivacyCookiesSubprocessorsAcceptable Use
v0.7.8GitHub repositorysupport@fivecoder.com

Cloud Deployment

Operating Aeonav Cloud — Waffo billing, plan entitlements, and the scheduling policy.

DEPLOYMENT_MODE=cloud runs the managed, multi-tenant SaaS offering: self-serve signup, one umbrella organization per customer holding all their brands, Waffo subscription billing, and plan-enforced limits. Local, demo, and whitelabel deployments are unaffected by everything on this page — outside cloud mode, entitlements resolve to unlimited and none of the billing code paths execute.

Required environment

On top of the shared variables (DATABASE_URL, BETTER_AUTH_SECRET, SCRAPE_TARGETS, DEPLOYMENT_MODE), cloud requires:

VariablePurpose
APP_URLPublic base URL (auth, email links, and Waffo checkout return URLs)
WAFFO_MERCHANT_IDWaffo merchant id (MER_...)
WAFFO_PRIVATE_KEYWaffo merchant RSA private key; server-only
WAFFO_STORE_IDWaffo store id; webhooks from another store are rejected
WAFFO_PRODUCT_IDSJSON map from plan:interval to Waffo product id
RESEND_API_KEY, RESEND_FROM_EMAILTransactional email
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRETGoogle sign-in
CLOUD_SIGNUP_ALLOWLISTSignup gate — empty denies everyone, * opens signup
TURNSTILE_SECRET_KEY, VITE_TURNSTILE_SITE_KEYCloudflare Turnstile keys for email auth bot protection

Optional: set CLERK_ISSUER, CLERK_CLIENT_ID, and CLERK_CLIENT_SECRET together to add Clerk as an OIDC sign-in method. A partial configuration is ignored. Register this exact redirect URI in the Clerk OAuth application: <APP_URL>/api/auth/callback/clerk.

Bot protection

Email sign-up, sign-in, password reset, and verification-email resend all send mail to whatever address the caller types, so they are guarded with Cloudflare Turnstile through better-auth's captcha plugin. Google sign-in, API keys, and MCP/OAuth are not challenged.

Create a Turnstile widget in the Cloudflare dashboard for the app's hostname (Managed mode), then set its site key as VITE_TURNSTILE_SITE_KEY and its secret as TURNSTILE_SECRET_KEY. The check is required in cloud mode: startup validation fails when either key is missing, and the auth forms stay protected. Tokens are only accepted when Cloudflare reports they were solved on the hostname in APP_URL, so the widget's hostname list must include it.

For local development, use Cloudflare's test keys, which always pass and skip the hostname check:

VITE_TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

Site key 2x00000000000000000000AB always fails and 3x00000000000000000000FF forces the interactive checkbox, for trying the error and challenge paths.

Waffo setup

The plan catalog lives in packages/config/src/plans.ts — prices, limits, sampling rates, and the premium pool per plan. Create one Waffo product for each plan and interval in the Waffo dashboard, then map the product ids in WAFFO_PRODUCT_IDS. Every plan that customers may switch between must belong to the same Waffo subscription product group. If customers can change plans in the customer portal, enable selfServicePlanChange for that group.

WAFFO_PRODUCT_IDS='{
  "starter:month":"PROD_starter_month","starter:year":"PROD_starter_year",
  "basic:month":"PROD_basic_month","basic:year":"PROD_basic_year",
  "pro:month":"PROD_pro_month","pro:year":"PROD_pro_year",
  "business:month":"PROD_business_month","business:year":"PROD_business_year"
}'

Checkout resolves plan:interval from this map; a missing entry fails before the customer is sent to Waffo. Checkout and the Waffo customer portal own card capture, tax handling, retries, and cancellations. The app sends the organization id as buyerIdentity and puts plan and interval in order metadata so the webhook can identify the purchase.

Configure the store webhook in Waffo:

  • URL: <APP_URL>/api/webhooks/waffo
  • Events: subscription.activated, subscription.renewed, subscription.recovered, subscription.plan_changed, subscription.canceling, subscription.uncanceled, subscription.canceled, subscription.past_due, refund.succeeded

Waffo signs the exact request body with RSA-SHA256 over ${timestamp}.${rawBody} and sends the signature in X-Waffo-Signature; the handler verifies it before parsing the body. The SDK allows a 45-minute past window and a one-minute future window. The handler also rejects events from another Waffo store or environment and treats an unknown or intentionally ignored event as a successful delivery. It persists a payload-derived ordering key, so a redelivery is a no-op and an older event cannot overwrite a newer subscription state; deliveries for one organization are serialized in a transaction. It returns 401 for a bad signature or payload, and 500 when applying a verified event fails, so Waffo retries transient database errors. Do not subscribe to order.completed or subscription.payment_succeeded: neither carries the subscription status and period this handler needs, so the app deliberately ignores them.

The webhook reads WAFFO_WEBHOOK_TEST_PUBLIC_KEY outside production and WAFFO_WEBHOOK_PROD_PUBLIC_KEY in a production cloud deployment. It fails closed with 503 when that environment-specific key is missing; it does not fall back to the SDK's built-in key or the legacy shared WAFFO_WEBHOOK_PUBLIC_KEY. Store the matching key in the secret manager or .env.

Test and production are separate Waffo environments. A cloud deployment uses production only when NODE_ENV=production; other deployments use test mode. The webhook-maintained subscription table is the single source of truth for billing state. A past_due subscription keeps tracking alive for a 7-day grace window, then pauses tracking until Waffo reports recovery.

Waffo owns customer payment notifications. The Waffo webhook does not invoke the app's dunning email templates.

Discounts

Create coupons and promotion codes in the Waffo dashboard. Waffo checkout and the customer portal own validation and redemption; the app does not see or validate discount codes.

SCRAPE_TARGETS for cloud

The instance-level SCRAPE_TARGETS defines the menu customers pick from. Configure one target per standard platform (the run policy uses the first match per model), plus a grounded target for each model sold in the premium tier:

chatgpt:brightdata:online,google-ai-mode:brightdata:online,google-ai-overview:brightdata:online,copilot:brightdata:online,perplexity:brightdata:online,gemini:brightdata:online,qwen:openrouter:qwen/qwen3-235b-a22b-2507,deepseek:openrouter:deepseek/deepseek-v3.2,mistral:openrouter:mistralai/mistral-medium-3.1,claude:anthropic-api:claude-haiku-5-5,kimi:openrouter:moonshotai/kimi-k2.5,chatgpt:openai-api:gpt-6-luna:online,grok:openrouter:x-ai/grok-4.5:online,claude:anthropic-api:claude-haiku-5-5:online

Platforms are offered to customers only when they appear in both the plan menu (STANDARD_PLATFORM_MENU) and SCRAPE_TARGETS.

Supported model versions

These are the exact versions Aeonav Cloud runs. Scraped platforms have no version to pin: they track whatever the live product serves its users that day.

PlatformSold asVersion
ChatGPTPick (scraped)Live chatgpt.com
ChatGPTPremiumgpt-6-luna with web search, via the OpenAI API
Google AI ModePick (scraped)Live product
Google AI OverviewsPick (scraped)Live product
CopilotPick (scraped)Live product
PerplexityPick (scraped)Live product
GeminiPick (scraped)Live gemini.google.com
QwenPick (API)qwen/qwen3-235b-a22b-2507, via OpenRouter
DeepSeekPick (API)deepseek/deepseek-v3.2, via OpenRouter
MistralPick (API)mistralai/mistral-medium-3.1, via OpenRouter
ClaudePick (API)claude-haiku-5-5, via the Anthropic API
ClaudePremiumclaude-haiku-5-5 with web search, via the Anthropic API
GrokPremiumx-ai/grok-4.5 with web search, via OpenRouter
KimiPick (API)moonshotai/kimi-k2.5, via OpenRouter

API picks are priced against a design point of roughly $0.004 per run, which is why Kimi runs K2.5 rather than K3: K3's output tokens cost several times that per answer. Check a new model's per-run price against that point before adding it to the menu or bumping a version.

Some models are configured twice on purpose, and the two entries are sold differently:

  • Ungrounded (claude:anthropic-api:...) is an ordinary platform pick on every standard plan. A pick always resolves the ungrounded target, never the grounded one, so a brand cannot spend the premium pool by choosing a platform.
  • Grounded (...:online on an API provider) is the premium tier, counted against the org's pool of prompt/model pairings (prompts.premium_models) — plan included plus purchased add-on. A grounded call is roughly ten times an ungrounded one, which is why it is metered per pairing rather than per brand. Customers assign it in the prompts editor; the LLM settings page reports the totals.

The two tiers are independent, not two rungs of one ladder. A brand's picks run on every one of its prompts; a premium model is chosen on individual prompts and runs in addition to them. One prompt might carry Claude and Grok while the next carries only Grok and a third carries none, and all three still run the brand's four picks. A premium model need not be picked, and picking a model does not discount its premium slot — resolvePromptRunPlan builds the two sets from different inputs (brand.enabledModels and prompt.premiumModels) and simply concatenates them.

Which models are sellable that way is a premium flag in CLOUD_PLATFORMS, not a model id threaded through the code: today Claude, Grok and ChatGPT (GPT Search). Adding another means adding the flag and a grounded target — nothing in the schema, the guards or the run policy names a model.

"Grounded" means a direct API call with the model's own web search on, which is what isGroundedApiTarget decides. Web search alone doesn't say it: every scraped target is :online too, because a consumer surface always searches. It is the combination that costs.

Every pick samples at the plan's rate, whether it is scraped or an API call — a model is not premium, a grounded call is. Only the grounded targets depart, at PREMIUM_RUNS_PER_DAY, which a custom plan can raise through the premiumRunsPerDay override.

Qwen, DeepSeek, Mistral and Kimi carry no :online variant at all: they have no native search tool, so OpenRouter would fall back to Exa, whose results are not a surface a consumer would ever see. They report mentions but no citations, since a run with no web search returns no source annotations — as does any ungrounded pick.

Grok is sold only as a premium target, grounded — it is the one model on this list with no ungrounded entry in the cloud menu. Its own answers run about $0.004 per call against a plan design point near $0.004 per run, because OpenRouter prices Grok's output at several times a scrape and Aeonav pays for output tokens, not queries. One of four picks devoted to it, sampled four times a day, would cost more than the scraped surface it displaced. Metered once daily out of the paid pool, it prices the same way grounded Claude does.

The customer-facing version of this distinction is how the LLM settings page is laid out: three cards — Scraped Engines, LLM APIs and Premium — each explaining in its own description what that group tells you about a brand, with the pick budget and one save bar shared across them. Providers declare which they are; DataForSEO declares both and picks per target, since pinning a model version routes to LLM Responses instead of the scraper.

The third card behaves differently per mode. In cloud it reports the premium pool rather than offering checkboxes, because grounded calls are metered per prompt/model pair. Self-hosted has no pool, so a grounded target there is an ordinary pick.

Those tooltips also carry a per-run cost estimate and a monthly projection for the brand — but only in local mode, where the operator pays the providers. Cloud and whitelabel omit it: the person looking pays a plan price rather than the bills, and the per-provider numbers behind it are internal attribution estimates (packages/lib/src/usage/cost.ts), not quotes.

Local mode also lists the platforms this instance has not configured, with the providers that can serve each and a link into that provider's section of the setup guide. The suggestions come from providersByModel(), derived from STATUS_TARGETS, so every one is a combination the scheduled provider workflow exercises. A test asserts the reverse too: every model in KNOWN_MODELS has a status target, so a platform can't ship on the menu without something watching whether it still works.

How plan limits are enforced

Three layers, all resolving through getOrgEntitlements(orgId):

  1. Write time — brand count, the org-wide tracked-prompt pool, platform picks, and premium assignments are checked in the web server functions and /api/v1 before any insert.
  2. Schedule time — every process-prompt firing re-resolves the org's entitlements and runs only targets that are due at their plan cadence (standard platforms at the plan's samples/day, premium targets once daily), metered against actual prompt_runs history. Canceled or paused orgs stop running; schedule-maintenance revives tracking within minutes of reactivation. A per-org daily run ceiling derived from plan limits caps runaway spend.
  3. Downgrades — nothing is deleted. Resources beyond the new limits simply stop running, oldest-first wins, and the billing page shows the overage.

Every provider call is recorded in usage_events with the org, brand, prompt, target, and a rough cost estimate (tune the per-provider numbers in packages/lib/src/usage/cost.ts against real invoices). When the provider reports its billed cost, the record also keeps provider_cost_usd, provider_endpoint, and provider_request_id. DataForSEO retries sum the cost of every billed attempt, so a failed first call followed by a successful retry cannot disappear from attribution.

Custom plans

Custom plans are configuration, not code. Insert (or update) a row in organization_settings:

INSERT INTO organization_settings (organization_id, entitlement_overrides)
VALUES ('<org-id>', '{
  "planOverride": "custom",
  "maxBrands": 10,
  "maxPrompts": 1000,
  "platformPicks": 6,
  "extraPlatforms": ["kimi"],
  "standardRunsPerDay": 7,
  "premiumPoolIncluded": 100
}')
ON CONFLICT (organization_id) DO UPDATE SET entitlement_overrides = EXCLUDED.entitlement_overrides;

Every field is optional and overrides the org's plan (or the Business plan when planOverride: "custom" marks an org billed outside self-serve — such an org is fully entitled without any Waffo subscription). The shape is validated by entitlementOverridesSchema in packages/config/src/entitlements.ts; malformed JSON falls back to plain plan limits, never to unlimited. extraPlatforms entries must also exist in SCRAPE_TARGETS to actually run.

Notes for self-hosted upgrades

The cloud build-out ships in the same migrations every deployment runs. Plan limits do not: outside cloud, entitlements resolve to unlimited before any query, so every write-time guard and the worker's run policy short-circuit and behavior is unchanged. subscription, organization_settings and prompts.premium_models sit empty or unused.

Two deliberate improvements come along for the ride: the LLMs settings page edits brands.enabledModels directly, and a prompt re-fired early (e.g. by the self-healing scheduler after a provider outage) re-runs only the targets that are actually stale instead of re-sampling every model.

Two things do change for every install. usage_events is written in all modes — one row per provider call, success or failure — because it is the per-run attribution and provider failure-rate record, not a billing-only table. It has no retention job, so it grows with run volume. And migration 0014 deletes duplicate member rows before adding the unique index it should always have had; whitelabel installs are the ones likely to have any, since the Auth0 sync could race itself. It keeps the most-privileged, then oldest, row per (organization, user) and is verified against a seeded database in CI.

Was this page helpful?

On this page

Required environmentBot protectionWaffo setupDiscountsSCRAPE_TARGETS for cloudSupported model versionsHow plan limits are enforcedCustom plansNotes for self-hosted upgrades