Docs · Sellers
Accept middleware
LiveUpdated
Sellers. Written for merchants that receive agent orders.
@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
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" }) };// 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"] };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 })));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
- Recognizes the agent.
Signature-Agent(RFC 9421 Web Bot Auth: parsed and reported as signed; the signature itself is not checked in this version), theUCP-Agentheader against a fingerprint table (Google, ChatGPT and OpenAI, Copilot, Muse, RyeBot, Enruta, Anthropic), known agentUser-Agentsubstrings, and MCP JSON-RPC bodies. - Verifies the mandate. On
POST …/checkout-sessions/{id}/completeand on MCPtools/callwithname: "complete_checkout", it readsap2.checkout_mandate, verifies the ES256 signature against Enruta’s published keys, checkspayee === domain,expin the future andcheckout.reference === id. The credential token in the body is left for you; it is never logged or forwarded elsewhere. - Labels the request with the headers below, so your own code can write
agent_id + mandate_ref + policy_versioninto your PSP’s metadata, your order attributes or your risk engine. - Reports the order. When you answer
2xxand the body carries anorder, it posts{ domain, order, mandate_jws, checkout_id, source: "middleware" }toPOST /v0/sellers/orderswith your seller key, in the background where the runtime allows (ctx.waitUntil), else with a 3 s timeout, failures ignored. Your response gainsX-Enruta-Recordwhen 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 |
// 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 on your organization’s chain: labels { agent_id, mandate_ref, policy_version, principal, host }, a risk signal { 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.
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.metadataand Shopify order attributes are planned for October 2026, Adyen and Checkout.commetadatafor 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, and fulfillment confirmation. The reference merchant at reamandrule.com runs exactly this middleware on its complete and /mcp routes.