AI features

An AI model picks save offers when a customer cancels, and runs the dashboard agent. Both are optional and switch on with an Anthropic API key.

Where the models run

ModelUsed forWhere
claude-opus-5-5Dynamic save offers. DECIDE_MODEL overrides it.The API, api/src/lib/decide.ts
claude-sonnet-5-5The Agent page: answering questions and proposing catalog changes.The dashboard, offer-app/src/app/api/agent/route.ts
claude-haiku-4-5-20251001A short title for each new agent chat.The dashboard, same route.

The two features read their key on different services, so you can turn on one without the other:

VariableServiceWithout it
ANTHROPIC_API_KEYAPI (offersdk-api)Cancel flows show their static offer. If a flow has dynamic offers on, the editor says the key is missing.
ANTHROPIC_API_KEYDashboard (offer-app)The Agent page explains how to enable it, and POST /api/agent returns 503.

Both are sync: false in render.yaml, so Render asks for them on the first deploy and you can leave them empty. Locally, put them in api/.env and offer-app/.env.local.

Dynamic save offers

A cancel flow's offer step can have Dynamic offers turned on. When a customer reaches that step, the API sends the model what it knows about the account and why they're leaving, and the model picks one offer and writes its copy.

What the model sees
The app's name. The customer's plan, price, number of payments, months as a customer, the offer they bought through, and usage against each limit. Their answers to the flow's questions. The offer kinds and limits that apply, and the team's instructions.
What the model returns
JSON that matches a schema built for this request: a short reasoning for the team, the offer kind (or none), its numbers, a plan or incentive id, and a headline, body and button label.
How it's called
The Messages API with structured output (output_config.format set to a JSON schema), low effort, up to 4,000 output tokens, one retry, and a 20 second timeout (DECIDE_TIMEOUT_MS). Plan and incentive ids are enums in the schema, so the model can only name ones that exist.

The system prompt tells the model to match the offer to the reason (price, low use, a missing feature) and to respect a decision no offer can fix, such as switching to a competitor or closing the business. It asks for the smallest offer likely to work, plain copy with no pressure or fake urgency, and no prices in the text.

Guardrails the API enforces

The team sets limits in the cancel flow editor. The API uses them twice: to tell the model what's allowed, and to check the answer. The model's output is never trusted as is.

GuardrailRangeDefault
kindsdiscount, pause, downgrade, incentiveAll four
max_discount_percent1 to 9050
max_discount_cycles1 to 243
max_pause_months1 to 123
max_incentive_months1 to 243
incentivesIncentive ids the model may offerNone
downgrade_plansPlans the model may move the account toAny cheaper paid plan
instructionsUp to 2,000 charactersEmpty
  • Kinds are narrowed to what can apply to this account. Discounts, pauses and downgrades need an active, recurring PayPal subscription; a one-time purchase doesn't qualify. A downgrade also needs an allowed paid plan whose price for the subscription's billing period (monthly or yearly) is lower than what the account pays now. An incentive needs at least one allowed incentive.
  • Numbers are rounded and clamped: discounts to between 5% and the maximum, discount cycles, pause months and incentive months to between 1 and their maximum.
  • A plan or incentive that isn't in the allowed list is dropped. Copy is cut to 120 characters for the headline, 500 for the body and 60 for the button.
  • The reasoning is stored on the session for the dashboard and the cancel_flow.saved webhook. The customer never sees it.

When the model isn't used

The flow falls back to its static offer (the offer set for the customer's answer, else the step's default) when there's no ANTHROPIC_API_KEY, the call fails or times out, the model refuses or runs out of tokens, the JSON doesn't parse, the model picks none, or it picks something that doesn't apply. If no static offer applies either, the step is skipped. The session records which happened in decide:

Session field
{ "decide": { "used": false, "skipped": "ANTHROPIC_API_KEY is not set" } }

Accepting the offer works the same for static and dynamic offers. Discounts and downgrades go to PayPal for approval (see PayPal), pauses run on Render Workflows when configured (see Render), and incentives apply at once. Cancel flows covers the SDK side.

The dashboard agent

The Agent page in each app is a chat with an AI agent about that app's catalog. It runs in the dashboard's own server at POST /api/agent, built on the Vercel AI SDK with its Anthropic provider. Each request uses adaptive thinking with a summary shown in the chat, medium effort, and at most 10 steps.

Only a signed-in user whose active workspace owns the app can use it. It's off for the shared demo login: the page is hidden and POST /api/agent answers 403. Each chat is saved as a thread through the API and is private to the user who started it.

Read tools

These run as soon as the agent calls them:

getAppOverview  getCatalog  showCatalog  searchAccounts
getAccount  findAccountsNearLimits  getUsage  getTopAccounts

Write tools

These change plans, entitlements, add-ons and incentives. Each one stops and shows the user a card with the change. Nothing runs until the user approves it. If the user declines, the agent is told to ask what they want instead of trying again.

createPlan  updatePlan  deletePlan
createEntitlement  updateEntitlement  deleteEntitlement
createAddon  updateAddon  deleteAddon
createIncentive  updateIncentive  deleteIncentive

Every tool goes through the same gateway as the rest of the dashboard, so the agent sees and changes what the dashboard shows. The agent can't change accounts.

Signed approvals

Approvals come back to the server inside the chat history the browser sends, so a client could try to forge one. The server signs each approval with an HMAC when it issues it and checks the signature before a write runs. The key comes from AGENT_APPROVAL_SECRET, then BETTER_AUTH_SECRET, then OFFER_API_ADMIN_KEY.

Running more than one dashboard instance? Set the same AGENT_APPROVAL_SECRET on all of them, or an approval issued by one instance can fail on another.

Agent and MCP shows what using the agent looks like in the dashboard.

Bring your own assistant over MCP

You can also bring your own AI assistant. Each app has an MCP server in the API, and any MCP client connects to it with OAuth or the secret key. That needs no Anthropic API key on your side: the model runs in the client, and the server only serves tools, resources and prompts.