@atrib/agent
v0.3.3
Published
Client-side middleware for atrib's verifiable action layer. Adds chain context, gap records, and transaction fallback records across MCP framework adapters.
Maintainers
Readme
@atrib/agent
Client-side middleware for atrib's verifiable action layer. Outbound MCP tool calls carry atrib/W3C context, consume upstream signed records, record local gap nodes, and emit signed fallback transaction records when commerce closes. Works with major MCP framework surfaces. Sits above every major agent payment protocol so commerce-closing evidence can join the same trace.
@atrib/agent is the client-side half of the atrib protocol. It keeps outbound MCP calls in the atrib session lifecycle: context goes out, upstream atrib tokens come back in, unsigned hops become local gap nodes, and transaction-shaped responses can produce signed agent-side fallback records. That makes agent work easier to coordinate across calls, handoffs, and later verification. Ordinary tool_call records are signed at the tool boundary by @atrib/mcp, @atrib/mcp-wrap, or an instrumented upstream server.
You set up one atrib() interceptor, plug it into your framework's adapter, and every outbound tools/call from that point on carries W3C trace context, an atrib chain token, and the full atrib session lifecycle. When a payment completes through a supported commerce protocol, the interceptor can consume an upstream atrib record or emit an agent-side fallback transaction record. When commerce never closes the chain, the substrate still serves recall, audit, and cross-agent provenance.
Two coverage surfaces define what you get:
Boundary: this package covers framework middleware and client-side call flow.
Full host runtime integrations are a separate adapter family. Runtimes such as
Claude Code, Codex, OpenClaw, Hermes, Cursor, Goose, and hosted agent systems
own sessions, native tool hooks, approvals, subagents, checkpoints, telemetry,
and run logs. Those integrations compose @atrib/mcp-wrap, host-specific proof
code, @atrib/openinference, @atrib/runtime-log, @atrib/verify, and
hook-class producers instead of turning @atrib/agent into the harness layer.
Install
pnpm add @atrib/agentVerify a local build with pnpm --filter @atrib/agent test.
Coverage Matrix 1: MCP Framework Adapters
| Framework | Package | Adapter helper | Integration shape | Status |
| ------------------------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------- |
| Raw @modelcontextprotocol/sdk Client | @modelcontextprotocol/sdk | wrapMcpClient(client, interceptor, { serverUrl? }) | Proxy-based wrapper, returns new client | ✅ Shipped |
| Claude Agent SDK | @anthropic-ai/claude-agent-sdk | Case A (in-process tools): zero code. the SDK's createSdkMcpServer returns a real McpServer that @atrib/mcp wraps directly | Reuses @atrib/mcp's atrib() middleware on the server side | ✅ Shipped |
| | | Case B (third-party servers): createAtribProxy({ upstream, interceptor }) from @atrib/mcp. in-process surrogate McpServer that forwards to an upstream | Proxy McpServer between SDK and upstream transport | ✅ Shipped |
| Cloudflare Agents | agents | attributeCloudflareAgentMcp(agent, { interceptor, serverUrls }) | Walks agent.mcp.mcpConnections, replaces .client via wrapMcpClient | ✅ Shipped |
| Vercel AI SDK MCP | @ai-sdk/mcp | attributeVercelAiSdkMcp(mcpClient, { interceptor, serverUrl }) | Monkey-patches mcpClient.request() (custom JSON-RPC, not SDK Client) | ✅ Shipped |
| LangChain JS MCP adapters | @langchain/mcp-adapters | High-level: attributeLangchainMcp(multiClient, { interceptor, serverUrls }) | Walks multiClient.config.mcpServers, monkey-patches callTool + fork on each internal Client | ✅ Shipped |
| | | Low-level: wrapMcpClient(rawClient, interceptor) passed to loadMcpTools(name, wrapped) | Reuses raw-SDK wrapper path | ✅ Shipped |
| OpenAI Agents SDK | @openai/agents | (planned. custom transport architecture, not @modelcontextprotocol/sdk) | Planned: subclass MCPServerSSE / MCPServerStdio / MCPServerStreamableHttp | ⏳ Planned |
| Mastra | @mastra/mcp | (planned. receipt proof covers MCPClient + MCPServer over stdio) | Adapter pending | 🧪 Proof |
The pattern across every row is identical: one atrib() interceptor object, one adapter helper call, zero changes to your existing tool invocation code. The name of the helper varies because each host framework exposes a structurally different integration surface, but the ToolCallInterceptor type, the options shape, and the observable behavior are uniform.
Coverage Matrix 2: Agent Payment Protocols
@atrib/agent sits above every major agent payment protocol. It does not implement payments, move money, or enforce transactions; it detects transaction events in the response flow of whichever payment protocol your agent is using, and writes a fallback transaction record when no upstream atrib token already closed the chain. You do not choose a payment protocol at install time; the detection logic for all six (ACP, UCP, x402, MPP, AP2, a2a-x402) runs simultaneously and fires on whichever one your tool responses happen to carry. The fallback record carries an agent signers[] entry over the atrib transaction bytes. Counterparty signatures and payment-receipt evidence are verifier inputs when the protocol supplies them; D052-aware verifiers still flag records that do not meet the required cross-attestation bar.
All detection logic lives in packages/agent/src/transaction.ts and runs against unit tests for each protocol's published spec.
| Protocol | Sponsor / origin | Detection signal | Spec reference |
| ------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| ACP. Agentic Commerce Protocol | Stripe / OpenAI; github.com/agentic-commerce-protocol | status === "completed" + embedded order on /checkout_sessions/{id}/complete, or order_create / order_update webhook | §1.7.1 |
| UCP. Universal Commerce Protocol | github.com/universal-commerce-protocol/ucp | Same shape as ACP + top-level ucp.version envelope | §1.7.2 |
| x402 | Coinbase. github.com/coinbase/x402 | HTTP PAYMENT-RESPONSE header (v2) or legacy X-PAYMENT-RESPONSE (v1) on the 200 response | §1.7.3 |
| MPP. Machine Payments Protocol | Tempo Labs / Stripe; IETF draft-ryan-httpauth-payment-01 | HTTP Payment-Receipt header on 200 success response | §1.7.4 |
| AP2. Agentic Payment Protocol | Google; github.com/google-agentic-commerce/AP2 | Successful AP2 PaymentReceipt or CheckoutReceipt; v0.1 ap2.mandates.PaymentMandate remains a fallback | §1.7.5 |
| a2a-x402 | Google. github.com/google-agentic-commerce/a2a-x402 | A2A task status.message.metadata["x402.payment.status"] === "payment-completed" + receipts[].success === true | §1.7.5 (reported as AP2 crypto path) |
The linking mechanism is the same across all six: the session context_id (16-byte anchor, equal to the W3C OTel trace-id by default) travels with the outbound payment request; via X-atrib-Context HTTP header for protocols that don't expose a free-form metadata field, or via params._meta.atrib for any payment protocol running over MCP transport. When the merchant's side sees the payment-completed signal, atrib writes a transaction record with that context_id, and the attribution graph can reconstruct the full chain from contributing tool calls → transaction → settlement.
You do not install a separate package for each protocol. ACP, UCP, x402, MPP, AP2 and a2a-x402 detection all ship in @atrib/agent and @atrib/mcp by default. Adding a new payment protocol happens by adding a detector in transaction.ts, not by asking users to install anything.
What each detector actually looks for on the wire
These are the exact shapes the production detectTransaction() function in packages/agent/src/transaction.ts matches against. Every shape below is covered by a unit test against a real spec fixture in packages/agent/test/fixtures/, and the verifier question "what does atrib actually detect" has a one-paragraph answer per protocol.
ACP: Stripe / OpenAI Agentic Commerce Protocol
Detected on the success response of POST /checkout_sessions/{id}/complete. Required fields: top-level status: "completed" plus an order object with a string id. The optional order.permalink_url becomes the checkoutUrl for Path 2 content_id derivation per spec §5.4.5.
{
"id": "checkout_session_abc123",
"status": "completed",
"order": {
"id": "ord_xyz789",
"permalink_url": "https://merchant.example.com/orders/ord_xyz789"
}
}The order_create and order_update webhook event shapes are also detected (type: "order_create" plus a data object containing the order fields).
UCP: Universal Commerce Protocol
Identical to ACP plus a top-level ucp envelope with a version string. The envelope is the only structural distinguisher between ACP and UCP, without it the same shape is reported as ACP.
{
"ucp": { "version": "1.0", "capabilities": [] },
"id": "checkout_session_abc123",
"status": "completed",
"order": { "id": "ord_xyz789", "permalink_url": "https://..." }
}x402: Coinbase
Detected entirely from an HTTP response header, not the body. The v2 header is PAYMENT-RESPONSE (per github.com/coinbase/x402); the v1 legacy header X-PAYMENT-RESPONSE is also accepted. Header name lookup is case-insensitive per RFC 7230.
HTTP/1.1 200 OK
PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlfQ==The header value is base64-encoded JSON {success, transaction, network, payer, requirements} but detectTransaction() only checks for the header's presence; the surrounding wrapMcpClient / framework adapter is responsible for capturing response headers and passing them to the detector.
MPP: IETF draft draft-ryan-httpauth-payment-01
Also header-based, but a different header from x402 (the two were conflated in earlier drafts of this code; see DECISIONS.md D016). MPP uses Payment-Receipt per §5.3 of the IETF draft. Both headers may not co-exist on a real response, but if they do, x402 wins (deterministic precedence documented in the test suite).
HTTP/1.1 200 OK
Payment-Receipt: eyJzdGF0dXMiOiJzdWNjZXNzIn0AP2: Google Agentic Payment Protocol (v0.2)
Detected from successful AP2 receipts. Current AP2 uses Checkout and Payment Mandates for authorization, then returns signed Checkout Receipts and Payment Receipts when a verifier accepts or rejects the mandate. atrib treats the successful receipt as the transaction close signal.
AP2 receipt JWT signatures remain verifier evidence. They are not counted as transaction signers[] unless an AP2 participant also signs the atrib transaction record bytes. See D098.
Decoded receipt objects are detected when they carry status: "Success" and the required AP2 fields. Signed receipt JWTs are detected in AP2 sample result envelopes when the envelope has status: "success" plus payment_receipt or checkout_receipt.
For Path 2 fallback transaction records, AP2 detection also returns a protocol-specific contentId when stable identity fields are visible. Decoded Payment Receipts take priority, then compact payment receipt JWT hashes, decoded Checkout Receipts, compact checkout receipt JWT hashes, legacy AP2 PaymentMandates, and a2a-x402 transaction receipts. If none are present, middleware falls back to the MCP server URL plus "checkout" like before.
{
"status": "success",
"order_id": "order_123",
"checkout_receipt": "eyJhbGciOiJFUzI1NiJ9.eyJzdGF0dXMiOiJTdWNjZXNzIn0.signature"
}Mandate-only payloads are not detected, including vct: "mandate.payment.1" and vct: "mandate.checkout.1". Mandates authorize a future action; they do not prove the verifier accepted it.
Verifier-side AP2 / Verifiable Intent checks live in @atrib/verify, not this detector. Use verifyAp2ViEvidence() for decoded receipts or verifyAp2ViEvidenceAsync() for compact signed receipt JWTs when a merchant or auditor needs to validate AP2 receipt references, VI SD-JWT signatures, sd_hash links, disclosure digests, delegated agent keys, and checkout/payment binding after detection.
The older AP2 v0.1 DataPart shape remains supported as a compatibility fallback:
{
"messageId": "b5951b1a-8d5b-4ad3-a06f-92bf74e76589",
"contextId": "sample-payment-context",
"role": "user",
"parts": [
{
"kind": "data",
"data": {
"ap2.mandates.PaymentMandate": {
"payment_details": { "payment_request_id": "order_shoes_123", "...": "..." }
}
}
}
]
}IntentMandate and CartMandate are still not detected; they are upstream funnel events, not transaction events.
a2a-x402: Google AP2 crypto path
Detected from an A2A task whose status.message.metadata contains "x402.payment.status": "payment-completed" and at least one entry in "x402.payment.receipts" with success: true. A payment-completed status with no successful receipt does NOT detect (a failed receipt is not a transaction). Reported as protocol: 'AP2' because a2a-x402 is the AP2 crypto path, not a separate protocol. When a successful receipt exposes a transaction id, Path 2 uses that receipt identity for the AP2 contentId; otherwise it uses the generic AP2 fallback.
{
"kind": "task",
"status": {
"message": {
"metadata": {
"x402.payment.status": "payment-completed",
"x402.payment.receipts": [{ "success": true, "transaction": "0xabc...", "network": "base" }]
}
}
}
}Heuristic fallback (last resort)
If none of the above match, the detector falls back to checking the tool name against a set of common keywords: create_order, complete_checkout, process_payment, place_order, purchase, checkout. A match returns protocol: 'heuristic' and is included in the chain, but it's a weaker signal than any of the protocol-specific detectors above. Use this only when no payment protocol is in play and you want the chain to still close on a domain-specific tool name.
Quick start: one interceptor, any framework
Every adapter wiring looks the same:
import { atrib } from '@atrib/agent'
const interceptor = atrib({
// 32-byte Ed25519 seed in base64url. Generate with:
// node -e 'console.log(Buffer.from(crypto.randomBytes(32)).toString("base64url"))'
creatorKey: process.env.ATRIB_PRIVATE_KEY!,
// Your merchant identity (used for Path 1 transaction detection per §5.4.5).
merchantDomain: 'https://merchant.example.com',
// Canonical URLs of MCP servers this agent will call (drives policy negotiation).
serverUrls: ['https://search.example.com', 'https://shop.example.com'],
// Optional: where to submit signed records. Omit in development.
logEndpoint: process.env.ATRIB_LOG_ENDPOINT,
})That's the interceptor. Now plug it into whichever framework you use.
@atrib/agent types against each host framework structurally and does not
depend on any of them, so install the framework package you use
(@modelcontextprotocol/sdk, @ai-sdk/mcp, agents, @langchain/mcp-adapters,
or @anthropic-ai/claude-agent-sdk) alongside @atrib/agent.
Raw @modelcontextprotocol/sdk
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { wrapMcpClient } from '@atrib/agent'
const raw = new Client({ name: 'my-agent', version: '1.0.0' }, { capabilities: {} })
await raw.connect(transport)
const client = wrapMcpClient(raw, interceptor, {
serverUrl: 'https://my-tool.example.com',
})
// Use `client` anywhere the raw Client would have been used.Claude Agent SDK: Case A (in-process tools, zero atrib code on this side)
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
import { atrib as wrapServer } from '@atrib/mcp' // note: server-side package
import { z } from 'zod' // ships with the Claude Agent SDK
const sdkServer = createSdkMcpServer({
name: 'my-tools',
tools: [tool('search', 'Search the web', { q: z.string() }, async ({ q }) => ({ ... }))],
})
wrapServer(sdkServer.instance, { creatorKey: process.env.ATRIB_PRIVATE_KEY! })
// Pass sdkServer to the Claude Agent SDK as a normal `{ type: 'sdk', ... }` config.
// Attribution flows at the server side; the interceptor on this side is not needed.Claude Agent SDK: Case B (third-party MCP servers via proxy)
import { createAtribProxy } from '@atrib/mcp'
const proxy = await createAtribProxy({
upstream: { type: 'http', url: 'https://my-tool.example.com/mcp' },
interceptor,
})
// Pass `proxy.mcpServer` to the Claude Agent SDK as `{ type: 'sdk', instance: proxy.mcpServer, ... }`.Cloudflare Agents
import { Agent } from 'agents'
import { attributeCloudflareAgentMcp } from '@atrib/agent'
class MyAgent extends Agent {
async onRequest() {
await this.mcp.addMcpServer('search', 'https://search.example.com/mcp')
attributeCloudflareAgentMcp(this, {
interceptor,
serverUrls: { search: 'https://search.example.com' },
})
// ... call MCP tools as normal
}
}Vercel AI SDK
import { createMCPClient } from '@ai-sdk/mcp'
import { streamText } from 'ai'
import { attributeVercelAiSdkMcp } from '@atrib/agent'
const mcpClient = await createMCPClient({
transport: { type: 'http', url: 'https://my-tool.example.com/mcp' },
})
attributeVercelAiSdkMcp(mcpClient, {
interceptor,
serverUrl: 'https://my-tool.example.com',
})
const tools = await mcpClient.tools()
const result = await streamText({ model: 'openai/gpt-5.4', tools, prompt: '...' })LangChain JS
import { MultiServerMCPClient } from '@langchain/mcp-adapters'
import { attributeLangchainMcp } from '@atrib/agent'
const multi = new MultiServerMCPClient({
mcpServers: { search: { transport: 'http', url: 'https://search.example.com/mcp' } },
})
await multi.initializeConnections()
await attributeLangchainMcp(multi, {
interceptor,
serverUrls: { search: 'https://search.example.com' },
})
const tools = await multi.getTools()
// ... pass `tools` to your LangChain agent as normalIn every case: same interceptor, one adapter call, identical behavior. The differences between adapters are forced by differences between host frameworks; not invented by atrib.
What you get
Once the adapter is wired in, every successful tools/call from your agent:
- Carries W3C trace context (
traceparent,tracestate,baggage) inparams._meta, so downstream servers can correlate calls with your OTel traces. - Carries an attribution chain token in
params._meta.atrib, a ~87-char base64url token identifying the prior call in the chain (§1.5.2). - Emits a signed attribution record to the submission queue asynchronously, zero blocking on the hot path (§5.3.5).
- Updates session state with the response's own
_meta.atribtoken, so the next call chains correctly from the current response. - Detects transaction events in the response via the
transaction.tsdetector, across all six payment protocols in coverage matrix 2. When a transaction is detected, a transaction record is emitted linking the sessioncontext_idto the transaction. - Fails silent: if any internal atrib step (signing, submission, interceptor logic) throws, the error is caught, logged with the
atrib:prefix, and the tool call proceeds normally per spec §5.8.
Runnable examples
claude-agent-sdk/. Case A (in-process tools) and Case B (proxy) side-by-sidecloudflare-agents/;McpAgentserver-side andAgentclient-side surfacesvercel-ai-sdk/;createMCPClientwith AI Gateway model routinglangchain-js/;MultiServerMCPClientand the low-levelloadMcpToolspath
Each example directory contains a README.md with framework-specific rationale and a runnable integration.ts snippet.
Failure model (spec §5.8)
The entire atrib integration is wrapped in defensive error handling at every adapter boundary. If any of the following fails, the original tool call continues normally and an atrib:-prefixed warning is logged:
onBeforeToolCallthrows → forward the request with original_meta(no injection)onAfterToolResponsethrows → return the result to the caller anyway- Signing throws → skip the record for that call
- Submission network error → the submission queue retries with exponential backoff; final failure drops the record silently
creatorKeymissing → pass-through mode with one console warning per process
atrib failures never affect the primary tool call or agent response. This is invariant #1 in CLAUDE.md and is enforced by unit tests in each adapter's test file.
Spec references
| Spec section | What it defines |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| §1.3 | JCS canonical serialization of records |
| §1.4 | Ed25519 signing and verification |
| §1.5 | Context propagation via params._meta and W3C trace context |
| §1.7 | Transaction event hooks for all 6 payment protocols |
| §2 | Merkle log protocol (Tessera-backed, tlog-tiles spec) |
| §3 | Graph query interface (fact layer only) |
| §4 | Policy format (merchant-side value distribution) |
| §5.3 | Agent-side middleware behavior |
| §5.4 | Path 1 / Path 2 transaction detection |
| §5.8 | Degradation contract; silent failure never breaks the host |
The full protocol spec is at atrib-spec.md.
A note on documentation links. The atrib protocol repository is currently private (in-progress public preparation). Links in this README to the spec and sister packages (
atrib-spec.md,packages/agent/README.md, etc.) point atgithub.com/creatornader/atrib/blob/main/...URLs that will resolve once the repository goes public. Until then, seeatrib.devfor the protocol overview.
Part of atrib
atrib is an open protocol for verifiable agent actions. Every action becomes a signed, chain-linked record that anyone can verify against a public Merkle log, with no operator to trust. This package is one entrypoint. See the full package family and the protocol spec.
