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
| Model | Used for | Where |
|---|---|---|
| claude-opus-5-5 | Dynamic save offers. DECIDE_MODEL overrides it. | The API, api/src/lib/decide.ts |
| claude-sonnet-5-5 | The Agent page: answering questions and proposing catalog changes. | The dashboard, offer-app/src/app/api/agent/route.ts |
| claude-haiku-4-5-20251001 | A 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:
| Variable | Service | Without it |
|---|---|---|
| ANTHROPIC_API_KEY | API (offersdk-api) | Cancel flows show their static offer. If a flow has dynamic offers on, the editor says the key is missing. |
| ANTHROPIC_API_KEY | Dashboard (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.formatset 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.
| Guardrail | Range | Default |
|---|---|---|
| kinds | discount, pause, downgrade, incentive | All four |
| max_discount_percent | 1 to 90 | 50 |
| max_discount_cycles | 1 to 24 | 3 |
| max_pause_months | 1 to 12 | 3 |
| max_incentive_months | 1 to 24 | 3 |
| incentives | Incentive ids the model may offer | None |
| downgrade_plans | Plans the model may move the account to | Any cheaper paid plan |
| instructions | Up to 2,000 characters | Empty |
- 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.savedwebhook. 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:
{ "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 getTopAccountsWrite 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 deleteIncentiveEvery 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.
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.