# SAM — Agent Integration Contract (v1)

The one page an agent team needs to integrate. SAM's entire agent surface is
**plain HTTP/JSON + `Authorization: Bearer <key>`**. There is **no SAM SDK** —
any runtime that can send an HTTP request works (OpenAI, Cursor/MCP, Hermes,
OpenClaw, custom). Proven by `reference_client.py` (stdlib `urllib` only) passing
S1–S3 with zero dependencies.

## Base URL & leg mount prefixes

One composed service; each leg is a path prefix:

| Leg | Prefix | Does |
|---|---|---|
| SAMShield | `/shield` | Scan untrusted content for prompt-injection / policy risk |
| SAMScope | `/scope` | Issue short-lived scoped credentials |
| SAMHandler | `/handler` | Provision agent identities (+ seat payment) |
| SAMcypher | `/cypher` | Governed payments / transfers / subscriptions |
| sam-license | `/license` | Validate a SAMSerum/Shredder license (gate-on-use) |

`GET /healthz` → liveness + honest leg accounting (`legs_mounted` / `legs_missing`).

## Auth

Every Door-2 call: `Authorization: Bearer <api_key>`. The key is **scoped per
tenant + per permission** — your tenant and your allowed actions are bound to the
key server-side. A smuggled `tenant_id` in a body is ignored (your key decides
your tenant). Exceptions: `/license/validate` (the token IS the credential, no
Bearer) and `/license/mint` (an admin bearer secret, operator-only).

## Envelopes

Success: `{"ok": true, "data": {…}, "request_id": "req_…", "timestamp": "…"}`
Error:   `{"ok": false, "error": {"code": "…", "message": "…", "request_id": "req_…"}}`

