Skip to content
Open
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
4 changes: 4 additions & 0 deletions examples/typescript/clients/da-anchoring/.env-local
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions examples/typescript/clients/da-anchoring/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"proseWrap": "never"
}
90 changes: 90 additions & 0 deletions examples/typescript/clients/da-anchoring/README.md
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions examples/typescript/clients/da-anchoring/eslint.config.js
Original file line number Diff line number Diff line change
@@ -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"],
},
},
];
144 changes: 144 additions & 0 deletions examples/typescript/clients/da-anchoring/index.ts
Original file line number Diff line number Diff line change
@@ -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<typeof DecisionAnchor>,
apiUrl: string,
amount: string,
): Promise<DdCreateResult> {
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<typeof DecisionAnchor>,
ddId: string,
): Promise<DdConfirmResult> {
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<void> {
// 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);
});
32 changes: 32 additions & 0 deletions examples/typescript/clients/da-anchoring/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
15 changes: 15 additions & 0 deletions examples/typescript/clients/da-anchoring/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
Loading