diff --git a/api/examples/x402-paid-action.js b/api/examples/x402-paid-action.js deleted file mode 100644 index 5208cba..0000000 --- a/api/examples/x402-paid-action.js +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; - -const { signReceipt, resolveReceiptSigningConfigFromEnv, hasValidSigningConfig } = require('../../lib/receiptSigning'); -const { verifyWithProvider } = require('../../lib/x402ProviderVerification'); - -const seenReceipts = new Map(); -const MAX_TEXT_LENGTH = 4000; - -function buildDeterministicSummary(text) { - const normalized = text.trim().replace(/\s+/g, ' '); - const prefix = normalized.slice(0, 120); - return prefix.length < normalized.length ? `${prefix}…` : prefix; -} - -function normalizePaidActionReceipt(payload, signerId, verificationResult) { - const timestamp = new Date().toISOString(); - const paymentId = payload.payment.payment_id; - const requestId = payload.request_id; - - const receipt = { - receipt_id: `rcpt:x402:${paymentId}:${requestId}`, - signer: signerId, - verb: 'summarize', - source: 'x402.paid_action', - subject: { - type: 'paid_action', - id: requestId, - }, - input: { - action: payload.action, - text: payload.input.text, - payment: { - payment_id: paymentId, - protocol: payload.payment.protocol, - status: payload.payment.status, - asset: payload.payment.asset || null, - amount: payload.payment.amount || null, - network: payload.payment.network || null, - }, - }, - output: { - summary: buildDeterministicSummary(payload.input.text), - payment_accepted: true, - payment_verification_mode: verificationResult.paymentVerificationMode, - }, - execution: { status: 'succeeded' }, - ts: timestamp, - metadata: { - trace: { - trace_id: `x402:${requestId}`, - span_id: 'x402.paid_action.executed', - timestamp, - tags: { - payment_protocol: 'x402', - payment_id: paymentId, - action: 'summarize.text', - payment_verification_mode: verificationResult.paymentVerificationMode, - }, - }, - }, - }; - - if (verificationResult.provider) { - const safeProvider = {}; - if (verificationResult.provider.provider) safeProvider.provider = verificationResult.provider.provider; - if (verificationResult.provider.status) safeProvider.status = verificationResult.provider.status; - if (verificationResult.provider.reference) safeProvider.reference = verificationResult.provider.reference; - if (Object.keys(safeProvider).length) { - receipt.metadata.trace.provider_verification = safeProvider; - } - } - - return receipt; -} - -function parsePayload(body) { - if (!body) return null; - if (typeof body === 'object' && !Array.isArray(body)) return body; - if (typeof body === 'string') { - const trimmed = body.trim(); - if (!trimmed) return null; - return JSON.parse(trimmed); - } - return null; -} - -module.exports = async function handler(req, res) { - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.setHeader('Cache-Control', 'no-store'); - - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); - } - - let payload; - try { - payload = parsePayload(req.body); - } catch { - return res.status(400).json({ ok: false, status: 'malformed_payload' }); - } - - if (!payload || typeof payload !== 'object') { - return res.status(400).json({ ok: false, status: 'malformed_payload' }); - } - - if (!payload.payment || typeof payload.payment !== 'object' || Array.isArray(payload.payment)) { - return res.status(402).json({ ok: false, status: 'payment_required' }); - } - - const { request_id: requestId, action, input, payment } = payload; - - if (!requestId || typeof requestId !== 'string' || !action || typeof action !== 'string') { - return res.status(400).json({ ok: false, status: 'malformed_payload' }); - } - - if (action !== 'summarize.text') { - return res.status(400).json({ ok: false, status: 'unsupported_action' }); - } - - if (!input || typeof input !== 'object' || Array.isArray(input) || typeof input.text !== 'string') { - return res.status(400).json({ ok: false, status: 'malformed_payload' }); - } - - if (input.text.length > MAX_TEXT_LENGTH) { - return res.status(400).json({ ok: false, status: 'malformed_payload' }); - } - - if (payment.protocol !== 'x402' || payment.status !== 'accepted' || typeof payment.payment_id !== 'string' || !payment.payment_id.trim()) { - return res.status(400).json({ ok: false, status: 'payment_invalid' }); - } - - const verificationResult = await verifyWithProvider({ payload, req }); - if (!verificationResult.ok) { - return res.status(verificationResult.httpStatus).json({ ok: false, status: verificationResult.status }); - } - - const dedupeKey = `${requestId}::${payment.payment_id}`; - if (seenReceipts.has(dedupeKey)) { - return res.status(200).json({ ok: true, status: 'PAID_ACTION_EXECUTED_AND_SIGNED', duplicate: true, receipt: seenReceipts.get(dedupeKey) }); - } - - const signingCfg = resolveReceiptSigningConfigFromEnv(); - if (!hasValidSigningConfig(signingCfg)) { - return res.status(503).json({ ok: false, status: 'signing_unavailable' }); - } - - try { - const unsignedReceipt = normalizePaidActionReceipt(payload, signingCfg.signerId || 'runtime.commandlayer.eth', verificationResult); - const receipt = await signReceipt(unsignedReceipt, signingCfg); - seenReceipts.set(dedupeKey, receipt); - return res.status(200).json({ ok: true, status: 'PAID_ACTION_EXECUTED_AND_SIGNED', duplicate: false, receipt }); - } catch { - return res.status(503).json({ ok: false, status: 'signing_unavailable' }); - } -}; - -module.exports._internal = { - clearSeen: () => seenReceipts.clear(), - seenReceipts, - normalizePaidActionReceipt, - buildDeterministicSummary, -}; diff --git a/db/migrations/002_claim_status_machine.sql b/db/migrations/002_claim_status_machine.sql deleted file mode 100644 index ca79f7e..0000000 --- a/db/migrations/002_claim_status_machine.sql +++ /dev/null @@ -1,24 +0,0 @@ -alter table if exists claim_requests - add column if not exists reviewed_at timestamptz, - add column if not exists reviewed_by text, - add column if not exists rejected_at timestamptz, - add column if not exists rejection_reason text, - add column if not exists approved_at timestamptz, - add column if not exists admin_notes text, - add column if not exists last_error text, - add column if not exists last_error_at timestamptz; - -create table if not exists claim_status_transitions ( - id uuid primary key default gen_random_uuid(), - claim_id text not null references claim_requests(claim_id) on delete cascade, - from_status text, - to_status text not null, - action text not null, - actor text, - reason text, - metadata_json jsonb, - created_at timestamptz not null default now() -); - -create index if not exists idx_claim_status_transitions_claim_id - on claim_status_transitions(claim_id); diff --git a/db/migrations/003_agent_cards.sql b/db/migrations/003_agent_cards.sql deleted file mode 100644 index 7fa3d56..0000000 --- a/db/migrations/003_agent_cards.sql +++ /dev/null @@ -1,20 +0,0 @@ -alter table if exists claim_agents - add column if not exists card_url text, - add column if not exists card_status text, - add column if not exists card_published_at timestamptz; - -create table if not exists agent_cards ( - id uuid primary key default gen_random_uuid(), - claim_id text not null references claim_requests(claim_id) on delete cascade, - ens text not null, - card_url text unique not null, - card_json jsonb not null, - version text not null default '1.1.0', - status text not null default 'published', - published_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_agent_cards_claim_id on agent_cards (claim_id); -create index if not exists idx_agent_cards_ens on agent_cards (ens); -create index if not exists idx_agent_cards_card_url on agent_cards (card_url); diff --git a/db/migrations/004_payments.sql b/db/migrations/004_payments.sql deleted file mode 100644 index 2ac50e6..0000000 --- a/db/migrations/004_payments.sql +++ /dev/null @@ -1,27 +0,0 @@ --- 004_payments.sql - -alter table if exists claim_requests - add column if not exists payment_status text, - add column if not exists payment_amount_cents integer, - add column if not exists payment_currency text, - add column if not exists stripe_checkout_session_id text, - add column if not exists stripe_payment_intent_id text, - add column if not exists paid_at timestamptz; - -create table if not exists claim_payments ( - id uuid primary key default gen_random_uuid(), - claim_id text not null references claim_requests(claim_id) on delete cascade, - provider text not null default 'stripe', - stripe_checkout_session_id text unique, - stripe_payment_intent_id text, - amount_cents integer not null, - currency text not null default 'usd', - status text not null, - metadata_json jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_claim_payments_claim_id on claim_payments(claim_id); -create index if not exists idx_claim_payments_status on claim_payments(status); -create index if not exists idx_claim_payments_checkout_session_id on claim_payments(stripe_checkout_session_id); diff --git a/docs/integrations/x402-commandlayer-receipts.md b/docs/integrations/x402-commandlayer-receipts.md deleted file mode 100644 index 0cb62b9..0000000 --- a/docs/integrations/x402-commandlayer-receipts.md +++ /dev/null @@ -1,345 +0,0 @@ -# x402 → CommandLayer Paid-Action Receipt Integration Design - -## 1. Overview - -This document defines a **documentation-only** integration design for representing x402 paid HTTP/API actions as CommandLayer-signed CLAS action receipts. - -The integration separates two concerns that are often conflated: - -1. **Payment settlement attestation (x402 domain)**: whether a payment requirement was negotiated, presented, and accepted under x402-compatible rules. -2. **Execution attestation (CommandLayer domain)**: what action was requested, what runtime executed, and what result was produced, with CLAS `metadata.proof` and signature evidence. - -No production x402 implementation is included here. - -## 2. Why x402 matters for CommandLayer - -x402 introduces a standard interaction model for paid HTTP/API actions. For CommandLayer, this matters because it enables: - -- explicit pre-execution payment gating for premium actions, -- uniform expression of payment requirements and acceptance state, -- deterministic mapping from paid request context to signed CLAS receipts, -- portable post-execution verification using CommandLayer signatures and VerifyAgent. - -x402 improves economic coordination for APIs, while CommandLayer preserves verifiable agent/runtime execution semantics. - -## 3. Trust boundary - -### Boundary A: client ↔ x402 payment negotiation/settlement - -- Covers payment requirement discovery, payment submission, and settlement acceptance/rejection. -- Establishes whether payment conditions were met for a specific paid action request. -- Trust result: "payment was accepted/rejected under the configured x402 provider flow." - -### Boundary B: CommandLayer runtime execution + receipt signing - -- Starts after the runtime decides a paid action is authorized to execute. -- Covers action input capture, execution outcome capture, and CLAS receipt signing. -- Trust result: "CommandLayer attests what executed and what result was returned." - -**Important**: - -- x402 payment proof is **not** a CLAS action receipt. -- x402 does **not** prove that the agent/runtime executed the action correctly. -- Public third-party verification begins when CommandLayer emits a signed CLAS receipt. - -## 4. Paid-action lifecycle - -1. **Client requests paid action**. -2. **Server returns `402 Payment Required`** with x402 payment requirements. -3. **Client pays through x402** and resubmits proof/payment context. -4. **Server verifies/accepts payment** using configured x402 provider adapters. -5. **Agent/runtime executes action** under CommandLayer policy. -6. **CommandLayer emits signed receipt** containing execution and payment linkage metadata. -7. **VerifyAgent verifies receipt** (signature integrity, policy checks, trace coherence). - -## 5. CLAS receipt shape - -A CLAS paid-action receipt should encode execution truth plus payment linkage, while keeping payment settlement and execution proof semantically separate: - -```json -{ - "receipt_id": "rcpt:clas:act_01JW7R6Y8W8JQ5H7GH2D0P0F8N", - "action": "summarize.text", - "status": "succeeded", - "requested_at": "2026-05-22T12:00:00Z", - "executed_at": "2026-05-22T12:00:02Z", - "result": { - "summary": "Short technical summary..." - }, - "metadata": { - "trace": { - "request_id": "req_9f2f5f25", - "payment_id": "pay_x402_7f31", - "provider": "x402-compatible", - "workflow_id": "wf_2a10" - }, - "proof": { - "payment": { - "scheme": "x402", - "settlement_status": "accepted", - "payment_ref": "pay_x402_7f31" - }, - "execution": { - "runtime_id": "rt_prod_1", - "agent_id": "agent_summarizer_v3", - "policy_hash": "sha256:ab12..." - } - } - }, - "proof": { - "signature": [ - { - "role": "runtime", - "alg": "Ed25519", - "key_id": "cl_runtime_key_2026_01", - "sig": "base64..." - } - ] - } -} -``` - -## 6. `metadata.trace` usage - -`metadata.trace` provides correlation fields across request ingress, payment processing, execution, and verification. - -Recommended minimum fields: - -- `request_id`: idempotency/correlation anchor for the paid action attempt, -- `payment_id`: x402 payment correlation identifier, -- `receipt_id`: CLAS artifact identifier (may also appear top-level), -- `workflow_id` or equivalent runtime correlation identifier. - -Guidelines: - -- include stable identifiers, not secrets; -- avoid raw private keys, bearer tokens, or full card/wallet secrets; -- keep trace values deterministic enough for replay analysis and duplicate detection. - -## 7. `proof.signature` roles - -CommandLayer supports single-signature and multi-signature role entries. For paid actions, roles may include: - -- `user`: indicates end-user assent or request signing (optional). -- `payer`: indicates payer-side cryptographic attestation where available (optional and provider-dependent). -- `agent`: attests agent-level transformation/decision output. -- `runtime`: attests execution envelope and canonical receipt emission. -- `verifier`: attests post-hoc verification outcome in derived or companion receipts. - -Role notes: - -- Minimum public-verification baseline is usually a valid `runtime` signature. -- Multi-signature receipts should preserve explicit role labeling to avoid signature ambiguity. - -## 8. Example: paid summarize action - -### Paid action request - -```json -{ - "request_id": "req_9f2f5f25", - "action": "summarize.text", - "input": { - "text": "Long technical document..." - }, - "payment": { - "required": true, - "plan": "pro", - "max_amount": "0.05", - "currency": "USD" - } -} -``` - -### Payment accepted event - -```json -{ - "event": "payment.accepted", - "request_id": "req_9f2f5f25", - "payment_id": "pay_x402_7f31", - "provider": "x402-compatible", - "settled_amount": "0.05", - "currency": "USD", - "accepted_at": "2026-05-22T12:00:01Z" -} -``` - -### CLAS receipt after execution - -```json -{ - "receipt_id": "rcpt:clas:act_01JW7R6Y8W8JQ5H7GH2D0P0F8N", - "request_id": "req_9f2f5f25", - "payment_id": "pay_x402_7f31", - "action": "summarize.text", - "status": "succeeded", - "result": { - "summary": "Short technical summary..." - }, - "proof": { - "signature": [ - { - "role": "runtime", - "alg": "Ed25519", - "key_id": "cl_runtime_key_2026_01", - "sig": "base64..." - } - ] - } -} -``` - -## 9. Example: paid verification call - -A verifier-facing flow can be payment-gated while still yielding a CLAS-verifiable output artifact: - -1. Client requests `verify.receipt` for a target receipt. -2. Service returns `402` with verification pricing requirements. -3. Client completes x402 payment flow. -4. Service executes VerifyAgent and/or runtime verification. -5. Service returns verification result plus signed verification receipt. - -Illustrative response payload: - -```json -{ - "action": "verify.receipt", - "target_receipt_id": "rcpt:clas:act_01JW7R6Y8W8JQ5H7GH2D0P0F8N", - "verification": { - "status": "valid", - "checks": ["signature", "trace_consistency", "policy_constraints"] - }, - "proof": { - "signature": [ - { - "role": "verifier", - "alg": "Ed25519", - "key_id": "cl_verify_key_2026_01", - "sig": "base64..." - } - ] - } -} -``` - -## 10. Failure modes - -- **payment missing**: no valid payment context after `402` requirement; action is not executed. -- **payment invalid**: payment proof fails provider validation; action is not executed. -- **payment accepted but action failed**: receipt should record `status: failed` with structured error outcome. -- **action executed but receipt signing failed**: execution occurred but no portable attestation; must emit internal critical event and retry/compensate. -- **duplicate payment/action request**: idempotency logic must prevent duplicate execution and duplicate receipts. -- **verifier unavailable**: verification action cannot complete; return explicit retriable/unavailable state. - -## 11. Idempotency - -Use three distinct identifiers: - -- `request_id`: semantic action-attempt identity. -- `payment_id`: settlement identity in x402 provider domain. -- `receipt_id`: CLAS attestation artifact identity. - -Recommended constraints: - -- one `request_id` maps to at most one canonical execution outcome; -- one accepted `payment_id` is bound to one request scope/policy scope; -- one canonical execution outcome maps to one canonical `receipt_id` (retries should not fork receipt truth). - -## 12. Settlement vs execution proof - -Settlement proof answers: **"Was payment accepted for this request scope?"** - -Execution proof answers: **"What action executed, under what runtime context, with what result?"** - -Design rule: keep these proofs linked but non-substitutable. A valid payment signal is insufficient to claim execution correctness; a valid execution receipt does not imply payment settlement unless explicitly linked. - -## 13. Future integration with Coinbase AgentKit / Agentic Wallet - -Future adapters may use Coinbase AgentKit or Agentic Wallet primitives as one x402-compatible payment backend. In that model: - -- AgentKit/Wallet tools can originate payer intents and settlement references. -- CommandLayer still owns canonical action execution receipt emission. -- Provider abstraction remains open so non-Coinbase x402-compatible providers can be added without changing CLAS receipt semantics. - -## 14. Non-goals - -- This design does **not** claim CommandLayer settles payments. -- This design does **not** claim x402 alone proves agent/runtime execution. -- This design does **not** require Coinbase/CDP as the only x402 provider. -- This design does **not** introduce production x402 runtime code in this change. - -## Signed example endpoint - -A server-side example endpoint is available at `POST /api/examples/x402-paid-action` (`api/examples/x402-paid-action.js`). - -### Request example - -```json -{ - "request_id": "req_9f2f5f25", - "action": "summarize.text", - "input": { - "text": "Long technical document..." - }, - "payment": { - "payment_id": "pay_x402_7f31", - "protocol": "x402", - "status": "accepted", - "asset": "USDC", - "amount": "0.01", - "network": "base" - } -} -``` - -### Success status - -The endpoint returns status `PAID_ACTION_EXECUTED_AND_SIGNED` with a signed CLAS-style receipt. - -### Verification modes - -The example supports two server-side payment verification modes: - -- `demo_accepted_envelope` (default): used when `X402_PROVIDER_VERIFICATION_URL` is not configured. In this mode, the endpoint accepts the declared `payment.protocol = x402` + `payment.status = accepted` envelope and marks the receipt with `payment_verification_mode: "demo_accepted_envelope"`. -- `provider_verified`: enabled only when `X402_PROVIDER_VERIFICATION_URL` is configured. In this mode, the server posts the payment envelope and request metadata to the provider verification endpoint and executes only when provider verification indicates accepted/settled payment. - -Optional provider auth: - -- `X402_PROVIDER_API_KEY`: when set, the server sends `Authorization: Bearer ` to the provider verification endpoint. -- Keys are not returned in API responses or receipt metadata. - -Failure mapping in provider mode: - -- Provider payment rejection: `400 payment_invalid` or `402 payment_required`. -- Provider unavailable/network/malformed response: `503 payment_provider_unavailable`. - -### Verification command - -You can verify the returned receipt with the existing verify endpoint: - -```bash -curl -sS -X POST http://localhost:3000/api/verify \ - -H 'content-type: application/json' \ - -d '{"receipt": {"...": "signed receipt payload"}}' -``` - -### Trust boundary reminder - -x402 payment acceptance is not the same as execution proof. Payment rails attest payment acceptance/settlement state, while CommandLayer receipts attest the requested action, execution result, and signer-bound proof for that execution. - - -## Verified working flow - -Example verified result pattern (redacted for safety): - -- status: `PAID_ACTION_EXECUTED_AND_SIGNED` -- verb: `summarize` -- `/api/verify` result: `VERIFIED` -- hash_matches: `true` -- signature_valid: `true` -- key_id: `vC4WbcNoq2znSCiQ` - -This shows a paid action can emit signed execution proof after payment verification succeeds under the active mode. - -Important: CommandLayer proves execution and receipt integrity; x402 + the configured provider prove payment acceptance/settlement. The default example remains a demo accepted-envelope flow unless `X402_PROVIDER_VERIFICATION_URL` is configured. diff --git a/docs/ops/environment.md b/docs/ops/environment.md index 22a27ef..b6f7dac 100644 --- a/docs/ops/environment.md +++ b/docs/ops/environment.md @@ -25,8 +25,6 @@ For production ENS-related lookups, configure **one explicit mainnet RPC endpoin | `CL_RECEIPT_SIGNER_ID` | Required (for CL-prefixed signing path) | `lib/receiptSigning.js`, webhook/x402 APIs | Canonical signer ID used in receipt proof metadata. | `runtime.commandlayer.eth` | | `CL_RECEIPT_SIGNING_KID` | Required (for CL-prefixed signing path) | `lib/receiptSigning.js`, webhook/x402 APIs | Key identifier for receipt signature metadata. | `kid_prod_2026_01` | | `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` | Required (legacy/non-CL path) | `lib/receiptSigning.js`, webhook/x402 APIs | Base64-encoded PEM private key used to sign receipts when legacy variable path is used. | `` | -| `X402_PROVIDER_VERIFICATION_URL` | Optional (required for provider-verification mode) | `lib/x402ProviderVerification.js`, `api/examples/x402-paid-action.js` | External payment-provider verification endpoint for x402 flow. | `https://provider.example.com/verify` | -| `X402_PROVIDER_API_KEY` | Optional | `lib/x402ProviderVerification.js` | Bearer token for provider-verification endpoint authentication. | `` | ## Notes diff --git a/examples/x402-paid-action-receipt/.env.example b/examples/x402-paid-action-receipt/.env.example deleted file mode 100644 index eb77c08..0000000 --- a/examples/x402-paid-action-receipt/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# Optional server settings -PORT=4000 -WORKFLOW_ID=wf_example_local - -# Placeholder key id only. Do not place private signing keys in this example. -RUNTIME_SIGNING_KEY_ID=cl_runtime_key_example_2026 diff --git a/examples/x402-paid-action-receipt/README.md b/examples/x402-paid-action-receipt/README.md deleted file mode 100644 index 757e9fb..0000000 --- a/examples/x402-paid-action-receipt/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# x402 Paid Action → CLAS Receipt (Example Only) - -This example demonstrates a **mock** integration flow where a paid action request is linked to a simulated x402 payment acceptance event and emitted as a CLAS-style action receipt. - -> This is not production settlement or production signing. It is an educational example. - -## What this example does - -- Accepts a mock paid action request. -- Simulates an x402 `payment.accepted` event input. -- Executes a mock agent action (`summarize.text`). -- Emits a CLAS-style receipt containing: - - `metadata.trace` for correlation. - - `metadata.proof.payment` and `metadata.proof.execution`. - - `proof.signature` placeholders for `payer`, `agent`, `runtime`, and `verifier`. - -## Setup - -```bash -cp examples/x402-paid-action-receipt/.env.example .env -``` - -No secrets are required for this mock example. Do not add private keys to `.env`. - -## Environment variables - -- `PORT` (default `4000`): local server port. -- `WORKFLOW_ID` (optional): trace workflow correlation id. -- `RUNTIME_SIGNING_KEY_ID` (optional): key identifier string used in placeholder runtime signature metadata. - -## Run locally - -```bash -node examples/x402-paid-action-receipt/server.js -``` - -Health check: - -```bash -curl -s http://localhost:4000/health -``` - -## Sample curl command - -```bash -curl -s -X POST http://localhost:4000/paid-action \ - -H 'content-type: application/json' \ - -d '{ - "paid_action_request": { - "request_id": "req_9f2f5f25", - "action": "summarize.text", - "input": {"text": "CommandLayer receipts prove execution attestation separate from payment settlement."}, - "payment": {"required": true, "plan": "pro", "max_amount": "0.05", "currency": "USD"} - }, - "payment_accepted": { - "event": "payment.accepted", - "request_id": "req_9f2f5f25", - "payment_id": "pay_x402_7f31", - "provider": "x402-compatible", - "settled_amount": "0.05", - "currency": "USD", - "accepted_at": "2026-05-22T12:00:01Z" - } - }' -``` - -## Expected output - -A `200` JSON response containing: - -- `duplicate: false` on first execution. -- `receipt.receipt_id`, `request_id`, `payment_id`. -- `metadata.trace` fields including `request_id`, `payment_id`, `receipt_id`, `workflow_id`. -- `metadata.proof.commandlayer_signing_hook` placeholder to replace with real CommandLayer signing. -- `proof.signature` role entries for `payer`, `agent`, `runtime`, `verifier`. - -If the same `request_id + payment_id` is sent again, response includes `duplicate: true` and returns the original receipt. - -## Trust boundary - -- **x402/payment provider proves settlement**: payment requirement, acceptance/rejection, settlement status. -- **CommandLayer proves execution**: action request, runtime execution output, and signed receipt artifact. -- **Do not conflate them**: payment acceptance does not prove execution correctness. - -## Failure modes - -The API documents and returns error states for: - -- missing payment -- invalid payment -- duplicate `request_id` / `payment_id` (idempotent replay returns canonical receipt) -- action execution failed -- receipt signing failed -- verifier unavailable (documented operational dependency; this mock does not call an external verifier) - -## Idempotency model - -Use and persist three IDs: - -- `request_id`: semantic request identity. -- `payment_id`: payment-settlement identity. -- `receipt_id`: emitted CLAS receipt identity. - -Dedupe key in this example is `request_id + payment_id`. - -## Production-readiness path - -To move this example to production: - -1. Replace mock payment validation with real x402 provider verification. -2. Persist idempotency state in durable storage (DB/cache), not in-memory maps. -3. Implement canonical receipt signing using CommandLayer keys/HSM/KMS (replace placeholders). -4. Add verifier invocation and signed verifier attestations for `verifier` role. -5. Add schema validation for incoming request/event payloads. -6. Add audit logging, retries, and alerting for `RECEIPT_SIGNING_FAILED` and verifier outages. -7. Protect endpoints with authn/authz and rate limiting. - -## Validation commands - -```bash -npm test -node --test tests/x402-paid-action-receipt.test.js -``` diff --git a/examples/x402-paid-action-receipt/mockAgentAction.js b/examples/x402-paid-action-receipt/mockAgentAction.js deleted file mode 100644 index 946891f..0000000 --- a/examples/x402-paid-action-receipt/mockAgentAction.js +++ /dev/null @@ -1,23 +0,0 @@ -function summarizeText(input) { - if (!input || typeof input.text !== 'string' || input.text.trim().length === 0) { - throw new Error('ACTION_EXECUTION_FAILED: input.text is required for summarize.text.'); - } - - const normalized = input.text.replace(/\s+/g, ' ').trim(); - const sentence = normalized.split(/(?<=[.!?])\s+/)[0] || normalized; - return { - summary: sentence.length > 200 ? `${sentence.slice(0, 197)}...` : sentence - }; -} - -function executeMockAgentAction(action, input) { - if (action !== 'summarize.text') { - throw new Error(`ACTION_EXECUTION_FAILED: unsupported action ${action}.`); - } - - return summarizeText(input); -} - -module.exports = { - executeMockAgentAction -}; diff --git a/examples/x402-paid-action-receipt/mockX402Payment.js b/examples/x402-paid-action-receipt/mockX402Payment.js deleted file mode 100644 index aef2cf8..0000000 --- a/examples/x402-paid-action-receipt/mockX402Payment.js +++ /dev/null @@ -1,34 +0,0 @@ -const crypto = require('node:crypto'); - -function assertPaymentAccepted(paymentEvent, paidActionRequest) { - if (!paymentEvent) { - throw new Error('MISSING_PAYMENT: payment accepted event is required before execution.'); - } - - if (paymentEvent.event !== 'payment.accepted') { - throw new Error('INVALID_PAYMENT: expected event to be payment.accepted.'); - } - - if (paymentEvent.request_id !== paidActionRequest.request_id) { - throw new Error('INVALID_PAYMENT: request_id mismatch between action request and payment event.'); - } - - if (!paymentEvent.payment_id || !paymentEvent.provider) { - throw new Error('INVALID_PAYMENT: payment_id and provider are required.'); - } - - return { - settlement_status: 'accepted', - payment_id: paymentEvent.payment_id, - payment_ref: paymentEvent.payment_id, - provider: paymentEvent.provider, - settled_amount: paymentEvent.settled_amount, - currency: paymentEvent.currency, - accepted_at: paymentEvent.accepted_at, - verification_token: `x402v1:${crypto.createHash('sha256').update(JSON.stringify(paymentEvent)).digest('hex').slice(0, 24)}` - }; -} - -module.exports = { - assertPaymentAccepted -}; diff --git a/examples/x402-paid-action-receipt/receiptBuilder.js b/examples/x402-paid-action-receipt/receiptBuilder.js deleted file mode 100644 index 00a45a4..0000000 --- a/examples/x402-paid-action-receipt/receiptBuilder.js +++ /dev/null @@ -1,62 +0,0 @@ -const crypto = require('node:crypto'); - -function makeId(prefix) { - return `${prefix}_${crypto.randomUUID().replace(/-/g, '').slice(0, 20)}`; -} - -function buildClasReceipt({ paidActionRequest, paymentAcceptance, executionResult, signingKeyId, workflowId }) { - if (!paidActionRequest?.request_id || !paymentAcceptance?.payment_id) { - throw new Error('RECEIPT_SIGNING_FAILED: missing request_id or payment_id in receipt build input.'); - } - - const requestedAt = new Date().toISOString(); - const executedAt = new Date().toISOString(); - const receiptId = makeId('rcpt_clas_act'); - - const receipt = { - receipt_id: receiptId, - request_id: paidActionRequest.request_id, - payment_id: paymentAcceptance.payment_id, - action: paidActionRequest.action, - status: 'succeeded', - requested_at: requestedAt, - executed_at: executedAt, - result: executionResult, - metadata: { - trace: { - request_id: paidActionRequest.request_id, - payment_id: paymentAcceptance.payment_id, - receipt_id: receiptId, - workflow_id: workflowId || makeId('wf'), - provider: paymentAcceptance.provider - }, - proof: { - payment: { - scheme: 'x402', - settlement_status: paymentAcceptance.settlement_status, - payment_ref: paymentAcceptance.payment_ref - }, - execution: { - runtime_id: 'runtime_example_local', - agent_id: 'agent_mock_summarizer_v1', - policy_hash: 'sha256:example-policy-hash' - }, - commandlayer_signing_hook: 'Replace proof.signature placeholders with real CommandLayer signing in production.' - } - }, - proof: { - signature: [ - { role: 'payer', alg: 'Ed25519', key_id: 'payer_key_placeholder', sig: 'PLACEHOLDER_PAYER_SIG' }, - { role: 'agent', alg: 'Ed25519', key_id: 'agent_key_placeholder', sig: 'PLACEHOLDER_AGENT_SIG' }, - { role: 'runtime', alg: 'Ed25519', key_id: signingKeyId || 'runtime_key_placeholder', sig: 'PLACEHOLDER_RUNTIME_SIG' }, - { role: 'verifier', alg: 'Ed25519', key_id: 'verifier_key_placeholder', sig: 'PLACEHOLDER_VERIFIER_SIG' } - ] - } - }; - - return receipt; -} - -module.exports = { - buildClasReceipt -}; diff --git a/examples/x402-paid-action-receipt/sample-paid-action-request.json b/examples/x402-paid-action-receipt/sample-paid-action-request.json deleted file mode 100644 index 0cc1fb0..0000000 --- a/examples/x402-paid-action-receipt/sample-paid-action-request.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "request_id": "req_9f2f5f25", - "action": "summarize.text", - "input": { - "text": "CommandLayer receipts provide portable attestations of what an agent action executed and returned." - }, - "payment": { - "required": true, - "plan": "pro", - "max_amount": "0.05", - "currency": "USD" - } -} diff --git a/examples/x402-paid-action-receipt/sample-payment-accepted.json b/examples/x402-paid-action-receipt/sample-payment-accepted.json deleted file mode 100644 index c55046e..0000000 --- a/examples/x402-paid-action-receipt/sample-payment-accepted.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "event": "payment.accepted", - "request_id": "req_9f2f5f25", - "payment_id": "pay_x402_7f31", - "provider": "x402-compatible", - "settled_amount": "0.05", - "currency": "USD", - "accepted_at": "2026-05-22T12:00:01Z" -} diff --git a/examples/x402-paid-action-receipt/server.js b/examples/x402-paid-action-receipt/server.js deleted file mode 100644 index bfe5b67..0000000 --- a/examples/x402-paid-action-receipt/server.js +++ /dev/null @@ -1,92 +0,0 @@ -const http = require('node:http'); -const { assertPaymentAccepted } = require('./mockX402Payment'); -const { executeMockAgentAction } = require('./mockAgentAction'); -const { buildClasReceipt } = require('./receiptBuilder'); - -const dedupe = new Map(); - -function dedupeKey(requestId, paymentId) { - return `${requestId}:${paymentId}`; -} - -function sendJson(res, statusCode, body) { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(body, null, 2)); -} - -function createServer() { - return http.createServer((req, res) => { - if (req.method === 'GET' && req.url === '/health') { - return sendJson(res, 200, { ok: true }); - } - - if (req.method === 'POST' && req.url === '/paid-action') { - let raw = ''; - req.on('data', (chunk) => { - raw += chunk; - }); - - req.on('end', () => { - let body; - try { - body = raw ? JSON.parse(raw) : {}; - } catch { - return sendJson(res, 400, { error: 'INVALID_JSON', message: 'Request body must be valid JSON.' }); - } - - const { paid_action_request: paidActionRequest, payment_accepted: paymentAccepted } = body || {}; - if (!paidActionRequest) { - return sendJson(res, 400, { error: 'MISSING_REQUEST', message: 'paid_action_request is required.' }); - } - - let paymentAcceptance; - try { - paymentAcceptance = assertPaymentAccepted(paymentAccepted, paidActionRequest); - } catch (error) { - return sendJson(res, 402, { error: 'PAYMENT_REQUIRED_OR_INVALID', message: error.message }); - } - - const key = dedupeKey(paidActionRequest.request_id, paymentAcceptance.payment_id); - if (dedupe.has(key)) { - return sendJson(res, 200, { duplicate: true, receipt: dedupe.get(key) }); - } - - let actionResult; - try { - actionResult = executeMockAgentAction(paidActionRequest.action, paidActionRequest.input); - } catch (error) { - return sendJson(res, 500, { error: 'ACTION_EXECUTION_FAILED', message: error.message }); - } - - let receipt; - try { - receipt = buildClasReceipt({ - paidActionRequest, - paymentAcceptance, - executionResult: actionResult, - signingKeyId: process.env.RUNTIME_SIGNING_KEY_ID, - workflowId: process.env.WORKFLOW_ID - }); - } catch (error) { - return sendJson(res, 500, { error: 'RECEIPT_SIGNING_FAILED', message: error.message }); - } - - dedupe.set(key, receipt); - return sendJson(res, 200, { duplicate: false, receipt }); - }); - return; - } - - sendJson(res, 404, { error: 'NOT_FOUND' }); - }); -} - -if (require.main === module) { - const port = Number(process.env.PORT || 4000); - createServer().listen(port, () => { - // eslint-disable-next-line no-console - console.log(`x402 paid action receipt example listening on http://localhost:${port}`); - }); -} - -module.exports = { createServer, dedupeKey }; diff --git a/lib/stripe-client.js b/lib/stripe-client.js deleted file mode 100644 index 163e1a2..0000000 --- a/lib/stripe-client.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const Stripe = require('stripe'); - -function createStripeError(code, message) { - const error = new Error(message); - error.code = code; - return error; -} - -module.exports = function createStripeClient(secretKey, options = {}) { - const key = typeof secretKey === 'string' ? secretKey.trim() : ''; - if (!key) { - throw createStripeError('STRIPE_NOT_CONFIGURED', 'Stripe secret key is not configured.'); - } - if (key.startsWith('pk_')) { - throw createStripeError('STRIPE_SECRET_KEY_INVALID', 'Stripe secret key must be a server secret key (sk_*), not a publishable key.'); - } - return new Stripe(key, options); -}; diff --git a/lib/x402ProviderVerification.js b/lib/x402ProviderVerification.js deleted file mode 100644 index 3b50dbe..0000000 --- a/lib/x402ProviderVerification.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const PAYMENT_VERIFICATION_MODES = { - DEMO_ACCEPTED_ENVELOPE: 'demo_accepted_envelope', - PROVIDER_VERIFIED: 'provider_verified', -}; - -function getProviderVerificationUrl() { - const value = process.env.X402_PROVIDER_VERIFICATION_URL; - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -function resolveVerificationMode() { - return getProviderVerificationUrl() - ? PAYMENT_VERIFICATION_MODES.PROVIDER_VERIFIED - : PAYMENT_VERIFICATION_MODES.DEMO_ACCEPTED_ENVELOPE; -} - -async function verifyWithProvider({ payload, req }) { - const url = getProviderVerificationUrl(); - if (!url) { - return { - ok: true, - paymentVerificationMode: PAYMENT_VERIFICATION_MODES.DEMO_ACCEPTED_ENVELOPE, - provider: null, - }; - } - - const headers = { - 'Content-Type': 'application/json; charset=utf-8', - }; - if (process.env.X402_PROVIDER_API_KEY) { - headers.Authorization = `Bearer ${process.env.X402_PROVIDER_API_KEY}`; - } - - const providerPayload = { - payment: payload.payment, - request: { - request_id: payload.request_id, - action: payload.action, - input: payload.input, - }, - metadata: { - method: req.method, - path: req.url || req.path || '/api/examples/x402-paid-action', - headers: { - 'x-request-id': req.headers?.['x-request-id'] || req.headers?.['X-Request-Id'] || null, - }, - }, - }; - - let response; - try { - response = await fetch(url, { - method: 'POST', - headers, - body: JSON.stringify(providerPayload), - }); - } catch { - return { ok: false, httpStatus: 503, status: 'payment_provider_unavailable' }; - } - - let data; - try { - data = await response.json(); - } catch { - return { ok: false, httpStatus: 503, status: 'payment_provider_unavailable' }; - } - - if (!data || typeof data !== 'object') { - return { ok: false, httpStatus: 503, status: 'payment_provider_unavailable' }; - } - - const accepted = data.accepted === true || data.settled === true || data.status === 'accepted' || data.status === 'settled'; - if (!response.ok || !accepted) { - const paymentStatus = data.status; - if (paymentStatus === 'required') return { ok: false, httpStatus: 402, status: 'payment_required' }; - if (paymentStatus === 'invalid' || response.status === 400 || response.status === 402) { - return { ok: false, httpStatus: response.status === 402 ? 402 : 400, status: response.status === 402 ? 'payment_required' : 'payment_invalid' }; - } - return { ok: false, httpStatus: 503, status: 'payment_provider_unavailable' }; - } - - return { - ok: true, - paymentVerificationMode: PAYMENT_VERIFICATION_MODES.PROVIDER_VERIFIED, - provider: { - status: typeof data.status === 'string' ? data.status : 'accepted', - reference: typeof data.reference === 'string' ? data.reference : null, - provider: typeof data.provider === 'string' ? data.provider : null, - }, - }; -} - -module.exports = { - PAYMENT_VERIFICATION_MODES, - resolveVerificationMode, - verifyWithProvider, -}; diff --git a/package.json b/package.json index 4e72752..c900378 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "ajv-formats": "^3.0.1", "ethers": "^6.16.0", "siwe": "^3.0.0", - "@neondatabase/serverless": "^1.0.0", - "stripe": "^18.3.0" + "@neondatabase/serverless": "^1.0.0" } } diff --git a/public/integrations.html b/public/integrations.html index dfea89c..2ff90f3 100644 --- a/public/integrations.html +++ b/public/integrations.html @@ -53,7 +53,6 @@

x402 Paid Action

Live signed endpoint

A paid-action request with an accepted x402 payment envelope can execute a deterministic action, emit a signed CLAS receipt, and verify through /api/verify.

diff --git a/public/proof-flow-composer.html b/public/proof-flow-composer.html index 368e05f..793cc40 100644 --- a/public/proof-flow-composer.html +++ b/public/proof-flow-composer.html @@ -102,7 +102,7 @@

Verify cURL