# Accept middleware

One edge middleware that recognizes agent orders, verifies the mandate, labels the request for your PSP, and reports the order so you hold a record too. Free, forever.

Status: Live · Updated: 2026-09-13
For: seller
Source: https://enruta.ai/docs/accept-middleware

`@enruta/accept` runs in front of your checkout and answers the question no channel answers for you: this order is from which agent, for whom, under which limit and which policy. It changes nothing about your checkout page, your PSP, your acquirer or your merchant of record, and no money passes through it.

Zero runtime dependencies, Web APIs only (WebCrypto, `fetch`, `Request`, `Response`), so the same code runs on Cloudflare Workers, Vercel Edge, Fastly Compute, Deno and Node 22. Apache-2.0.

## Install

**Cloudflare Worker**

```ts
import { createAccept, toFetchHandler } from "@enruta/accept";

const accept = createAccept({
  domain: "shop.example",
  sellerKey: env.ENRUTA_SELLER_KEY,      // optional: without it the middleware labels but does not report
});

export default { fetch: toFetchHandler(accept, { origin: "https://origin.shop.example" }) };
```

**Vercel Edge / Next proxy**

```ts
// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
import { createAccept, nextHeadersFor } from "@enruta/accept";

const accept = createAccept({ domain: "shop.example", sellerKey: process.env.ENRUTA_SELLER_KEY });

export async function proxy(request: NextRequest) {
  const inspection = await accept.inspect(request);
  const headers = new Headers(request.headers);
  for (const [k, v] of Object.entries(nextHeadersFor(inspection))) headers.set(k, v);
  return NextResponse.next({ request: { headers } });
}

export const config = { matcher: ["/checkout-sessions/:id/complete", "/mcp"] };
```

**Hono**

```ts
import { Hono } from "hono";
import { createAccept, toHonoMiddleware } from "@enruta/accept";

const app = new Hono();
app.use("/checkout-sessions/:id/complete", toHonoMiddleware(createAccept({ domain: "shop.example", sellerKey: process.env.ENRUTA_SELLER_KEY })));
```

**Node / Express**

```ts
import express from "express";
import { createAccept, toExpress } from "@enruta/accept";

const app = express();
app.use(toExpress(createAccept({ domain: "shop.example", sellerKey: process.env.ENRUTA_SELLER_KEY })));
```

| Option | Default | Meaning |
|---|---|---|
| `domain` | required | Your domain, the `payee` a mandate must name |
| `sellerKey` | none | A seller key (`enruta_slr_…`) from the console; with it, completed orders are reported and you get a record |
| `enrutaApiUrl` | `https://api.enruta.ai` |  |
| `platformProfileUrl` | `https://app.enruta.ai/.well-known/agent-profile.json` | Where the mandate keys are fetched (fallback `/.well-known/enruta-keys.json`) |
| `cacheTtlSeconds` | 3600 | Key cache |
| `report` | `true` when `sellerKey` is set | Report completed orders to Enruta |
| `enforce` | `false` | When `true`, a `complete` request with an invalid mandate is answered `402` with the AP2 error code; by default it is passed through labelled `X-Enruta-Mandate-Valid: false` and your own AP2 check decides |
| `onEvent`, `fetch` |  | An event sink for logs; a custom `fetch` |

## What it does to each request

1. **Recognizes the agent.** `Signature-Agent` (RFC 9421 Web Bot Auth: parsed and reported as signed; the signature itself is not checked in this version), the `UCP-Agent` header against a fingerprint table (Google, ChatGPT and OpenAI, Copilot, Muse, RyeBot, Enruta, Anthropic), known agent `User-Agent` substrings, and MCP JSON-RPC bodies.
2. **Verifies the mandate.** On `POST …/checkout-sessions/{id}/complete` and on MCP `tools/call` with `name: "complete_checkout"`, it reads `ap2.checkout_mandate`, verifies the ES256 signature against Enruta’s published keys, checks `payee === domain`, `exp` in the future and `checkout.reference === id`. The credential token in the body is left for you; it is never logged or forwarded elsewhere.
3. **Labels the request** with the headers below, so your own code can write `agent_id + mandate_ref + policy_version` into your PSP’s metadata, your order attributes or your risk engine.
4. **Reports the order.** When you answer `2xx` and the body carries an `order`, it posts `{ domain, order, mandate_jws, checkout_id, source: "middleware" }` to `POST /v0/sellers/orders` with your seller key, in the background where the runtime allows (`ctx.waitUntil`), else with a 3 s timeout, failures ignored. Your response gains `X-Enruta-Record` when the report finished in time.