Status codes are standard HTTP: `200` ok · `202` escalated (needs owner
approval) · `400/422` bad request/validation · `401` missing/invalid key ·
`402` payment escalated (e.g. a seat payment whose mandate isn't armed) · `403`
permission denied or cross-tenant · `404` not found / not yours · `502`
upstream/gateway. **Never a 500 for an expected business outcome.**

> **Escalation: always check the body `status`, not just the HTTP code.** A
> `POST /cypher/v1/intents` that the policy engine escalates returns **202** with
> `data.status == "escalated"` (+ an `escalation_id`) — it is NOT executed. A
> Handler seat payment that escalates returns **402**. In both cases inspect
> `data.status` / the error `code`; treat `escalated` as "pending owner mandate,"
> not success.

> **Validation errors are your friend.** A `422` lists the valid enum values for
> the offending field in `error` / `ctx.expected`. If you're unsure of an enum,
> send your best guess and read the error — it tells you the allowed set.

## Idempotency (the money contract)

For any charge, **you supply `idempotency_key`** and **reuse it on retry** so the
charge dedupes — there is no safe server-side default. Two properties hold:
- same `(your tenant, key)` → dedupes to the same receipt;
- the same key from a *different* tenant does **not** collide.

## Per-leg endpoint catalog (the common surface)

| Call | Method + path | Required perm | Minimal body |
|---|---|---|---|
| Evaluate content | `POST /shield/v1/policy/evaluate` | `policy:evaluate` | `{content, source_type, context:{}}` |
| Issue credential | `POST /scope/v1/scope/issue` | `scope:issue` | `{service, purpose, ttl_seconds?}` |
| Read credential list | `GET /scope/v1/scope/credentials` | `scope:credentials:read` | — |
| Provision identity | `POST /handler/v1/identities` | (handler) | `{target_provider, tos_attestation_id, identity_attrs:{agent_id,display_name}, idempotency_key}` |
| Submit payment | `POST /cypher/v1/intents` | `wallet:intents:submit` | `{account_id, venue, action_type, asset, amount, reason, requested_by, idempotency_key, destination?}` (`destination` required for TRANSFER/SUBSCRIBE) |
| Read receipt | `GET /cypher/v1/receipts/{id}` | `wallet:receipts:read` | — |
| Arm open mandate | `POST /cypher/v1/mandates` | `wallet:mandates:write` | `{counterparty, constraints:{max_per_transaction,max_cumulative_30d,currency,allowed_action_types}, valid_until}` |
| Validate license | `POST /license/v1/license/validate` | (token) | `{token, fingerprint, nonce}` |

### Field semantics + enums (the things first-time agents have to guess)

- **`account_id`** (intents): your funding/account identifier — an opaque string
  you choose to group a tenant's activity (e.g. `"acct_main"`). It is namespaced
  to your tenant by your key; it is NOT your API key and NOT the credential id.
- **`asset`**: settlement asset, e.g. `"USD"` or `"USDC"`.
- **`venue`** ∈ `ALPACA · KALSHI · CDP · POLYMARKET · STRIPE_ISSUING · X402`.
- **`action_type`** ∈ `TRADE · PREDICT · TRANSFER · BRIDGE · APPROVE · SUBSCRIBE · PAY_API`.
- **`requested_by`** ∈ `PLANNER · EXECUTOR · SYSTEM`.
- **`source_type`** (Shield) ∈ `web_fetch · email · api_response · user_input · agent_generated · memory`.
- **`idempotency_key`**: any unique string you mint and **reuse on retry** (a uuid is fine).
- **`destination`** (intents): the structured counterparty
  `{type, id, rail, display_name?}`. **Required for `TRANSFER`/`SUBSCRIBE`** (the
  mandate gate matches on it); omittable for `PAY_API` within caps. `type` ∈
  `AGENT · PERSON · MERCHANT · VENDOR · API_SERVICE · ASSET`; `rail` is a
  rail-namespace string (`stripe:issuing`, `x402:base`, `alpaca`, …).

### Mandate matching (why a TRANSFER/SUBSCRIBE escalates)

A `TRANSFER`/`SUBSCRIBE` to a counterparty with no armed **open mandate**
escalates (fail-closed, 202 `escalated`) until the owner arms one with
`POST /cypher/v1/mandates`. **The intent's `destination` must match the mandate's
`counterparty`** (same `id` + `rail`) and the intent's `action_type` must be in
the mandate's `allowed_action_types`, within `max_per_transaction` /
`max_cumulative_30d` (the 30-day cap is tracked per `(counterparty, action_type)`).
`PAY_API` executes within caps without a per-counterparty mandate.

### Shield verdict — how to read it

`data.verdict` ∈ `pass · flag · block`; `data.risk_score` 0–100; `data.flags[]`
list the matched categories. Treat `block` as "do not act," `flag` as "act with
caution / log," `pass` as "no policy signal." Shield is pure-regex injection
detection + your tenant's configured rules; `rules_evaluated: 0` means your tenant
has no custom rules yet (not an error) — Shield is a *defense-in-depth* signal, not
a substitute for your own judgment on untrusted content.

## Minimal example (raw HTTP, any language)

```
POST /cypher/v1/intents
Authorization: Bearer sk_live_…
Content-Type: application/json

{"account_id":"acct","venue":"STRIPE_ISSUING","action_type":"PAY_API",
 "asset":"USD","amount":5,"reason":"api fee","requested_by":"EXECUTOR",
 "idempotency_key":"<uuid you reuse on retry>"}
```

## Snippets in this folder

| File | Runtime |
|---|---|
| `reference_client.py` | stdlib `urllib` only — the no-SDK proof (S1–S3) |
| `raw_http_curl.sh` | curl — Hermes / OpenClaw / any shell-capable agent |
| `openai_tool_calling.py` | OpenAI function/tool-calling (tool schema + dispatch handler) |
| `mcp_tool.py` | MCP server (Cursor / Claude Desktop) — the MCP→HTTP bridge |

All four reduce to the same thing: `POST <leg> + Bearer + JSON`.
