# Quickstart: from key to first governed tool call

Source: https://immiscible.fly.dev/docs/sdks/quickstart

# Quickstart: from key to first governed tool call

Five minutes, in TypeScript or Python. By the end, your agent asks Immiscible before a tool runs, waits for a person when one is asked, runs the tool only if allowed, records what happened, and hands over a receipt anyone can check.

Both SDKs have no runtime dependencies. Both fail closed: if Immiscible cannot be reached, the tool does not run.

## 1. Get an agent key (one minute)

The quickest way is one command in your project: `npx immiscible init` signs you in, creates the agent and its rule, and writes both variables below to `.env`. See [the CLI guide](https://immiscible.fly.dev/docs/cli.md). To do it by hand, in the console:

1. **Agents**, then **Add an agent**. Pick what it does (for this guide, **Something else**), its limits, and who approves; give it a name you will recognise on your phone.
2. Open the agent's setup page and choose **Collect the agent's key**. The **agent key** (`ask_...`) is shown once. It can ask for permission and nothing else.
3. **Agents**, then **Agent limits**, then **Add a rule**, from a template. For this guide take **Coding agent** (tool calls; anything that reaches the network may only go to the domains you name) and **Groceries** (payments at tesco.com, sainsburys.co.uk, ocado.com and three more; asks you above £80).

A new agent starts at the workspace's starting tier: **intern** unless an owner has chosen **junior** in the console. An intern has a person sign off every payment, data release and outside action until it has earned more autonomy, so expect its first calls to ask for approval; that is the system working.

```shell
export IMMISCIBLE_URL=https://immiscible.fly.dev     # the hosted service; your own server if you run one
export IMMISCIBLE_AGENT_KEY=ask_...
```

No server yet? Every example below runs against a built-in fake with `--demo`.

## 2. Install (thirty seconds)

TypeScript (`@immiscible/sdk` on npm: Node 18+, Deno, Bun, browsers and edge; ESM and CommonJS):

```shell
npm install @immiscible/sdk
```

Python (`immiscible` on PyPI: Python 3.9+, standard library only), in a virtual environment:

```shell
python3 -m venv .venv && . .venv/bin/activate
pip install immiscible
```

## 3. Ask before a tool runs (two minutes)

A run is one task: one trace and one session shared by everything the agent does for it. `guard` asks, waits for a person if asked, runs your function only if allowed, then settles.

**TypeScript**

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

const immiscible = new Immiscible();          // IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL
const run = immiscible.run();                 // one trace, one session, per task

const page = await run.guard(
  toolAction('fetch_page', { url: 'https://github.com/acme/api' }),   // a tool.call action
  async () => fetchPage('https://github.com/acme/api'),               // runs only if allowed
);
```

**Python**

```python
from immiscible import Immiscible, tool_action

immiscible = Immiscible()                     # IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL
run = immiscible.run()                        # one trace, one session, per task

with run.guard(tool_action("fetch_page", {"url": "https://github.com/acme/api"})) as decision:
    page = fetch_page("https://github.com/acme/api")                  # runs only if allowed
```

If the answer is no, the TypeScript call throws `ImmiscibleDeniedError` and Python raises it; `err.reasons` says why in plain English and `err.signals` lists the risk signals. Your code never ran.

## 4. When a person must approve (one minute)

Above a mandate's threshold, the answer is `approval_required` and a person is asked on their phone. `guard` waits, backing off between polls (500 ms growing to 8 s, with jitter), for up to ten minutes by default.

**TypeScript**

```ts
const order = await run.pay(
  { amount: 9500, currency: 'GBP', merchant: 'ocado.com', provenance: [{ source: 'user', detail: 'weekly shop' }] },
  (decision) => checkout(cart, decision.receipt),
  { onApprovalRequired: (d) => notify(`Approve at ${d.approval.url}`), timeoutMs: 5 * 60_000 },
);
```

**Python**

```python
payment = Immiscible.payment_action(9500, "GBP", "ocado.com", provenance=[{"source": "user", "detail": "weekly shop"}])
with run.guard(payment, on_approval_required=lambda d: notify(f"Approve at {d.approval_url}"), timeout=300) as decision:
    checkout(cart, decision.receipt)
```

Amounts are in minor units: `9500` is £95.00. To wait yourself, call `authorize`, then `waitForDecision(id, { signal })` / `wait_for_decision(id, cancel=event)`; an abort signal or a cancel event stops the wait without touching the approval.

## 5. Check the receipt (thirty seconds)

An allowed action carries a receipt: a compact JWS signed with Ed25519. Whoever receives the order checks it, offline against keys you pinned, and once online for single use.

**TypeScript**

```ts
import { fetchJwks, verifyReceipt } from '@immiscible/sdk';

const jwks = await fetchJwks(process.env.IMMISCIBLE_URL);     // once; store it with your config
const r = await verifyReceipt(receipt, { jwks, issuer: process.env.IMMISCIBLE_URL, online: true,
  expect: { amount: 9500, currency: 'GBP', merchant: 'ocado.com' } });
if (!r.valid) refuse(r.message);                               // r.reason: expired, bad_signature, replayed, ...
```

**Python**

```python
from immiscible import fetch_jwks, verify_receipt

jwks = fetch_jwks(url)                                         # once; store it with your config
r = verify_receipt(receipt, url, jwks=jwks, online=True, expect={"amount": 9500, "currency": "GBP", "merchant": "ocado.com"})
if not r.valid:
    refuse(r.message)
```

## 6. Run it

The demo ships with both SDKs from version 0.1.1, so it runs from an install, against a built-in fake:

```shell
npx -p @immiscible/sdk immiscible-demo
python3 -m immiscible.demo        # with the virtual environment active
```

Add `--live` to run the same steps against your own workspace (`IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY`). In a clone of the repository, the same code is `packages/immiscible-js/examples/quickstart.mjs` and `packages/immiscible-py/examples/quickstart.py`.

Both print an allowed tool call, a payment a person approved, a valid receipt, and a prompt-injected lookalike (`0cado.com`) refused before any code ran.

## What the SDK does for you

| | |
|---|---|
| Idempotency | Every `authorize` carries an idempotency key, in the body and the `Idempotency-Key` header, reused on every retry. A retry never authorises twice. Framework integrations key on the model's tool call id. |
| Retries | Network errors, 429 and 5xx are retried twice with backoff (honouring `Retry-After`). A settle retried after a lost answer returns the settled action, not a 409. |
| Trace | Every request carries a W3C `traceparent`: the run's trace id, a fresh span. Pass `traceparent` to `run()` to continue your own OpenTelemetry trace. The server's span comes back in `run.context.lastServerTraceparent`. |
| Session | Model calls through the gateway and action requests share one session, so the gate can compare what the agent declares with what entered the model's context. Leave the id out and the gateway issues one (`x-immiscible-session`, bound to your key). |
| Fail closed | No answer means no action. Do not catch the error and carry on. |

## Next

- Route model calls through the gateway in the same run: [gateway helpers](https://immiscible.fly.dev/docs/sdks/overview.md#the-gateway)
- Framework integrations: [OpenAI Agents SDK](https://immiscible.fly.dev/docs/sdks/integrations/openai-agents.md), [LangChain and LangGraph](https://immiscible.fly.dev/docs/sdks/integrations/langchain.md), [Vercel AI SDK](https://immiscible.fly.dev/docs/sdks/integrations/vercel-ai.md), [Claude Code](https://immiscible.fly.dev/docs/sdks/integrations/claude-code.md), [MCP clients](https://immiscible.fly.dev/docs/sdks/integrations/mcp.md)
