# For AI agents

> Everything an agent, or the person wiring one up, needs on one page. Ask before acting, follow the decision, retry safely, keep the receipt, and report what happened. Copy-paste setups for Claude Code, the OpenAI Agents SDK, MCP clients and x402.

Source: https://immiscible.fly.dev/docs/ai-agents

This page is written to be read by a model as much as by a person. Every page of these docs is also plain Markdown at the same address with `.md` on the end (this one is [https://immiscible.fly.dev/docs/ai-agents.md](https://immiscible.fly.dev/docs/ai-agents.md)), the core (getting started, the concepts, the answers and the decision API) is one file at [https://immiscible.fly.dev/llms-full.txt](https://immiscible.fly.dev/llms-full.txt), every other section is a file of its own under `/llms-full/`, and the index, which lists them all, is [https://immiscible.fly.dev/llms.txt](https://immiscible.fly.dev/llms.txt).

## Set up Immiscible for this project

If a person has asked you to "Set up Immiscible for this project", do this, in their project folder. They allow the sign-in once in their browser; you do the rest without a terminal prompt.

```bash
npx immiscible login --json        # line 1 has verification_uri_complete: show it to the person and wait for them
npx immiscible init --yes --json   # the agent, its rule, .env, the Claude Code hook (fails closed) and a live test call
npx immiscible doctor --json       # exit 0 when nothing failed
```

- `init` adds `IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY` to `.env` (never replacing a value without `--force`) and, in a Claude Code project, installs the PreToolUse hook in `.claude/settings.json`, so every Bash, Write, Edit, WebFetch and MCP call asks first. Pass `--no-hook` to skip it.
- Not Claude Code? Connect the MCP server instead: `npx immiscible mcp --client <cursor|vscode|windsurf|codex|gemini> --json` prints the entry for that client (see [install in your assistant](#install-in-your-assistant)).
- Exit codes: `3` not signed in (run `login`), `4` a flag is needed (the error names it), `10` the rule waits for another owner. Never paste the agent key into chat, and never commit `.env`.
- Then tell your person what changed: the agent's name, its rule, and the files you touched. To try it first with no account at all, `npx immiscible try` runs a local demo.

## The contract in six lines

1. Before a payment, a release of personal data, an email, an account change or a tool call that reaches another system, describe it to Immiscible: `POST /v1/actions/authorize`, or the MCP tools `request_payment`, `request_personal_data` and `authorize_action`.
2. `allow`: go ahead with exactly that, and keep the signed `receipt`.
3. `approval_required`: do not act; tell your person, and poll until a person decides.
4. `deny`: do not act, and do not try another route; tell your person the reasons.
5. Retry with the same idempotency key, never a new one.
6. Afterwards, settle the action with what actually happened.

## Integrate in under five minutes

1. **Get an agent key.** In the console, **Agents**, **Add an agent**, then **Collect the agent's key** on its setup page. It looks like `ask_...`, is shown once, and can only ask: it cannot approve, widen a rule or lift a freeze.
2. **Give it a rule.** **Agents**, **Agent limits**, **Add a rule**. With no rule (a mandate) for an action type, the answer is `deny` with `no_mandate`; there is no default allowance. See [mandates](https://immiscible.fly.dev/docs/concepts/mandates.md).
3. **Ask.** Set two variables and send one request:

```bash
export IMMISCIBLE_URL=https://immiscible.fly.dev
export IMMISCIBLE_AGENT_KEY=ask_...

curl -X POST "$IMMISCIBLE_URL/v1/actions/authorize" \
  -H "authorization: Bearer $IMMISCIBLE_AGENT_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: invoice-2026-10-northwind" \
  -d '{
    "type": "payment",
    "summary": "Pay the October invoice from Northwind Supplies",
    "payment": { "amount": 42000, "currency": "GBP", "merchant": { "name": "Northwind", "domain": "northwind.example" } },
    "provenance": [{ "source": "user", "detail": "monthly supplier run" }]
  }'
```

4. **Follow the decision** (below), then **settle**:

```bash
curl -X POST "$IMMISCIBLE_URL/v1/actions/act_7Qm2c1f0/settle" \
  -H "authorization: Bearer $IMMISCIBLE_AGENT_KEY" -H "content-type: application/json" \
  -d '{ "status": "completed", "amount": 42000 }'
```

Amounts are always whole minor units: `42000` is £420.00. The [quickstart](https://immiscible.fly.dev/docs/quickstart.md) walks the same steps with the console open beside you.

## Decision semantics

| `decision` | HTTP | What the agent does |
|---|---|---|
| `allow` | `200` | Proceed with exactly what was asked. `receipt` is a signed, single-use token; `expiresAt` says until when |
| `approval_required` | `200` | Do not act. Tell the person you act for; `approval.url` is the link a person decides at. Poll `GET /v1/actions/:id` (or `check_action_status`) every five seconds for a minute, then every thirty. It becomes `allow` or `deny` |
| `deny` | `200` | Do not act and do not try another way. Tell the person the `reasons` |

A refusal is an answer, not an error, so it is HTTP `200`. `reasons` are sentences for people; `risk.signals[].id` are stable codes for code (`over_transaction`, `approve_above`, `rule_of_two`, `lookalike_domain` and the rest are listed in [decisions](https://immiscible.fly.dev/docs/concepts/decisions.md#signals)). When Immiscible cannot reach a decision, the answer is `deny`, never `allow`.

To tell your person why, ask for the explanation: `GET /v1/actions/:id/explain`, or the MCP tool `explain_decision`. It is read only, safe to call at any time, and answers in plain English: the rule the action was judged under, each reason and signal, and what to do next.

## Error handling

HTTP errors mean Immiscible could not evaluate the request at all. Every one carries a stable `type`, a `message`, and, on every `4xx`, a one-sentence `fix` and a `docs` link:

```json
{
  "error": {
    "type": "agent_key_required",
    "message": "this endpoint needs an agent key; issue one for the agent in the console under Agents",
    "fix": "Send an agent key (ask_...), not a workspace or gateway key: in the console open Agents, Add an agent, then Collect the agent's key on its setup page.",
    "docs": "https://immiscible.fly.dev/docs/api/authentication#agent-keys"
  }
}
```

| You get | Do this |
|---|---|
| `400 invalid_request` | correct each field in `error.errors` (`field` is a dotted path into the body) and send again |
| `401 invalid_api_key` | the key is wrong, revoked or missing; do not retry until a person gives you a new one |
| `403 agent_stopped`, or a `deny` with `agent_frozen` | stop everything; a person has stopped this agent and only a person can restart it |
| `409 idempotency_conflict` | you reused a key for a different request; use a new key for a new request |
| `429 rate_limited` | wait `retry-after` seconds, then retry with the same idempotency key |
| `5xx` or no answer | retry with the same idempotency key and back off; if it never answers, do not act. Never treat an error as `allow` |

The full list is in [errors and headers](https://immiscible.fly.dev/docs/api/errors.md). Branch on `error.type`; the `message` is for people and may change.

## Idempotency

Send an idempotency key with every action request, in the body as `idempotencyKey` or as the `Idempotency-Key` header, 1 to 128 printable characters of your choosing, without spaces.

- The same key with the same body returns the same decision, and never asks a person twice. That is how to retry after a timeout.
- The same key with a different body is `409 idempotency_conflict`.
- A new key is a new request. Do not use a fresh key to get a different answer: a burst of attempts trips the `velocity` signal, which asks a person.

The SDKs make the key for you; the OpenAI Agents SDK integration uses the model's tool call id, so a retried tool call is the same action.

## Receipts

An `allow` carries `receipt`: a compact JWS signed with Ed25519, single use, naming the agent, the action, the mandate, the amount and whether a person approved it (`"hum": true`).

- Hand it to whoever needs proof: a merchant, a card issuer, an auditor.
- Anyone can check it with `POST /v1/verify` and `{ "receipt": "eyJ..." }`, no account needed, or offline against the public key set at [https://immiscible.fly.dev/.well-known/immiscible-keys.json](https://immiscible.fly.dev/.well-known/immiscible-keys.json).
- A receipt proves one action was allowed. It does not prove the action happened; settling records that.

More in [receipts](https://immiscible.fly.dev/docs/concepts/receipts.md).

## Copy-paste setups

Claude Code:

```bash
# 1. Immiscible's MCP server, so Claude can ask before paying or sharing data
claude mcp add --transport http immiscible https://immiscible.fly.dev/mcp \
  --header "Authorization: Bearer $IMMISCIBLE_AGENT_KEY"

# 2. The PreToolUse hook, so every Bash, Write, Edit, WebFetch and MCP call is
#    checked before it runs, whether or not the model remembers to ask
mkdir -p ~/.immiscible
curl -fsSL https://immiscible.fly.dev/downloads/claude-code-hook.mjs -o ~/.immiscible/claude-code-hook.mjs
# then add the hook to ~/.claude/settings.json (see "The Claude Code hook")
```

OpenAI Agents SDK:

```ts
import { Agent, run, tool } from '@openai/agents';
import { Immiscible } from '@immiscible/sdk';
import { guardOpenAITools } from '@immiscible/sdk/openai-agents';

const immiscible = new Immiscible().run(); // IMMISCIBLE_URL and IMMISCIBLE_AGENT_KEY

const agent = new Agent({
  name: 'Buyer',
  tools: guardOpenAITools([buy], {
    client: immiscible,
    mapToAction: ({ args }) => Immiscible.paymentAction({ amount: args.pence, currency: 'GBP', merchant: args.domain, provenance: [{ source: 'user' }] }),
  }),
});
await run(agent, 'Renew the team licence at vendor.example.');
```

MCP clients:

```json
{
  "mcpServers": {
    "immiscible": {
      "url": "https://immiscible.fly.dev/mcp",
      "headers": { "Authorization": "Bearer ${env:IMMISCIBLE_AGENT_KEY}" }
    }
  }
}
```

x402:

```ts
import { Immiscible, x402Fetch } from '@immiscible/sdk';

const pay = x402Fetch(new Immiscible(), {
  pay: ({ requirements, paymentRequired }) => myX402Client.createPaymentHeader(requirements, paymentRequired), // your signer, called only on allow
  provenance: [{ source: 'user', detail: 'the analyst asked for this report' }],
});
const res = await pay('https://api.data-vendor.example/v1/quotes');
```

- **Claude Code**: the hook and its settings are in [the Claude Code hook](https://immiscible.fly.dev/docs/guides/mcp-proxy.md#the-claude-code-hook); its model traffic can go through the gateway too (`ANTHROPIC_BASE_URL=https://immiscible.fly.dev/anthropic`).
- **OpenAI Agents SDK**: Python, the guardrail alternative and every option are in [the OpenAI Agents SDK guide](https://immiscible.fly.dev/docs/sdks/integrations/openai-agents.md). Install with `npm install @immiscible/sdk` or `pip install immiscible`.
- **MCP clients**: Claude, ChatGPT and other connector-capable clients can instead add `https://immiscible.fly.dev/mcp` as a custom connector and sign in with OAuth; see [Claude and ChatGPT as connectors](https://immiscible.fly.dev/docs/guides/mcp-proxy.md#claude-and-chatgpt-as-connectors).
- **Any other MCP client** (Cursor, VS Code, Windsurf, Codex, Gemini CLI): `npx immiscible mcp --client <client>` prints its entry; see [add the MCP server](https://immiscible.fly.dev/docs/answers/add-the-mcp-server.md). An AI coding agent can install Immiscible itself with `npx immiscible login --json` then `npx immiscible init --yes --json`, a person allowing the sign-in once.
- **x402**: Immiscible sits between the `402` and your signature and never signs; see [x402 payments](https://immiscible.fly.dev/docs/guides/x402.md).

## Install in your assistant

One line for each, from the packages in [efr7-7/immiscible-sdks](https://github.com/efr7-7/immiscible-sdks). Each one connects to the MCP server at `https://immiscible.fly.dev/mcp` and reads the agent key from `IMMISCIBLE_AGENT_KEY` (which `npx immiscible init` writes to `.env`) or signs in with OAuth.

| Where | Install | What you get |
|---|---|---|
| Claude Code (plugin) | `/plugin marketplace add efr7-7/immiscible-sdks`, then `/plugin install immiscible@immiscible` | the fail-closed hook on every tool call, the MCP server, the `ask-before-acting` skill and the `immiscible-analyst` subagent, which reads approvals, decisions and spend and never acts |
| Claude Code (no plugin) | `npx immiscible init` | the hook in `.claude/settings.json`, the agent, its rule and `.env` |
| Claude Desktop | open `immiscible.mcpb` (built from `packages/immiscible-desktop`) and paste the agent key | the eleven tools, through a local bridge with no dependencies |
| Claude and ChatGPT on the web | add `https://immiscible.fly.dev/mcp` as a custom connector and sign in | the eleven tools, acting as the agent you pick |
| ChatGPT and Codex (plugin) | `packages/immiscible-openai`: a plugin with the MCP server (OAuth) and the `ask-before-acting` skill | the same |
| Cursor | `npx immiscible mcp --client cursor` prints `.cursor/mcp.json` and a one-click `cursor://` install link | the eleven tools |
| VS Code | `code --add-mcp '{"name":"immiscible","type":"http","url":"https://immiscible.fly.dev/mcp"}'` | the eleven tools, with OAuth sign-in |
| Gemini CLI | `git clone https://github.com/efr7-7/immiscible-sdks && gemini extensions install ./immiscible-sdks/packages/immiscible-gemini`, or `gemini mcp add --transport http -H "Authorization: Bearer $IMMISCIBLE_AGENT_KEY" immiscible https://immiscible.fly.dev/mcp` | the eleven tools and the same instructions as context |
| Codex CLI | `codex mcp add immiscible --url https://immiscible.fly.dev/mcp --bearer-token-env-var IMMISCIBLE_AGENT_KEY` | the eleven tools |
| Windsurf (Devin Desktop) | `npx immiscible mcp --client windsurf` prints the entry | the eleven tools |

Every entry is in [add the MCP server](https://immiscible.fly.dev/docs/answers/add-the-mcp-server.md). The plugins, the Desktop bundle and the Gemini extension ship with the 0.1.1 release of the public repository; none is listed in a directory yet.

## Ask a person above an amount

*"How do I stop my Claude Code agent paying more than £500 without approval?"*

1. **Write a payment rule with an approval line.** **Agents**, **Agent limits**, **Add a rule**, kind payment, with **Ask me above** £500. In the API that is a payment mandate with `approveAbove: 50000`, sent by a signed-in owner or admin to [`POST /api/w/:wid/mandates`](https://immiscible.fly.dev/docs/api/post-api-w-wid-mandates.md); `perTransaction` stays the ceiling nobody can talk past (above it is `deny`, not a question):

```json
{
  "agentId": "agt_4f2c91a7",
  "kind": "payment",
  "title": "Claude Code purchases",
  "currency": "GBP",
  "perTransaction": 200000,
  "perPeriod": 500000,
  "period": "month",
  "approveAbove": 50000,
  "newMerchant": "approve"
}
```

2. **Make every payment pass the gate where the amount is visible.** The strongest first:
   - the [card rail](https://immiscible.fly.dev/docs/guides/card-rail.md): bind a Stripe Issuing card (or another issuer through signed webhooks) to the agent, and the issuer asks Immiscible before money moves; no receipt, no payment;
   - the [MCP proxy](https://immiscible.fly.dev/docs/guides/mcp-proxy.md) in front of a payment tool, with a tool map that says where the amount is (`"type": "payment", "amountPath": "amount"`);
   - the MCP server's `request_payment` tool or `POST /v1/actions/authorize`, which the agent calls itself, so pair it with one of the two above.
3. **Close the side doors with the Claude Code hook.** It sends Claude Code's own tools (Bash, Write, Edit, WebFetch) as `tool.call` actions; a `tool.call` rule that lists its domains refuses a shell command that posts to anywhere else, such as a payment API the rule does not name.

The agent's tier still applies on top: a new agent is an intern and a person signs off every payment, which is stricter than £500. For it to pay up to £500 alone it must reach `senior` (pays alone up to £1,000), on evidence. See [autonomy tiers](https://immiscible.fly.dev/docs/concepts/autonomy-tiers.md).

## One budget across OpenAI and Anthropic

*"What's the best way to govern agent spend across OpenAI and Anthropic?"*

1. **Connect the providers.** Under **Settings**, **Connections**, paste your OpenAI and Anthropic keys. Traffic is served on your own contracts; Immiscible never resells inference.
2. **Point every client at the gateway.** One base URL per protocol: OpenAI-shaped at `https://immiscible.fly.dev/v1`, Anthropic-shaped at `https://immiscible.fly.dev/anthropic`, with an Immiscible key instead of the provider's:

```bash
export ANTHROPIC_BASE_URL=https://immiscible.fly.dev/anthropic   # Claude Code, the Anthropic SDK
export OPENAI_BASE_URL=https://immiscible.fly.dev/v1             # the OpenAI SDK and compatible tools
```

3. **Set budgets.** By team, person or workspace, for a calendar month, checked against the projected cost before each call. Amounts are millionths of a US dollar, so `500000000` is $500. With a gateway key issued with the admin scope (see [authentication](https://immiscible.fly.dev/docs/api/authentication.md#workspace-keys)):

```bash
curl -X POST "https://immiscible.fly.dev/v1/admin/budgets" \
  -H "authorization: Bearer $IMMISCIBLE_ADMIN_KEY" -H "content-type: application/json" \
  -d '{ "scope": "org", "baseAllocation": 500000000, "hardCeiling": 600000000, "ownerId": "finance@example.com" }'
```

   Near the allocation the gateway nudges, then routes to cheaper eligible models; at the allocation it answers `402 approval_required` naming the owner; past the ceiling, `429 budget_exhausted`.
4. **Switch to enforce.** Every workspace starts in [shadow mode](https://immiscible.fly.dev/docs/guides/gateway.md#shadow-mode-first), where nothing is blocked, not even an exhausted budget: run a week, read **Assessment**, then switch to **Enforce** under **Rules**, **Models and enforcement**.
5. **Find the spend that bypasses it.** [Discovery](https://immiscible.fly.dev/docs/guides/discovery.md) reads OpenAI, Anthropic and OpenRouter admin APIs for keys and projects outside the gateway, and [finance dashboards](https://immiscible.fly.dev/docs/guides/finance-dashboards.md) put spend by team, provider and agent where finance already looks.

Inference that runs in a vendor's own backend (Devin, GitHub Copilot, Cursor's hosted models) cannot pass through any gateway; it is reconciled from the vendor's API and marked `governed: false`. See [the gateway](https://immiscible.fly.dev/docs/guides/gateway.md#budgets).

## The MCP server

`https://immiscible.fly.dev/mcp` speaks Streamable HTTP (JSON-RPC 2.0, protocol `2025-06-18`, also `2025-03-26` and `2024-11-05`). Authenticate with an agent key as `Authorization: Bearer ask_...`, or an OAuth access token from the connector sign-in. Its tools:

| Tool | Call it | Read only |
|---|---|---|
| `request_payment` | before spending any money | no |
| `request_personal_data` | before giving anyone the person's details | no |
| `authorize_action` | before any other consequential action: email, calendar, account changes, tool calls | no |
| `check_action_status` | to poll after `approval_required` | yes |
| `explain_decision` | to say in plain English why something was allowed, held or refused | yes |
| `settle_action` | once, after an allowed action, with what happened | no |
| `spend_summary` | when the person asks what the company spent on AI, by provider, model, team or key | yes |
| `find_waste` | when they ask what could be cheaper: routing and caching estimates, each an upper bound | yes |
| `unwatched_keys` | when they ask which API keys nobody is watching | yes |
| `set_budget` | to ask for a monthly budget; a person approves before it is set | no |
| `revoke_key` | to ask to switch a key off; a person approves, then an owner confirms | no |

The last five are the AI spend analyst; [add the analyst to Claude](https://immiscible.fly.dev/docs/analyst.md) says how they answer and act. A refused call comes back as a tool result with `isError: true` and text naming the error, the fix and the docs link, so the model can correct itself. The server's card is at [https://immiscible.fly.dev/mcp/server-card](https://immiscible.fly.dev/mcp/server-card).

## Machine-readable

| What | Where |
|---|---|
| Docs index for models | [https://immiscible.fly.dev/llms.txt](https://immiscible.fly.dev/llms.txt) |
| The core of the docs in one file | [https://immiscible.fly.dev/llms-full.txt](https://immiscible.fly.dev/llms-full.txt); every other section at `https://immiscible.fly.dev/llms-full/<section>.txt`, listed in llms.txt |
| Any page as Markdown | the page address plus `.md`, for example [https://immiscible.fly.dev/docs/quickstart.md](https://immiscible.fly.dev/docs/quickstart.md) |
| OpenAPI 3.1 for the decision API | [https://immiscible.fly.dev/openapi.json](https://immiscible.fly.dev/openapi.json) |
| MCP server card | [https://immiscible.fly.dev/mcp/server-card](https://immiscible.fly.dev/mcp/server-card), listed in [https://immiscible.fly.dev/.well-known/ai-catalog.json](https://immiscible.fly.dev/.well-known/ai-catalog.json) |
| Receipt signing keys | [https://immiscible.fly.dev/.well-known/immiscible-keys.json](https://immiscible.fly.dev/.well-known/immiscible-keys.json) |
