diff --git a/examples/typescript/clients/da-anchoring/.env-local b/examples/typescript/clients/da-anchoring/.env-local new file mode 100644 index 0000000000..9f70a132b1 --- /dev/null +++ b/examples/typescript/clients/da-anchoring/.env-local @@ -0,0 +1,4 @@ +EVM_PRIVATE_KEY=0x_your_evm_private_key +DA_AUTH_TOKEN=da_tk_your_auth_token +TARGET_API_URL=http://localhost:4021/weather +DA_BASE_URL=https://api.decision-anchor.com diff --git a/examples/typescript/clients/da-anchoring/.prettierrc b/examples/typescript/clients/da-anchoring/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/examples/typescript/clients/da-anchoring/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/examples/typescript/clients/da-anchoring/README.md b/examples/typescript/clients/da-anchoring/README.md new file mode 100644 index 0000000000..59b81aaea6 --- /dev/null +++ b/examples/typescript/clients/da-anchoring/README.md @@ -0,0 +1,90 @@ +# x402 + Decision Anchor Payment Anchoring + +Example client demonstrating how to anchor x402 payment decisions externally using [Decision Anchor](https://api.decision-anchor.com). Creates append-only proof of payment authorization scope before x402 execution. + +```typescript +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm/exact/client"; +import DecisionAnchor from "decision-anchor-sdk"; + +// Anchor the decision BEFORE payment +const dd = await da.dd.create({ ... }); + +// Execute x402 payment +const response = await fetchWithPayment(url); + +// Confirm the anchor AFTER payment +await da.dd.confirm(dd.dd_id); +``` + +## What This Demonstrates + +On-chain records show who paid whom how much — but not **why** an agent authorized a payment, or what decision scope was in effect. Internal logs are self-testimony: the agent claims it decided the spend was necessary, but that claim is unfalsifiable after the fact. + +This example wraps x402 payments with DA Decision Declarations: + +1. **Before payment** — creates a DD that records the authorization scope externally +2. **x402 payment** — executes USDC payment via `@x402/fetch` +3. **After payment** — confirms the DD, anchoring the completion timestamp + +The DD is external (not in the agent's own logs), append-only (cannot be retroactively modified), and created at the moment of decision (not reconstructed later). + +## Prerequisites + +- Node.js v20+ (install via [nvm](https://github.com/nvm-sh/nvm)) +- pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) +- A running x402 server (see [express server example](../../servers/express)) +- A DA agent auth token (register at https://api.decision-anchor.com — see [SDK docs](https://github.com/zse4321/decision-anchor-sdk)) +- Valid EVM private key with USDC on Base + +## Setup + +1. Install and build all packages from the typescript examples root: + +```bash +cd ../../ +pnpm install && pnpm build +cd clients/da-anchoring +``` + +2. Create a `.env` file with your credentials: + +```bash +EVM_PRIVATE_KEY=0x... # Ethereum private key for x402 payments +DA_AUTH_TOKEN=da_tk_... # Decision Anchor auth token +TARGET_API_URL=http://localhost:4021/weather # x402-protected endpoint +DA_BASE_URL=https://api.decision-anchor.com # Optional, defaults to production +``` + +3. Run the example: + +```bash +pnpm start +``` + +## Expected Output + +``` +=== x402 Payment with DA Anchoring === + +1. Creating Decision Declaration (pre-payment anchor)... + DD ID: dd_abc123... + Anchored at: 2026-04-07T12:00:00.000Z + Integrity hash: sha256:... + +2. Executing x402 payment... + Status: 200 + Response: { ... } + +3. Confirming Decision Declaration (post-payment anchor)... + Status: confirmed + Confirmed at: 2026-04-07T12:00:01.000Z + +=== Done === +``` + +## Related + +- [fetch client example](../fetch/) — basic x402 payment without anchoring +- [Decision Anchor SDK](https://github.com/zse4321/decision-anchor-sdk) — DA client library +- [x402.org](https://x402.org) — x402 protocol specification diff --git a/examples/typescript/clients/da-anchoring/eslint.config.js b/examples/typescript/clients/da-anchoring/eslint.config.js new file mode 100644 index 0000000000..dc7a98733b --- /dev/null +++ b/examples/typescript/clients/da-anchoring/eslint.config.js @@ -0,0 +1,74 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + console: "readonly", + fetch: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/examples/typescript/clients/da-anchoring/index.ts b/examples/typescript/clients/da-anchoring/index.ts new file mode 100644 index 0000000000..62abc78ac8 --- /dev/null +++ b/examples/typescript/clients/da-anchoring/index.ts @@ -0,0 +1,144 @@ +// On-chain records show who paid whom how much — but not why. +// Internal logs are self-testimony. +// A DA Decision Declaration anchored before payment provides +// external, append-only proof of authorization scope. + +import { config } from "dotenv"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { privateKeyToAccount } from "viem/accounts"; +import DecisionAnchor from "decision-anchor-sdk"; + +config(); + +const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}`; +const daAuthToken = process.env.DA_AUTH_TOKEN as string; +const targetApiUrl = process.env.TARGET_API_URL || "http://localhost:4021/weather"; + +interface DdCreateResult { + dd_id: string; + dac_amount: number; + anchored_at: string; + integrity_hash: string; +} + +interface DdConfirmResult { + status: string; + confirmed_at: string; +} + +/** + * Create a Decision Declaration on DA before payment execution. + * + * @param da - authenticated Decision Anchor SDK client + * @param apiUrl - the x402-protected endpoint being called + * @param amount - payment amount from the 402 response + * @returns - the created DD with id, timestamp, and integrity hash + */ +async function anchorBeforePayment( + da: InstanceType, + apiUrl: string, + amount: string, +): Promise { + const dd = await da.dd.create({ + requestId: crypto.randomUUID(), + dd: { + dd_unit_type: "single", + dd_declaration_mode: "self_declared", + decision_type: "external_interaction", + decision_action_type: "execute", + origin_context_type: "external", + selection_state: "SELECTED", + }, + ee: { + ee_retention_period: "medium", + ee_integrity_verification_level: "standard", + ee_disclosure_format_policy: "summary", + ee_responsibility_scope: "standard", + ee_direct_access_period: "30d", + ee_direct_access_quota: 5, + }, + context: { + summary: `x402 payment authorization: ${amount} USDC for ${apiUrl}`, + api_url: apiUrl, + payment_amount: amount, + payment_chain: "base", + }, + }); + + return dd as DdCreateResult; +} + +/** + * Confirm a Decision Declaration on DA after successful payment. + * + * @param da - authenticated Decision Anchor SDK client + * @param ddId - the DD ID returned from anchorBeforePayment + * @returns - the confirmation result with status and timestamp + */ +async function anchorAfterPayment( + da: InstanceType, + ddId: string, +): Promise { + const confirmed = await da.dd.confirm(ddId); + return confirmed as DdConfirmResult; +} + +/** + * Main function demonstrating x402 payment with DA decision anchoring. + * + * Flow: + * 1. Set up x402 fetch client with EVM payment scheme + * 2. Create a DA Decision Declaration (pre-payment anchor) + * 3. Execute x402 payment via wrapped fetch + * 4. Confirm the DD (post-payment anchor) + * + * @returns - resolves when the example completes + */ +async function main(): Promise { + // Initialize x402 client + const evmSigner = privateKeyToAccount(evmPrivateKey); + const x402 = new x402Client(); + x402.register("eip155:*", new ExactEvmScheme(evmSigner)); + const fetchWithPayment = wrapFetchWithPayment(fetch, x402); + + // Initialize DA client with existing auth token + const da = new DecisionAnchor({ + baseUrl: process.env.DA_BASE_URL || "https://api.decision-anchor.com", + authToken: daAuthToken, + }); + + console.log("=== x402 Payment with DA Anchoring ===\n"); + + // Step 1: Anchor the payment decision BEFORE execution + console.log("1. Creating Decision Declaration (pre-payment anchor)..."); + const dd = await anchorBeforePayment(da, targetApiUrl, "0.001"); + console.log(` DD ID: ${dd.dd_id}`); + console.log(` Anchored at: ${dd.anchored_at}`); + console.log(` Integrity hash: ${dd.integrity_hash}`); + + // Step 2: Execute x402 payment + console.log("\n2. Executing x402 payment..."); + const response = await fetchWithPayment(targetApiUrl, { method: "GET" }); + const contentType = response.headers.get("content-type") ?? ""; + const body = contentType.includes("application/json") + ? await response.json() + : await response.text(); + console.log(` Status: ${response.status}`); + console.log(" Response:", body); + + // Step 3: Confirm the DD AFTER successful payment + console.log("\n3. Confirming Decision Declaration (post-payment anchor)..."); + const confirmed = await anchorAfterPayment(da, dd.dd_id); + console.log(` Status: ${confirmed.status}`); + console.log(` Confirmed at: ${confirmed.confirmed_at}`); + + console.log("\n=== Done ==="); + console.log("The DD provides external proof that this payment was authorized"); + console.log("at the recorded scope, independent of on-chain records and internal logs."); +} + +main().catch(error => { + console.error(error?.response?.data?.error ?? error); + process.exit(1); +}); diff --git a/examples/typescript/clients/da-anchoring/package.json b/examples/typescript/clients/da-anchoring/package.json new file mode 100644 index 0000000000..9f5f445dfd --- /dev/null +++ b/examples/typescript/clients/da-anchoring/package.json @@ -0,0 +1,32 @@ +{ + "name": "@x402/da-anchoring-example", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "@x402/evm": "workspace:*", + "@x402/fetch": "workspace:*", + "decision-anchor-sdk": "^0.1.0", + "dotenv": "^16.4.7", + "viem": "^2.39.0" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/examples/typescript/clients/da-anchoring/tsconfig.json b/examples/typescript/clients/da-anchoring/tsconfig.json new file mode 100644 index 0000000000..78f9479b1b --- /dev/null +++ b/examples/typescript/clients/da-anchoring/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": ["index.ts"] +}