Your AI product acts in each customer's own account

Each user connects their own Attio, Notion, Asana or GitHub. Your agent acts there under that user's limit, waits for the user's yes in your app on risky steps, and never sees the user's key. You don't build token storage or an approval backend.

Waiting on your userattio-add-note
pending
for
user_8f3k2
limit
20 changes a day, each user
key
held by Anlyon

This user decides it in your app. It does not reach your team's inbox.

ApproveDeny
your app, your user

Ship an agent and you hold every customer's keys. Then one bug acts in the wrong account.

Acting for customers means storing a credential per customer, keeping it working, and deciding on every call whose it is. It also means a limit per customer and an approval the customer can answer, because your team can't judge a change in someone else's CRM.

Without Anlyon

  • The wrong customer's account

    If the user id passes through the model, a prompt can name someone else.

  • One user uses up everyone's room

    A single shared limit lets one heavy user's agent stop the rest. No limit lets it change anything.

  • An approval nobody can answer

    A review queue in your own dashboard asks your team about data only the customer understands.

One call, for one user. Your server names the user. The model never does.

The SDK binds the user when the client is built, and no tool has a user field, so a prompt can't switch users.

A user who hasn't connected gets a link

Call for a user with no connection and the SDK throws EndUserNotConnectedError with a connect link. The link lasts 10 minutes and works once. The user pastes their own key, Anlyon checks it with the provider, and your server confirms it for whoever is signed in.

  • The key is never shown back, to the user or to you
  • Over MCP, the tool result carries the same link

Each user gets the whole limit

A limit with the scope end_user gives every user their own amount on their own period: 20 changes a day per user. One heavy user stops at their cap, and everyone else keeps working.

  • Reserved before dispatch, so a race can't pass it
  • The agent can't raise it: changing a limit needs a scope no key receives by default

The user approves in your app

A policy decided by the end user sends the approval to that user, not your team's inbox. Your app lists and decides it through the API, and approving runs the parked call.

  • An approval of another user answers 404
  • Your team sees it read-only, under Waiting on your users

Who was it for? Every receipt carries the user.

Invocations, receipts, approvals and audit records carry the end user, and lists filter by it. Show a customer what the agent did in their account from one call. Attio's note is read back by its id, and its title has to match.

GET /api/v2/actions/invocations/inv_01JABCDEF
{  "id": "inv_01JABCDEF",  "actionName": "attio-add-note",  "actionVersionId": "acv_01JABCDEF",  "status": "succeeded",  "grade": "confirmed",  "effectId": "eff_01JABCDEF",  "endUserId": "user_8f3k2",  "responseStatus": 200,  "approvalId": "apr_01JABCDEF",  "error": null,  "retryOf": null}

Why it pays. Less risk, less cost, less effort.

Less risk

A key that requires an end user never touches your workspace's accounts. The model never sees a user's key, and a limit stops one user's agent before the write that crosses it.

Less cost

No store of your users' keys to secure, and no table of per-user counters to keep right when two calls race.

Less effort

The connect form, the key check and the approval list are built, and prebuilt React components render them in your app.

Start from your server. Then invoke for your signed-in user.

Create a key with endUserRequired, then invoke for your signed-in user. When they haven't connected, the error carries the link to send them to.

agent.ts
import { Client, EndUserNotConnectedError } from "@anlyonhq/sdk";const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });try {  const { data } = await anlyon    .forUser(session.userId)    .actions.invoke("attio-add-note", note);  return { status: "done", grade: data.grade };} catch (error) {  if (error instanceof EndUserNotConnectedError) {    return { status: "connect_required", url: error.connectUrl };  }  throw error;}

Let your agent act for your users, safely.

A few lines of server code, then send a user a connect link. Nothing to host.

$ claude mcp add --transport http anlyon https://api.anlyon.com/mcp