Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/x402-paid-action-receipt/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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
123 changes: 123 additions & 0 deletions examples/x402-paid-action-receipt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# 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
```
23 changes: 23 additions & 0 deletions examples/x402-paid-action-receipt/mockAgentAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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
};
34 changes: 34 additions & 0 deletions examples/x402-paid-action-receipt/mockX402Payment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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
};
62 changes: 62 additions & 0 deletions examples/x402-paid-action-receipt/receiptBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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
};
13 changes: 13 additions & 0 deletions examples/x402-paid-action-receipt/sample-paid-action-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"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"
}
92 changes: 92 additions & 0 deletions examples/x402-paid-action-receipt/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 };
Loading
Loading