Skip to content
Closed
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
1 change: 1 addition & 0 deletions examples/x402/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PRIVATE_KEY="0x1111111111111111111111111111111111111111111111111111111111111111"
33 changes: 33 additions & 0 deletions examples/x402/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# prod
dist/

# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf

# deps
node_modules/
.wrangler

# env
.env
.env.production
.dev.vars

# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# misc
.DS_Store
104 changes: 104 additions & 0 deletions examples/x402/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# ACK ID + x402 example

This example shows how to use ACK ID to verify a server's identity, and then use x402 to require payment to access protected routes.

The example consists of two parts:

1. A server that implements the ACK ID protocol to serve a Verifiable Credential that allows buyers to verify the server's identity, and the x402 payment middleware to require payment to access protected routes.
2. A client that purchases a product from the server by first verifying the server's identity using ACK ID, and then sending a x402 payment using the x402-fetch library.

## Server

The server is a simple Hono server that implements the ACK ID protocol to serve a Verifiable Credential that allows buyers to verify the server's identity, and the x402 payment middleware to require payment to access protected routes.

The server hosts the following API endpoints:

- `GET /identify` - Returns the server's Verifiable Credential
- `GET /buy` - A protected endpoint that requires payment to access

### Configure the server

The server requires 2 private keys to run:

- `SERVER_PRIVATE_KEY` - The private key for the server
- `CONTROLLER_PRIVATE_KEY` - The private key for the controller

You can generate these keys by running `pnpm run setup`.

### Run the server

To run the server:

```sh
pnpm install
pnpm run dev
```

The server will be available at `http://localhost:5000`.

## Client

The client is a simple script that purchases a product from the server by first verifying the server's identity using ACK ID, and then sending a x402 payment using the x402-fetch library.

### Configure the client

The client sends a small $0.03 payment to the server, on the Sepolia testnet. To do this it requires a wallet's private key, from which it will derive the account to send the payment from.

You can choose not to provide a private key, in which case the client will use a random account to send the payment from. This is useful for testing, but you will not be able to verify the payment was made.

- `PRIVATE_KEY` - The wallet private key for the buyer

This should be placed in the .env file. If you don't fill this in, the example will still run but the actual payment at the end will fail. However, the client will still verify the server's identity and go through the x402 payment process.

### Run the example

To run the example and buy the product from the server:

```sh
pnpm install
pnpm run buy
```

The client will make a request to the server's `/identity` endpoint to verify the server's identity using ACK ID, and then make a request to the server's `/buy` endpoint, which requires payment to access. The server will verify the client's identity using ACK ID, and then send a x402 payment using the x402-fetch library.

Running the example will give you output like this:

```sh
pnpm run buy

> x402@ buy /Users/ed/Code/catena/ack/examples/x402
> tsx bin/buy.ts

[dotenv@17.2.2] injecting env (1) from .env -- tip: 🛠️ run anywhere with `dotenvx run -- yourcommand`
Client account 0xE302B76b44dF928D37E67D041c461D685AEb56a0 - this will be used to sign the payment
Verifying server identity using ACK ID...
Received Verifiable Credential:
{
"credentialSubject": {
"controller": "did:web:localhost%3A5000:trusted",
"id": "did:web:localhost%3A5000"
},
"issuer": {
"id": "did:web:localhost%3A5000"
},
"type": [
"VerifiableCredential",
"ControllerCredential"
],
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"issuanceDate": "2025-09-18T22:48:06.000Z",
"proof": {
"type": "JwtProof2020",
"jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiQ29udHJvbGxlckNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiY29udHJvbGxlciI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBNTAwMDp0cnVzdGVkIn19LCJzdWIiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTUwMDAiLCJuYmYiOjE3NTgyMzU2ODYsImlzcyI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBNTAwMCJ9.i2DbDq7RY4W9jVw6ADoySN8-PjVK-0bIU1z3oRLQabI8zmK4yOPZVsWScLYnYu8MAatOQG3y87t2jNLab2xLDA"
}
}
✅ VC verified successfully
✅ Seller identity verified, proceeding with purchase...
Making request to http://localhost:5000/buy
✅ Purchase complete, received response:
{
"message": "Here is the content you paid for. Enjoy!"
}
```
108 changes: 108 additions & 0 deletions examples/x402/bin/buy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* This script purchases a product from the server by first verifying the server's
* identity using ACK ID, and then sending a x402 payment using the x402-fetch
* library.
*/
import dotenv from "dotenv"
import { createWalletClient, http } from "viem"
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
import { wrapFetchWithPayment } from "x402-fetch"
import { baseSepolia } from "viem/chains"
import {
getControllerClaimVerifier,
getDidResolver,
verifyParsedCredential,
W3CCredential
} from "agentcommercekit"

dotenv.config()

// this is the private key for the buyer's wallet
const privateKey = process.env.PRIVATE_KEY || generatePrivateKey()

// this is the API host for the seller
const apiHost = "http://localhost:5000"
const buyUri = `${apiHost}/buy`
const identifyUri = `${apiHost}/identify`

const account = privateKeyToAccount(privateKey as `0x${string}`)

console.log(
"Client account",
account.address,
"- this will be used to sign the payment"
)

// Create a wallet client - this is used to sign the payment.
// x402 uses EIP-712 to sign the payment,
const client = createWalletClient({
account,
transport: http(),
chain: baseSepolia
})

