MCP server: @oid4pay/oid4pay-mcp
The OID4Pay MCP server is a stdio-mode Model Context Protocol server that surfaces agent payments as model-callable tools. Wire it into Claude Desktop, Cline, Continue, or any MCP-aware client; the model can then initiate a payment against an enabled merchant without you implementing the protocol by hand. The tools reach the hosted Authorization Server over HTTP; the server never holds settlement logic of its own.
Twelve tools ship in @oid4pay/oid4pay-mcp 0.1.0, covering
registration, payment, mandate verification, merchant discovery, wallet
management, GDPR DSAR, and audit chain queries. These are MCP tools the model
invokes through the MCP protocol; they are not direct JavaScript methods.
License: Apache-2.0.
Install
npm install -g @oid4pay/oid4pay-mcpThe package ships a single binary, oid4pay-mcp, with exactly two
subcommands: register and serve.
Register
Register the agent once, using a one-shot setup token issued by the wallet.
Registration mints a fresh DPoP keypair and a private_key_jwt keypair (if not already present) and calls the Authorization Server's /oauth/register endpoint. The resulting client_id and keys are persisted under ~/.local/share/oid4pay-mcp/ for the serve command
to reuse.
oid4pay-mcp register --setup-token setup_4f6a2c1d8b9e3f70Options:
| Flag | Default | Meaning |
|---|---|---|
--setup-token <code> | required | One-shot setup token issued by the wallet. |
--as-url <url> | https://as.oid4pay.com | Authorization Server base URL. |
--redirect-uri <url> | http://127.0.0.1:8765/oid4ac/callback | Redirect URI the agent uses after consent. |
--client-name <name> | (unset) | Human-readable client label. |
To rotate, delete the persisted state under ~/.local/share/oid4pay-mcp/ and run register again
with a fresh setup token.
Serve
Run the MCP server over stdio. The serve subcommand takes no
options; it reads the persisted registration and keys and speaks the MCP
protocol on stdin and stdout. MCP clients spawn this command and connect over
those streams.
oid4pay-mcp serveWire into an MCP client
Claude Desktop
Add to ~/.config/Claude/claude_desktop_config.json (or the
Windows / macOS equivalent):
{
"mcpServers": {
"oid4pay": {
"command": "oid4pay-mcp",
"args": ["serve"]
}
}
}No environment keys are required; the server reads its registration and keys
from ~/.local/share/oid4pay-mcp/. Run oid4pay-mcp register before starting the client.
Other MCP clients
Cline, Continue, and other MCP-aware toolchains follow the same pattern:
point the client at the oid4pay-mcp command with the single
argument serve. Any client that speaks stdio MCP can drive the
twelve tools.
Token types
Each tool documents which token type the Authorization Server accepts on the wire:
- Agent-only access token: scoped to the agent's own
actions. Carries the agent
client_idassub; does not carry a wallet-session principal. - Wallet-session access token: minted after a
principal-bound consent. Carries the principal resolved by the wallet.
Required for DSAR (GDPR Articles 15 and 20) and audit chain queries.
Agent-only tokens are refused with 401
invalid_token. - One-shot setup token: opaque string issued by the wallet during agent registration. Single-use; bound to the principal who authorised the QR scan or dashboard link.
Tools (table of contents)
The model invokes each tool by name through the MCP protocol. The raw
JSON-RPC tools/call shape is shown per tool for reference; in
practice the MCP client constructs that envelope for you.
| Tool | Purpose |
|---|---|
agent_register | RFC 7591 Dynamic Client Registration. |
agent_payment_initiate | Full agent-payment chain (PAR : authorize : token : verify-mandate). |
agent_verify_mandate | Present an SD-JWT VC mandate + KB-JWT at a merchant /verify-mandate. |
discovery_list_merchants | Directory query (country, currency, rail filters). |
agent_browse_merchant | Signed catalog preview for a single merchant. |
agent_wallet_register | Wallet-side registration with display metadata (client_name, purpose). |
agent_wallet_list_agents | Read the principal's registered agents (wallet:read). |
agent_wallet_revoke_agent | Cascade-revoke a registered agent (wallet:write). |
agent_buy_cheapest_from_store | Fetch catalog : pick cheapest : pay. |
agent_dsar_initiate | GDPR Article 15 (access) or Article 20 (portability) DSAR. |
agent_audit_chain_query | Forensic query against the principal's audit chain entries. |
agent_payment_history | Payment-centric projection over the audit chain. |
agent_register
Generates or loads the agent's DPoP and private_key_jwt keypairs, then calls POST /oauth/register on the Authorization
Server with the supplied setup token. Persists the resulting client_id. RFC 7591 Dynamic Client Registration; client
authentication follows the private_key_jwt assertion.
Subsequent tool calls reuse the persisted registration.
Input schema
{
"type": "object",
"properties": {
"setup_token": { "type": "string", "minLength": 1 },
"as_url": { "type": "string", "format": "uri" },
"redirect_uris": { "type": "array", "minItems": 1,
"items": { "type": "string", "format": "uri" } },
"client_name": { "type": "string" }
},
"required": ["setup_token", "as_url", "redirect_uris"]
}Output
{
"ok": true,
"client_id": "agent_3ff8a1d2b9c4e7f0",
"as_url": "https://as.oid4pay.com"
}Errors
| Operator-facing message | Cause |
|---|---|
agent_register failed: invalid_client_metadata | Authorization Server rejected the client_name or other RFC 7591 fields. |
agent_register failed: invalid_redirect_uri | One of the redirect_uris uses a disallowed scheme. |
agent_register failed: invalid_software_statement | The setup_token is expired, already used, or unbound to this Authorization Server. |
Auth requirement
One-shot setup token issued by the wallet. The token binds the agent to a principal at issue time; the Authorization Server rejects reuse.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "agent_register",
"arguments": {
"setup_token": "setup_4f6a2c1d8b9e3f70",
"as_url": "https://as.oid4pay.com",
"redirect_uris": ["https://my-agent.example.com/callback"],
"client_name": "acme-research-agent"
}
}
}From the CLI this is the same operation as oid4pay-mcp register --setup-token setup_4f6a2c1d8b9e3f70.
agent_payment_initiate
Drives the full payment chain against the configured Authorization Server: PAR : authorize : token : verify-mandate. Returns the mandate id and (when issued) the signed receipt. Every leg follows its wire shape: PAR response, JWT-AT claim set, SD-JWT VC mandate, KB-JWT, DPoP, resource indicators, algorithm whitelist, and authorization code single-use. Settlement is performed by the Authorization Server, reached over HTTP.
Input schema
{
"type": "object",
"properties": {
"merchant_url": { "type": "string", "format": "uri" },
"merchant_base_api": { "type": "string", "format": "uri" },
"merchant_domain": { "type": "string", "minLength": 1 },
"merchant_verify_url": { "type": "string", "format": "uri" },
"amount": {
"type": "object",
"properties": {
"currency": { "type": "string", "length": 3 },
"amount_minor": { "type": "integer", "minimum": 1 }
},
"required": ["currency", "amount_minor"]
},
"line_items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"sku": { "type": "string", "minLength": 1 },
"qty": { "type": "integer", "minimum": 1 },
"unit_price_minor": { "type": "integer", "minimum": 0 },
"currency": { "type": "string", "length": 3 }
},
"required": ["sku", "qty"]
}
},
"offer_id": { "type": "string" },
"offer_digest": { "type": "string" },
"scope": { "type": "string", "default": "openid oid4ac:payment" },
"redirect_uri": { "type": "string", "format": "uri" },
"consent_mode_hint": { "enum": ["auto", "dashboard", "scan"] },
"consent_poll_timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 900 }
},
"required": ["merchant_url", "amount", "line_items", "redirect_uri"]
}Set merchant_base_api to the merchant's live payment API base
(for example https://api.merchant.example); the agent fetches a
single-use nonce and posts the DPoP-bound payment request. The legacy merchant_verify_url is used only when merchant_base_api is absent. Bind offer_digest so
the Authorization Server mints it as a disclosable mandate claim;
omitting it issues a mandate with no offer digest and the merchant rejects
the payment with offer_digest_mismatch.
Output
The tool returns a PayResult. Always branch on the status discriminant: paid means the payment
settled, step_up_required means the human consent poll timed
out (re-poll or re-run with a longer consent_poll_timeout_seconds), setup_required means the principal must complete wallet setup first, and declined means no mandate was issued. On the merchant_base_api path the success result populates status, order_id, receipt, and settlement; on the legacy merchant_verify_url path it populates status, mandate_id, receipt_jws, and payment_provider_ref.
{
"status": "paid",
"order_id": "ord_7b2c4d6e8f0a1b3c",
"receipt": { },
"settlement": { }
}Legacy merchant_verify_url success:
{
"status": "paid",
"mandate_id": "mandate_7b2c4d6e8f0a1b3c",
"receipt_jws": "eyJhbGciOiJFZERTQSIsInR5cCI6InJlY2VpcHQrand0In0...",
"payment_provider_ref": "ch_3OQ8Z2L8KZ4f5G2c0HiJkLmN"
}Non-settled variants:
{
"status": "step_up_required",
"request_uri": "urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c"
}{
"status": "declined",
"error": { "code": "consent_denied" }
}Errors
| Operator-facing message | Cause |
|---|---|
agent_payment_initiate failed: PAR failed: 400 | PAR body rejected: missing authorization_details, bad resource indicator, or stale DPoP nonce. |
agent_payment_initiate failed: token failed: 400 invalid_grant | Authorization code expired or reused. Re-run PAR. |
agent_payment_initiate failed: token failed: 400 invalid_target | The resource indicator does not match the PAR-bound audience (see resource indicators). |
agent_payment_initiate failed: verify-mandate failed: 422 | Merchant rejected the SD-JWT VC presentation (audience mismatch, expired, status-revoked, or KB-JWT nonce wrong). |
agent_payment_initiate failed: verify-mandate failed: 502 | Merchant upstream error; the mandate is still valid; retry with the same idempotency key. |
agent_payment_initiate failed: declined: no mandate issued | Principal declined consent at the wallet; no access token was issued. |
Auth requirement
Agent-registered client_id with a DPoP keypair. The server
reads the persisted registration; agent_register must have run
first.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "agent_payment_initiate",
"arguments": {
"merchant_url": "https://shop.alpacanica.com",
"merchant_base_api": "https://api.alpacanica.com",
"amount": { "currency": "EUR", "amount_minor": 1299 },
"line_items": [{ "sku": "alpaca-sock-blue-43", "qty": 1 }],
"redirect_uri": "https://my-agent.example.com/callback",
"scope": "openid oid4ac:payment"
}
}
}agent_verify_mandate
Posts a compact SD-JWT VC presentation (mandate JWT + selective
disclosures + KB-JWT) to the merchant's /verify-mandate endpoint and returns the verifier's reply. Standalone helper for
re-verification after a network failure. The presentation envelope follows
the SD-JWT VC mandate shape;
KB-JWT key binding enforces the cnf.jkt claim end-to-end.
Input schema
{
"type": "object",
"properties": {
"merchant_verify_url": { "type": "string", "format": "uri" },
"presentation": { "type": "string", "minLength": 1 },
"line_items": { "type": "array",
"items": { "$ref": "#/definitions/LineItem" } },
"offer_id": { "type": "string" },
"idempotency_key": { "type": "string" }
},
"required": ["merchant_verify_url", "presentation", "line_items"]
}Output
The tool returns the merchant verifier's reply verbatim. The SDK pins only
two fields: ok (the verification result) and mandate_id. Any further fields are merchant-defined
passthrough and typed as opaque; do not depend on them.
{
"ok": true,
"mandate_id": "mandate_7b2c4d6e8f0a1b3c"
}Errors
| Operator-facing message | Cause |
|---|---|
agent_verify_mandate failed: verify-mandate failed: 422 mandate_audience_mismatch | Mandate aud claim does not match merchant_verify_url origin. |
agent_verify_mandate failed: verify-mandate failed: 422 mandate_kb_nonce_mismatch | KB-JWT nonce does not match the merchant's challenge nonce. |
agent_verify_mandate failed: verify-mandate failed: 422 mandate_status_revoked | Mandate revoked at the Authorization Server (cascade-revoke or principal-initiated). |
agent_verify_mandate failed: verify-mandate failed: 422 mandate_expired | Mandate exp claim in the past. |
Auth requirement
None at the Authorization Server layer; the presentation carries its own
cryptographic proof. The agent must hold the SD-JWT VC + KB-JWT (issued
during agent_payment_initiate).
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "agent_verify_mandate",
"arguments": {
"merchant_verify_url": "https://shop.alpacanica.com/verify-mandate",
"presentation": "eyJhbGciOiJFZERTQSIs...~WyJzYWx0...~eyJhbGciOiJFZERTQSIs...",
"line_items": [{ "sku": "alpaca-sock-blue-43", "qty": 1 }],
"idempotency_key": "ik_4f6a2c1d8b9e3f70"
}
}
}discovery_list_merchants
Calls GET /merchants on the OID4Pay discovery service and
returns the paginated list of verified merchants. Filters: country (ISO
3166-1 alpha-2), currency (ISO 4217), rail (card, ideal, sepa_debit, etc.), free-text search
against the business name. Pagination via page + page_size (server caps page_size at 100).
Returns merchant identities only (id, audience, business name, country,
accepted currencies and rails); no inventory.
Input schema
{
"type": "object",
"properties": {
"country": { "type": "string", "length": 2 },
"currency": { "type": "string", "length": 3 },
"rail": { "type": "string" },
"q": { "type": "string" },
"page": { "type": "integer", "minimum": 1 },
"page_size": { "type": "integer", "minimum": 1, "maximum": 100 }
}
}Output
{
"merchants": [
{
"id": "merch_alpacanica",
"merchant_audience": "https://shop.alpacanica.com",
"business_name": "Alpacanica Outfitters",
"country": "NL",
"accepts_currency": ["EUR", "USD"],
"accepts_rail": ["card", "ideal", "sepa_debit"],
"verification_status": "verified",
"verified_at": "2026-04-01T00:00:00Z"
},
{
"id": "merch_bristleandslate",
"merchant_audience": "https://shop.bristleandslate.com",
"business_name": "Bristle and Slate",
"country": "NL",
"accepts_currency": ["EUR"],
"accepts_rail": ["card", "ideal"],
"verification_status": "verified",
"verified_at": "2026-04-12T00:00:00Z"
}
],
"page": 1,
"page_size": 20,
"total": 2
}Errors
| Operator-facing message | Cause |
|---|---|
discovery_list_merchants failed: 400 invalid_filter | Filter value rejected (e.g. country not ISO 3166-1, currency not ISO 4217). |
discovery_list_merchants failed: 500 server_error | Discovery service upstream error. |
discovery_list_merchants failed: fetch failed | Transport-level failure (ECONNREFUSED, DNS, TLS). |
Auth requirement
None. The discovery service is unauthenticated for read; results are
filtered to verified, in-good-standing merchants. The base URL is read from
the OID4PAY_DISCOVERY_URL environment variable when set;
otherwise it defaults to the production discovery service.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "discovery_list_merchants",
"arguments": {
"country": "NL",
"currency": "EUR",
"rail": "ideal",
"page": 1,
"page_size": 20
}
}
}agent_browse_merchant
Calls GET /merchants/{id}/catalog-preview on the
OID4Pay discovery service. The discovery service proxies the merchant's
RFC 9421 signed catalog (first page only); the response carries the
merchant's signature, signature_input, and content_digest fields. The server rejects responses missing
any signed field, refusing to act on an unsigned catalog; that is where the HTTP message signature is
enforced.
Input schema
{
"type": "object",
"properties": {
"merchant_id": { "type": "string", "minLength": 1 }
},
"required": ["merchant_id"]
}Output
{
"@context": "https://oid4pay.com/ns/catalog/v1",
"@type": "SignedCatalog",
"merchant": "https://shop.alpacanica.com",
"catalog_url": "https://shop.alpacanica.com/oid4ac/catalog",
"signature": ":MEUCIQDt...:",
"signature_input": "sig1=(\"@method\" \"@target-uri\" \"content-digest\");keyid=\"merch_alpacanica_2026Q2\";alg=\"ed25519\";created=1715750488;expires=1715750548",
"content_digest": "sha-256=:bdaTFvfksp...:",
"items": [
{
"sku": "alpaca-sock-blue-43",
"title": "Alpaca wool sock, sky blue, size 43"
}
]
}Item entries pass through opaque: the SDK pins the top-level @context, @type, merchant, catalog_url, signature, signature_input, content_digest, and items keys, but does not pin per-item fields beyond what the
merchant publishes.
Errors
| Operator-facing message | Cause |
|---|---|
agent_browse_merchant rejected upstream catalog (missing signed field "signature") | Upstream catalog response omitted a signed field; the server refuses to act on an unsigned catalog. |
agent_browse_merchant failed: 404 not_found | No merchant with that id in the directory. |
agent_browse_merchant failed: 429 rate_limited | Discovery service rate-limit. Backoff before retrying. |
agent_browse_merchant failed: 502 bad_gateway | Upstream merchant catalog endpoint unreachable. |
Auth requirement
None. The server verifies the merchant's RFC 9421 signature before returning.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "agent_browse_merchant",
"arguments": {
"merchant_id": "merch_alpacanica"
}
}
}agent_wallet_register
RFC 7591 Dynamic Client Registration against the Authorization Server,
persisting the new client_id and DPoP keypair locally. Differs
from agent_register in that it ships a client_name and purpose that the wallet renders in its agents list (the
user-facing labels). Returns the bound principal id alongside the issued
client id.
Input schema
{
"type": "object",
"properties": {
"setup_token": { "type": "string", "minLength": 1 },
"as_url": { "type": "string", "format": "uri" },
"redirect_uris": { "type": "array", "minItems": 1,
"items": { "type": "string", "format": "uri" } },
"client_name": { "type": "string", "minLength": 1 },
"purpose": { "type": "string", "minLength": 1 },
"scope": { "type": "string", "default": "wallet:read wallet:write" }
},
"required": ["setup_token", "as_url", "redirect_uris", "client_name", "purpose"]
}Output
{
"ok": true,
"client_id": "agent_3ff8a1d2b9c4e7f0",
"principal_id": "principal_8a2c1d8b9e3f70a4",
"client_name": "acme-research-agent",
"purpose": "Find and book research papers under EUR 50/month.",
"scope": "wallet:read wallet:write",
"as_url": "https://as.oid4pay.com"
}Errors
| Operator-facing message | Cause |
|---|---|
agent_wallet_register failed: invalid_client_metadata | client_name empty or violates the Authorization Server validators. |
agent_wallet_register failed: invalid_software_statement | Setup token expired, single-use, or unbound. |
agent_wallet_register failed: invalid_redirect_uri | One of the redirect_uris rejected (disallowed scheme, mismatched host). |
Auth requirement
One-shot setup token issued by the wallet QR scan.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "agent_wallet_register",
"arguments": {
"setup_token": "setup_4f6a2c1d8b9e3f70",
"as_url": "https://as.oid4pay.com",
"redirect_uris": ["https://my-agent.example.com/callback"],
"client_name": "acme-research-agent",
"purpose": "Find and book research papers under EUR 50/month.",
"scope": "wallet:read wallet:write"
}
}
}agent_wallet_list_agents
Runs a wallet:read OAuth dance against the Authorization
Server to mint an access token scoped wallet:read, then GET /wallet/agents. Returns the principal's agents (the
Authorization Server filters on the access token sub claim
resolved by the wallet session, so an agent cannot read another principal's
agents). Errors with insufficient_scope if the Authorization
Server refuses the requested scope.
Input schema
{
"type": "object",
"properties": {
"as_url": { "type": "string", "format": "uri" },
"resource": { "type": "string", "format": "uri" },
"redirect_uri": { "type": "string", "format": "uri" }
},
"required": ["as_url", "resource", "redirect_uri"]
}Output
{
"agents": [
{
"id": "wa_7c1e9b04d8f24a6e",
"client_id": "agent_3ff8a1d2b9c4e7f0",
"label": "acme-research-agent",
"purpose": "Find and book research papers under EUR 50/month.",
"created_at": "2026-05-01T09:14:22Z",
"last_used_at": "2026-05-15T05:21:28Z",
"revoked": false,
"scopes": ["wallet:read", "wallet:write"]
}
]
}Errors
| Operator-facing message | Cause |
|---|---|
agent_wallet_list_agents: insufficient_scope (need wallet:read); ... | Authorization Server refused the wallet:read scope (the principal has not granted it). |
agent_wallet_list_agents failed (401 invalid_token): ... | Access token expired or DPoP proof invalid. |
agent_wallet_list_agents failed (403 forbidden): ... | Token valid but principal mismatch; the Authorization Server rejected the cross-principal request. |
Auth requirement
Wallet-session access token, scope wallet:read.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "agent_wallet_list_agents",
"arguments": {
"as_url": "https://as.oid4pay.com",
"resource": "https://as.oid4pay.com",
"redirect_uri": "https://my-agent.example.com/callback"
}
}
}agent_wallet_revoke_agent
Runs a wallet:write OAuth dance against the Authorization
Server to mint an access token scoped wallet:write, then POST /wallet/agents/{agent_client_id}/revoke. The
Authorization Server cascade-revokes every active token family for the
agent (mandates, refresh tokens, access tokens), as set out in authorization code single-use and /oauth/revoke. Errors with insufficient_scope if the Authorization Server refuses the
requested scope.
Input schema
{
"type": "object",
"properties": {
"as_url": { "type": "string", "format": "uri" },
"agent_client_id": { "type": "string", "minLength": 1 },
"resource": { "type": "string", "format": "uri" },
"redirect_uri": { "type": "string", "format": "uri" }
},
"required": ["as_url", "agent_client_id", "resource", "redirect_uri"]
}Output
{
"ok": true,
"client_id": "agent_3ff8a1d2b9c4e7f0",
"revoked_at": "2026-05-15T05:21:28Z",
"families_revoked": 3
}Errors
| Operator-facing message | Cause |
|---|---|
agent_wallet_revoke_agent: insufficient_scope (need wallet:write); ... | Authorization Server refused the wallet:write scope. |
agent_wallet_revoke_agent failed (404 not_found): ... | No agent with that client_id bound to this principal. |
agent_wallet_revoke_agent failed (409 conflict): ... | Agent already revoked; the cascade is idempotent but the Authorization Server reports the prior state. |
agent_wallet_revoke_agent failed (503 server_error): ... | Authorization Server transient failure; retry safe (revoke is idempotent). |
Auth requirement
Wallet-session access token, scope wallet:write.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "agent_wallet_revoke_agent",
"arguments": {
"as_url": "https://as.oid4pay.com",
"agent_client_id": "agent_3ff8a1d2b9c4e7f0",
"resource": "https://as.oid4pay.com",
"redirect_uri": "https://my-agent.example.com/callback"
}
}
}agent_buy_cheapest_from_store
Fetches the merchant's /.well-known/oid4ac-catalog, picks the
cheapest in-stock item matching the optional category and shipping filters
under max_amount_minor, and pays for it through the full
payment flow. Returns the chosen item and the payment result. Settlement is
performed by the Authorization Server.
Input schema
{
"type": "object",
"properties": {
"store_url": { "type": "string", "format": "uri" },
"merchant_verify_url":{ "type": "string", "format": "uri" },
"max_amount_minor": { "type": "integer", "minimum": 1 },
"currency": { "type": "string", "length": 3, "default": "EUR" },
"category": { "type": "string" },
"ships_to_country": { "type": "string" },
"redirect_uri": { "type": "string", "format": "uri" }
},
"required": ["store_url", "merchant_verify_url", "max_amount_minor", "redirect_uri"]
}Output
{
"chosen": {
"sku": "alpaca-sock-blue-43",
"title": "Alpaca wool sock, sky blue, size 43",
"unit_price_minor": 1299,
"currency": "EUR",
"in_stock": true,
"category": "apparel"
},
"result": {
"status": "paid",
"mandate_id": "mandate_7b2c4d6e8f0a1b3c",
"receipt_jws": "eyJhbGciOiJFZERTQSIsInR5cCI6InJlY2VpcHQrand0In0...",
"payment_provider_ref": "ch_3OQ8Z2L8KZ4f5G2c0HiJkLmN"
}
}Errors
| Operator-facing message | Cause |
|---|---|
agent_buy_cheapest_from_store: no in-stock matching items under the cap | No item satisfies the category, currency, max_amount_minor, and ships_to_country filters. |
agent_buy_cheapest_from_store failed: catalog 404 | The merchant does not serve a catalog at the expected well-known path. |
agent_buy_cheapest_from_store failed: catalog 502 | Upstream merchant catalog endpoint unreachable. |
agent_buy_cheapest_from_store failed: PAR failed: 400 | PAR rejected after the pick; same causes as agent_payment_initiate. |
agent_buy_cheapest_from_store failed: verify-mandate failed: 502 | Mandate was issued but merchant verifier upstream errored; the mandate is still valid; retry verify with the same idempotency key. |
Auth requirement
Agent-registered client_id with a DPoP keypair (same
preconditions as agent_payment_initiate).
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "agent_buy_cheapest_from_store",
"arguments": {
"store_url": "https://shop.alpacanica.com",
"merchant_verify_url": "https://shop.alpacanica.com/verify-mandate",
"max_amount_minor": 2500,
"currency": "EUR",
"category": "apparel",
"redirect_uri": "https://my-agent.example.com/callback"
}
}
}agent_dsar_initiate
Initiates a GDPR Article 15 (access) or Article 20 (portability) Data Subject Access Request on behalf of the principal. The agent presents the principal's wallet-session access token as proof of consent. Article 15 dumps deliver asynchronously to the principal's verified email; Article 20 dumps stream synchronously as JSON in the response. Agent-only tokens are refused (DSAR requires a wallet-session principal). Rate-limit is 10 per day per principal per endpoint.
Input schema
{
"type": "object",
"properties": {
"right": { "enum": ["access", "portability"] },
"principal_token": { "type": "string", "minLength": 1 },
"delivery_preference": {
"enum": ["email", "synchronous_json"],
"default": "email"
}
},
"required": ["right", "principal_token"]
}Output (Article 15 access, asynchronous email)
{
"status": "queued",
"request_id": "dsar_4f6a2c1d8b9e3f70",
"estimated_delivery": "2026-05-15T05:51:28Z"
}Output (Article 20 portability, synchronous JSON)
{
"schema_version": 1,
"request_id": "dsar_4f6a2c1d8b9e3f70",
"principal": {
"principal_id": "principal_8a2c1d8b9e3f70a4",
"wallet_operator": "https://wallet.oid4pay.com"
},
"agent_clients": [...],
"audit_chain_entries": [...]
}Errors
| Operator-facing message | Cause |
|---|---|
agent_dsar_initiate failed: DSAR requires a wallet-session JWT-AT; agent-only tokens are refused | Authorization Server returned 401 invalid_token; the principal token was an agent-only token. |
agent_dsar_initiate failed: rate limited; DSAR caps at 10/day per principal per endpoint | Authorization Server returned 429 rate_limited. |
agent_dsar_initiate failed: insufficient_scope (need privacy:dsar); ... | Wallet-session token does not carry the privacy:dsar scope. |
agent_dsar_initiate failed: agent is not registered yet; call agent_register first | No persisted registration. |
Auth requirement
Wallet-session access token (the principal's), passed in the principal_token input. Agent-only tokens are refused.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "agent_dsar_initiate",
"arguments": {
"right": "access",
"principal_token": "eyJhbGciOiJFZERTQSIs...",
"delivery_preference": "email"
}
}
}agent_audit_chain_query
Forensic query against the audit chain for events involving the authenticated principal. The agent presents the principal's wallet-session access token as proof of consent. Returns the audit entries within the requested time window, scoped to the principal (the Authorization Server filters the chain to the principal, so an agent cannot read another principal's chain). Rate-limit is 30/min per principal. Hard cap of 200 results per call.
Input schema
{
"type": "object",
"properties": {
"principal_token": { "type": "string", "minLength": 1 },
"from_iso": { "type": "string", "format": "date-time" },
"to_iso": { "type": "string", "format": "date-time" },
"event_types": {
"type": "array",
"items": {
"enum": [
"oid4ac.mandate.issued",
"oid4ac.payment.succeeded",
"oid4ac.payment.failed",
"oid4ac.payment.disputed",
"oid4ac.token.revoked",
"oid4ac.privacy.access",
"oid4ac.privacy.erasure"
]
}
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 200,
"default": 50
}
},
"required": ["principal_token"]
}Output
{
"principal_id": "principal_8a2c1d8b9e3f70a4",
"from_iso": "2026-05-01T00:00:00Z",
"to_iso": null,
"event_types": ["oid4ac.payment.succeeded"],
"limit": 50,
"entries": [
{
"seq": 4231,
"ts": "2026-05-15T05:21:28Z",
"event": "oid4ac.payment.succeeded",
"tenant_id": "tenant_oid4pay_prod",
"actor": {
"kind": "agent",
"id": "agent_3ff8a1d2b9c4e7f0"
},
"payload": {
"mandate_id": "mandate_7b2c4d6e8f0a1b3c",
"payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c1AbCdE",
"amount_minor": 1299,
"currency": "EUR",
"merchant_audience": "https://shop.alpacanica.com"
}
}
]
}Errors
| Operator-facing message | Cause |
|---|---|
agent_audit_chain_query failed: audit chain query requires a wallet-session JWT-AT | Authorization Server returned 401 invalid_token (agent-only token). |
agent_audit_chain_query failed: principal_id mismatch | Authorization Server returned 403 principal_mismatch. |
agent_audit_chain_query failed: rate limited; audit chain caps at 30/min per principal | Authorization Server returned 429 rate_limited. |
agent_audit_chain_query failed: insufficient_scope (need audit:read); ... | Wallet-session token does not carry the audit:read scope. |
agent_audit_chain_query failed: agent is not registered yet; call agent_register first | No persisted registration. |
Auth requirement
Wallet-session access token, scope audit:read. Agent-only
tokens are refused.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "agent_audit_chain_query",
"arguments": {
"principal_token": "eyJhbGciOiJFZERTQSIs...",
"from_iso": "2026-05-01T00:00:00Z",
"to_iso": "2026-05-15T23:59:59Z",
"event_types": ["oid4ac.payment.succeeded", "oid4ac.payment.disputed"],
"limit": 100
}
}
}agent_payment_history
Payment-centric projection over the audit chain. Filters the chain to oid4ac.payment.succeeded (and optionally oid4ac.payment.disputed), then projects each entry into a
payment row shape suitable for direct rendering in a chat-style agent UI.
Returns at most 100 payments per call; paginate older payments via from_iso. Inherits the same wallet-session access token
requirement and principal scoping as agent_audit_chain_query.
Input schema
{
"type": "object",
"properties": {
"principal_token": { "type": "string", "minLength": 1 },
"from_iso": { "type": "string", "format": "date-time" },
"to_iso": { "type": "string", "format": "date-time" },
"include_disputed": { "type": "boolean", "default": true },
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 50
}
},
"required": ["principal_token"]
}Output
{
"payments": [
{
"payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c1AbCdE",
"merchant_audience": "https://shop.alpacanica.com",
"amount_minor": 1299,
"currency": "EUR",
"processed_at_iso": "2026-05-15T05:21:28Z",
"status": "succeeded",
"mandate_id": "mandate_7b2c4d6e8f0a1b3c",
"agent_client_id": "agent_3ff8a1d2b9c4e7f0",
"receipt_url": "https://as.oid4pay.com/receipts/r_4f6a2c1d8b9e.json"
},
{
"payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c2XyZ12",
"merchant_audience": "https://shop.bristleandslate.com",
"amount_minor": 4500,
"currency": "EUR",
"processed_at_iso": "2026-05-12T11:08:01Z",
"status": "disputed_pending",
"mandate_id": "mandate_9c3d5e7f1a2b4c6d",
"agent_client_id": "agent_3ff8a1d2b9c4e7f0"
}
],
"total_count": 2,
"has_more": false
}Status values
| Status | Source event |
|---|---|
succeeded | oid4ac.payment.succeeded |
disputed_pending | oid4ac.payment.disputed with dispute_status=pending |
disputed_won | oid4ac.payment.disputed with dispute_status=won |
disputed_lost | oid4ac.payment.disputed with dispute_status=lost |
Errors
| Operator-facing message | Cause |
|---|---|
agent_payment_history failed: payment history query requires a wallet-session JWT-AT | Authorization Server returned 401 invalid_token (agent-only token). |
agent_payment_history failed: principal_id mismatch | Authorization Server returned 403 principal_mismatch. |
agent_payment_history failed: rate limited; payment history caps at 30/min per principal | Authorization Server returned 429 rate_limited. |
agent_payment_history failed: insufficient_scope (need audit:read); ... | Wallet-session token does not carry the audit:read scope. |
agent_payment_history failed: agent is not registered yet; call agent_register first | No persisted registration. |
Auth requirement
Wallet-session access token, scope audit:read. Agent-only
tokens are refused.
Invocation (raw JSON-RPC over the MCP transport)
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "agent_payment_history",
"arguments": {
"principal_token": "eyJhbGciOiJFZERTQSIs...",
"from_iso": "2026-05-01T00:00:00Z",
"to_iso": "2026-05-15T23:59:59Z",
"include_disputed": true,
"limit": 50
}
}
}Library mode
Most integrators wire the server into an MCP client and let the model call
the twelve tools. If you instead want to drive the Authorization Server from
a Node program without going through the MCP layer, the package exports a
single programmatic client: the OID4PayClient class, imported
from the @oid4pay/oid4pay-mcp/lib/oid4ac subpath. It is
constructed with the Authorization Server URL plus the DPoP and client
assertion keys, and exposes methods such as .register(). There
is no separate discovery or wallet client export; discovery and wallet
operations are reached through the MCP tools above.
import { OID4PayClient } from "@oid4pay/oid4pay-mcp/lib/oid4ac";
const client = new OID4PayClient({
asUrl: "https://as.oid4pay.com",
dpopKey, // a stored Ed25519 key
clientAssertionKey, // a stored Ed25519 key
});
const reg = await client.register({
setupToken: "setup_4f6a2c1d8b9e3f70",
redirectUris: ["https://my-agent.example.com/callback"],
clientName: "acme-research-agent",
});
console.log(reg.client_id);Permissions and consent
The server prompts for principal consent through the wallet on every new
merchant audience. Mandates are scoped per merchant; the agent cannot
silently extend a mandate from one merchant to another. The wallet:read + wallet:write scopes gate the
wallet-management tools; audit:read gates the audit-chain and
payment-history tools; privacy:dsar gates the DSAR tool. Scopes
that the principal has not granted come back as insufficient_scope at the Authorization Server.
Algorithm whitelist
The server enforces the algorithm whitelist and refuses
to call any endpoint that returns a JWT or RFC 9421 signature using a
rejected algorithm. EdDSA only for JWT-AT / SD-JWT VC / KB-JWT signatures; ed25519 or ecdsa-p256-sha256 for RFC 9421 HTTP
message signatures. alg=none, HMAC for DPoP, and RS256 outside
the legacy window are refused.
Source
The server lives at oid4pay-mcp/ in the OID4Pay repo. Run oid4pay-mcp --help for the CLI reference, or npm test in the package directory to run the test suite. The
machine-readable tool spec lives at oid4pay-mcp/docs/tool-spec.json. License: Apache-2.0.