| Header | Value |
|---|---|
| `X-Enruta-Agent` | `agt_procurement_02` |
| `X-Enruta-Mandate` | `mnd_01M2512M8FQ7ZK4T1V9W3N6BXC` |
| `X-Enruta-Policy-Version` | `4` |
| `X-Enruta-Principal` | `org_halvern` |
| `X-Enruta-Agent-Platform` | `claude`, `chatgpt`, `google`, … or `none` |
| `X-Enruta-Mandate-Valid` | `true` or `false` |

```ts
// accept.inspect(request) → Inspection
{
  agent:   { kind: "ucp" | "web_bot_auth" | "user_agent" | "mcp" | "none", platform: "claude", profile_url: "https://app.enruta.ai/.well-known/agent-profile.json", signed: false },
  mandate: { present: true, valid: true, mandate_id: "mnd_01M2512M8FQ7ZK4T1V9W3N6BXC", agent: "agt_procurement_02",
             policy: { id: "pol_office_supplies", version: 4 }, amount: "24.98", currency: "USD", expires_at: "2026-09-10T17:57:08Z",
             principal_login_id: "halvern/dana.reyes@halvernrobotics.com", reasons: [] }
}
```

## What you get back

With a seller key, every mandated order becomes an [agent order](https://enruta.ai/docs/schema) on your organization’s chain: `labels { agent_id, mandate_ref, policy_version, principal, host }`, a [risk signal](https://enruta.ai/docs/risk-assessment) `{ type: "enruta_mandate_verified", score, metadata { enruta_record, mandate, human_approved, amount_within_mandate, payee_in_mandate } }`, the buyer record’s id and hash, and your own sealed record (`kind: agent_order`, `chain: merchant_view_of`). When your store signs its orders, the record’s subject is `third_party_signed`.

```http
POST /v0/sellers/orders           Authorization: Bearer enruta_slr_…
{ "domain": "reamandrule.com", "order": { "id": "ord_01M2512SM3V3A9R4EBJM8RRRV5", … }, "mandate_jws": "eyJ…", "checkout_id": "chk_01M2511XQ4N7B8T2R6H9K3W5MD", "source": "middleware" }

200 {
  "id": "mor_01M2513C4K8Q2W6T9N1B7D5XRH",
  "labels": { "agent_id": "agt_procurement_02", "mandate_ref": "mnd_01M2512M8FQ7ZK4T1V9W3N6BXC", "policy_version": 4, "principal": "org_halvern", "host": "claude" },
  "risk_signals": [{ "type": "enruta_mandate_verified", "score": 0, "metadata": { "enruta_record": "https://api.enruta.ai/v0/records/rec_01M25129GZMJXTNQ47TMZRDGHN", "mandate": "mnd_01M2512M8FQ7ZK4T1V9W3N6BXC", "human_approved": false, "amount_within_mandate": true, "payee_in_mandate": true } }],
  "seller_record_id": "rec_01M2513CJ7W2T9Q4K8N3B6D1XR",
  "buyer_record_id": "rec_01M25129GZMJXTNQ47TMZRDGHN"
}
```

## Seller keys and domains

A seller organization is created in the console (organization type `seller`); it adds its domains under Settings, and Enruta verifies each by fetching `https://<domain>/.well-known/ucp` and storing its keys and endpoints. A seller key may only act for verified domains. Orders can also reach Enruta without the middleware: the UCP order webhook `order_placed` creates the same agent order when the payee has a seller organization, and the middleware’s report is idempotent per order, so both may run.

| Route | Purpose |
|---|---|
| `POST /v0/sellers/orders` | Report (observe) an agent order; idempotent per `(seller, order_id)` |
| `GET /v0/sellers/orders`, `GET /v0/sellers/orders/{id}` | Your agent orders with labels, risk signals and record ids; filter `order_id=` |
| `POST /v0/sellers/verify` | `{ mandate_jws, domain, checkout?, amount? }` → `{ valid, claims, reasons[] }`: the same check the middleware makes, server-side |
| `GET /v0/sellers/domains`, `POST /v0/sellers/domains` | List and add domains; verification by `ucp_keys`, `dns_txt` or manual |
| `GET /v0/sellers/middleware` | Install snippets with your key placeholder, endpoint status, last agent order seen |

## What is not in this version

- Verification of the Web Bot Auth signature itself (the header is parsed and reported as `signed: true`, `verified: "not_checked"`); Visa TAP and Shopify Signed recognition. Planned.
- Automatic write-back into your PSP: Stripe `PaymentIntent.metadata` and Shopify order attributes are planned for October 2026, Adyen and Checkout.com `metadata` for January 2027. Until then, your code writes the headers where you want them; the Ream & Rule shows the pattern.
- Braintree, PayPal and Worldpay adapters: 2027.

Pricing: Accept is free, permanently: the middleware, `verify_record`, record intake for mandated orders, the [conformance check and badge](https://enruta.ai/docs/conformance), and fulfillment confirmation. The reference merchant at reamandrule.com runs exactly this middleware on its `complete` and `/mcp` routes.