// Wrap fetch with payment middleware - this automatically handles
// the payment process using the client that we configured above
const fetchWithPay = wrapFetchWithPayment(fetch, client, 1000000)

// this is the main function run when the script is executed
async function main() {
await verifySeller()

console.log("Making request to", buyUri)
const response = await fetchWithPay(buyUri, {
method: "GET"
})
const data = await response.json()

console.log("✅ Purchase complete, received response:")
console.log(JSON.stringify(data, null, 2))
}

/**
* Verifies the server's identity and the Verifiable Credential by fetching from
* the seller's /identify endpoint, which returns a Verifiable Credential.
*/
async function verifySeller() {
try {
console.log("Verifying server identity using ACK ID...")
const response = await fetch(identifyUri)
const vc = await response.json()

console.log("Received Verifiable Credential:")
console.log(JSON.stringify(vc, null, 2))

await verifyCredential(vc)

console.log("✅ Seller identity verified, proceeding with purchase...")

return true
} catch (error) {
console.error("❌ Failed to verify seller:", error)
throw error
}
}

/**
* Verifies a Verifiable Credential using the controller claim verifier.
*
* @param vc - The Verifiable Credential to verify
*/
async function verifyCredential(vc: W3CCredential) {
const verifier = getControllerClaimVerifier()
const resolver = getDidResolver()

try {
await verifyParsedCredential(vc, {
resolver,
verifiers: [verifier]
})
console.log("✅ VC verified successfully")
return { did: vc.issuer.id }
} catch (error) {
console.error("❌ VC verification failed:", error)
throw error
}
}

main().catch(console.error)
32 changes: 32 additions & 0 deletions examples/x402/bin/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* This script generates private keys for the server and controller and saves
* them to a .dev.vars file for use with wrangler.
*/
import { writeFileSync } from "fs"
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
import { resolve } from "path"
import { fileURLToPath } from "url"

function main() {
console.log("Generating private keys and receiver address...")

const serverKey = generatePrivateKey()
const controllerKey = generatePrivateKey()
const receiverAddress = privateKeyToAccount(generatePrivateKey()).address

const content = `SERVER_PRIVATE_KEY="${serverKey}"\nCONTROLLER_PRIVATE_KEY="${controllerKey}"\nRECEIVER_ADDRESS="${receiverAddress}"\n`

// Resolve path to the root of the x402 example directory
const __dirname = fileURLToPath(new URL(".", import.meta.url))
const filePath = resolve(__dirname, "../.dev.vars")

try {
writeFileSync(filePath, content, "utf-8")
console.log(`✅ Successfully created .dev.vars file at ${filePath}`)
console.log("You can now run 'pnpm dev' to start the server.")
} catch (error) {
console.error("❌ Failed to write .dev.vars file:", error)
}
}

main()
25 changes: 25 additions & 0 deletions examples/x402/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "x402",
"type": "module",
"scripts": {
"buy": "tsx bin/buy.ts",
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
"deploy": "wrangler deploy --minify",
"dev": "wrangler dev",
"setup": "tsx bin/setup.ts"
},
"dependencies": {
"@coinbase/x402": "^0.6.4",
"@hono/node-server": "^1.14.2",
"agentcommercekit": "workspace:*",
"dotenv": "^17.2.2",
"hono": "^4.9.7",
"viem": "^2.37.6",
"vitest": "^3.2.4",
"x402-fetch": "^0.6.0",
"x402-hono": "^0.6.1"
},
"devDependencies": {
"wrangler": "^4.4.0"
}
}
79 changes: 79 additions & 0 deletions examples/x402/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* This is a simple server that implements the ACK ID protocol to serve
* a Verifiable Credential that allows buyers to verify the server's identity,
* and the x402 payment middleware to require payment to access protected routes.
*/
import { Hono } from "hono"
import { paymentMiddleware } from "x402-hono"
import { getServerIdentity, getControllerIdentity } from "./server-identity"
import {
createControllerCredential,
getDidResolver,
parseJwtCredential,
signCredential
} from "agentcommercekit"
import { Address } from "viem"

const app = new Hono()

// The address that will receive the payment
const receiverAddress = process.env.RECEIVER_ADDRESS!

// Configure the payment middleware
app.use(
paymentMiddleware(
receiverAddress as Address,
{
// Route configurations for protected endpoints
"/buy": {
price: "$0.03",
network: "base-sepolia",
config: {
description: "Access to premium content"
}
}
},
{
url: "https://x402.org/facilitator" // for testnet
}
)
)

// Returns the server's DID and Verifiable Credential
app.get("/identify", async (c) => {
// the buyer trusts the controller DID
const { did: controllerDid } = await getControllerIdentity()

const signer = await getServerIdentity()
const { did } = signer

const unsigned = await createControllerCredential({
issuer: did,
subject: did,
controller: controllerDid
})

const vcJwt = await signCredential(unsigned, signer)

const vc = await parseJwtCredential(vcJwt, getDidResolver())

return c.json(vc)
})

// Serve the controller DID document for did:web:localhost%3A5000:trusted
app.get("/trusted/.well-known/did.json", async (c) => {
const { didDocument } = await getControllerIdentity()
return c.json(didDocument)
})

// Serves the server's DID document
app.get("/.well-known/did.json", async (c) => {
const { didDocument } = await getServerIdentity()
return c.json(didDocument)
})

app.get("/buy", (c) => {
return c.json({ message: "Here is the content you paid for. Enjoy!" })
})

export default app
Loading
Loading