Zum Inhalt springen

Sub-system 18: agent payments over the Machine Payments Protocol

Sub-system 18: agent payments over the Machine Payments Protocol

Section titled “Sub-system 18: agent payments over the Machine Payments Protocol”

Status: implemented 2026-09-21. Binding decision: master D59.

Give agents one resource they can buy per request without an account. The transaction runs over the Machine Payments Protocol (MPP, draft-httpauth-payment-01) with the stripe charge method.

  • In scope: GET /api/v1/spotlights/{day}/metrics on the apex, priced at 100 cents EUR per request; the 402 challenge; credential verification; settlement through Stripe; a receipt header; OpenAPI discovery through x-payment-info; documentation in /pricing.md, /developers.md, /agents.md and /llms.txt.
  • Out of scope: sessions (intent=session), other payment methods, an MCP tool for the paid resource, refunds of MPP charges.
  1. The agent requests the resource with no credential.
  2. The apex answers 402, Cache-Control: no-store, and WWW-Authenticate: Payment id, realm, method="stripe", intent="charge", request, expires, description. request is the JCS-serialised, base64url JSON {amount, currency, description, externalId, methodDetails:{networkId,paymentMethodTypes:["card"]}}. The challenge is valid for 5 minutes.
  3. The agent obtains a Stripe Shared Payment Token and retries with Authorization: Payment <base64url JSON {challenge, payload:{spt}}>.
  4. The apex verifies the credential shape and binding: realm, method, intent, request for the same resource, and expiry. It reads the metrics row. A missing day answers 404 before any charge.
  5. The apex calls POST /internal/agent-payments/settle on apps/api over the service binding with the X-NYL-Internal digest. The api confirms a PaymentIntent with payment_method_data.shared_payment_granted_token on Stripe API version 2026-04-22.preview, idempotency key mpp:<challenge id>, and stores one row in agent_payments.
  6. The apex answers 200 with the metrics and Payment-Receipt ({status:"success", method:"stripe", timestamp, reference}).

A replayed credential finds the stored operation on the api, verifies its resource, amount, currency and payment-token fingerprint, and returns the stored receipt. Before calling Stripe, the api persists an attempt in agent_payment_attempts. Uncertain results return 503 and require the same credential and idempotency key. Known pending PaymentIntents are retrieved, not recreated. After 23 hours an unknown PaymentIntent requires operator reconciliation, before Stripe may prune its idempotency key. Expired challenges can reconcile an existing attempt but cannot start a new payment.

agent_payments(challenge_id PK, resource, amount_cents, currency, payment_intent_id, settled_at), migration 0049. Reads seek the primary key.

  • The apex holds no Stripe secret. Only apps/api charges.
  • The settle route accepts the internal host only and a constant-time digest compare of the shared secret.
  • The credential must echo the exact request the apex minted for that resource, so an agent cannot pay for one day and read another.
  • The Stripe SDK 18.5.0 typings do not carry shared_payment_granted_token. The parameter is passed through one typed cast in stripeCharge, pinned by a contract test.
  • One price, one resource. A second paid resource copies the route entry.
  • The challenge id contains a random nonce and an HMAC binding the realm, resource request and expiry, using a domain-separated derivation of the existing handoff secret. Missing signing configuration fails closed. The apex stores no challenge; the api stores payment attempts before settlement.

The same fixed EUR 1 report is available through ACP 2026-04-17 at /checkout_sessions on the API and apex. Create/get/update/complete/cancel persist in migration 0051. OAuth account:read identifies the owner (user and grant); a buyer-approved Stripe SPT separately authorizes the charge. A revoked or replaced grant cannot retrieve its earlier purchases.

All requests require API-Version; mutations also require Idempotency-Key. Server pricing and one settled day are authoritative. Checkout expires after 30 minutes; an atomic freeze prevents complete/cancel and update/complete races. Identical retries preserve payment identity. Unknown outcomes return 503; known payment failures or buyer action return requires_escalation, with no automatic 3DS redirect. Cart changes/cancellation are refused after payment starts. Fulfillment requires confirmed settlement and the owning bearer grant.

The custom com.nowyourlink.stripe.spt handler publishes config and instrument schemas. API production config supplies the merchant account and agent profile; preview fails closed without configuration. This does not implement a card vault, ACP delegate-payment issuance, UCP, AP2 or x402.

Focused protocol, ownership, replay, storage and payment-race tests cover this path. An opt-in test confirms a EUR 1 test-mode Stripe payment and credential replay, using the same settlement adapter:

NYL_STRIPE_TEST_ENV_FILE=/path/to/test.env pnpm --filter @nowyourlink/api exec vitest run --config vitest.sandbox.config.ts

The test rejects non-test keys before making requests. No live charge was made.