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
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Authored by:** Chris Wajule and Simon Goldberg

This solution demonstrates pay-per-use AI content generation using the x402 payment protocol. Users pay with USDC on Base Sepolia to access Amazon Nova 2 Lite for text and Amazon Nova Canvas for image generation. Two payment architectures are included: the serverless flow where users sign payments via browser wallets, and the agentic flow using the Strands agent powered by Amazon Bedrock AgentCore with CDP AgentKit, both utilizing the x402.org facilitator to verify signatures and settle payments on-chain via EIP-3009 transferWithAuthorization.
This solution demonstrates pay-per-use AI content generation using the x402 payment protocol (v2). Users pay with USDC on Base Sepolia to access Amazon Nova 2 Lite for text and Amazon Nova Canvas for image generation. Two payment architectures are included: the serverless flow where users sign payments via browser wallets, and the agentic flow using the Strands agent powered by Amazon Bedrock AgentCore with CDP AgentKit, both utilizing the x402.org facilitator to verify signatures and settle payments on-chain via EIP-3009 transferWithAuthorization.

## Table of Contents

Expand Down Expand Up @@ -41,7 +41,7 @@ The numbers in the following flow correspond to the serverless stablecoin paymen

5. **Payment Authorization:** The frontend is hosted on AWS Amplify and displays a payment modal with the cost preview. The user confirms and the application generates an `EIP-712` typed data signature using the connected wallet. The wallet prompts the user to sign the message. The signature authorizes the USDC transfer with validity timestamps and a unique nonce.

6. **Payment Submission:** The frontend retries the `/generate` request with the `EIP-712` signature in the `PAYMENT-SIGNATURE` header. The request includes the authorization object (from, to, value, validAfter, validBefore, nonce) and signature.
6. **Payment Submission:** The frontend retries the `/generate` request with the `EIP-712` signature in the `PAYMENT-SIGNATURE` header. The Base64-encoded payload uses the x402 v2 shape `{ x402Version: 2, payload: { signature, authorization }, accepted }`, where the authorization object holds (from, to, value, validAfter, validBefore, nonce) and `accepted` echoes the chosen payment requirements.

7. **Payment Verification:** The AWS Lambda function sends the payment payload to the x402.org facilitator's `/verify` endpoint. The facilitator validates the `EIP-712` signature against the USDC contract domain on Base Sepolia.

Expand Down Expand Up @@ -373,19 +373,33 @@ Agent: 🎉 Success! Your futuristic city at sunset image has been generated suc
}
```

**Response (402 Payment Required):**
**Response (402 Payment Required):** (x402 v2 wire format)

```json
{
"scheme": "exact",
"network": "base-sepolia",
"maxAmountRequired": "192",
"payTo": "0x...",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"description": "AI content generation with nova-llm"
"x402Version": 2,
"accepts": [
{
"scheme": "exact",
"network": "eip155:84532",
"amount": "192",
"payTo": "0x...",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"maxTimeoutSeconds": 300,
"extra": { "name": "USDC", "version": "2" }
}
],
"resource": {
"url": "https://.../generate",
"description": "AI content generation with nova-llm",
"mimeType": "application/json"
},
"error": "Payment required"
}
```

> x402 v2 changes from v1: `x402Version` is `2`, `network` uses the CAIP-2 form (`eip155:84532` for Base Sepolia), the amount field is `amount` (was `maxAmountRequired`), and `resource`/`description`/`mimeType` are hoisted to a top-level `resource` object. The same requirements are also returned Base64-encoded in the `PAYMENT-REQUIRED` response header.

**Response (200 Success):**

```json
Expand Down Expand Up @@ -544,7 +558,7 @@ rm -rf node_modules/ dist/ serverless/node_modules/ serverless/cdk.out/ serverle
- Verify transaction hash on [BaseScan Sepolia](https://sepolia.basescan.org)

10. **Gateway Returns 402 After Payment:**
- Check that the x402 client is using the correct network (base-sepolia)
- Check that the x402 client is using the correct network (CAIP-2 `eip155:84532` for Base Sepolia in x402 v2)
- Verify the USDC contract address matches
- Ensure the payment amount matches the required amount

Expand Down
2 changes: 2 additions & 0 deletions agentic/.env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ CDP_API_KEY_SECRET=your_api_key_secret
CDP_WALLET_SECRET=your_wallet_secret

# Network Configuration
# NETWORK_ID uses the human-readable name; the x402 v2 client maps it to the
# CAIP-2 form on the wire (base-sepolia -> eip155:84532, base -> eip155:8453).
NETWORK_ID=base-sepolia
RPC_URL=https://sepolia.base.org
USDC_CONTRACT=0x036CbD53842c5426634e7929541eC2318f3dCF7e
Expand Down
20 changes: 14 additions & 6 deletions agentic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,22 @@ Agent: 🎉 Success! Your futuristic city at sunset image has been generated suc

```json
{
"x402Version": 1,
"x402Version": 2,
"accepts": [{
"scheme": "exact",
"network": "base-sepolia",
"maxAmountRequired": "40000",
"network": "eip155:84532",
"amount": "40000",
"payTo": "0x...",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
}]
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"maxTimeoutSeconds": 300,
"extra": { "name": "USDC", "version": "2" }
}],
"resource": {
"url": "https://.../generate_image",
"description": "AI image generation with Nova Canvas",
"mimeType": "application/json"
},
"error": "Payment required"
}
```

Expand Down Expand Up @@ -241,7 +249,7 @@ cdk diff
- Review CloudWatch logs for specific error messages

5. **Gateway Returns 402 After Payment:**
- Check that the x402 client is using the correct network (base-sepolia)
- Check that the x402 client is using the correct network (CAIP-2 `eip155:84532` for Base Sepolia in x402 v2)
- Verify the USDC contract address matches
- Ensure the payment amount matches the required amount

Expand Down
126 changes: 61 additions & 65 deletions agentic/lambda/seller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,44 @@ import https from 'https';

const app = new Hono();

// x402 Configuration
// x402 v2 Configuration
// Use the canonical facilitator host (www.x402.org). The apex x402.org 308-redirects
// every request to www, so pointing here avoids an extra round-trip per verify/settle.
const X402_CONFIG = {
facilitatorUrl: 'https://x402.org/facilitator',
facilitatorUrl: 'https://www.x402.org/facilitator',
usdcBase: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
network: 'base-sepolia',
network: 'eip155:84532', // Base Sepolia in CAIP-2 form (required by x402 v2)
scheme: 'exact'
};

const X402_VERSION = 2;

// Build the v2 PaymentRequirements the seller expects for a given authorized amount.
// `accepted` (sent inside the payment payload to the facilitator) is the same object
// without the EIP-712 `extra` metadata.
const buildPaymentRequirements = (amount, resourceUrl) => ({
scheme: X402_CONFIG.scheme,
network: X402_CONFIG.network,
amount: String(amount),
asset: X402_CONFIG.usdcBase,
payTo: process.env.SELLER_WALLET,
maxTimeoutSeconds: 300,
extra: { name: 'USDC', version: '2' }
});

const toAccepted = (paymentRequirements) => {
const { extra, ...accepted } = paymentRequirements;
return accepted;
};

// Idempotency cache - for production, persist to DynamoDB for multi-instance scalability
const processedPayments = new Map();

// CORS middleware
app.use('*', async (c, next) => {
c.header('Access-Control-Allow-Origin', '*');
c.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-PAYMENT, PAYMENT-SIGNATURE');
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, PAYMENT-SIGNATURE');
c.header('Access-Control-Expose-Headers', 'PAYMENT-REQUIRED, PAYMENT-RESPONSE');

if (c.req.method === 'OPTIONS') {
Expand Down Expand Up @@ -56,19 +78,18 @@ const generateCDPJWT = async (requestMethod, requestPath) => {
};
*/

// Verify payment with x402.org facilitator
// Verify payment with x402.org facilitator (x402 v2 wire format)
const verifyPayment = async (paymentPayload, paymentRequirements) => {
const requestBody = {
x402Version: 1,
x402Version: X402_VERSION,
paymentPayload: {
x402Version: 1,
scheme: X402_CONFIG.scheme,
network: X402_CONFIG.network,
x402Version: X402_VERSION,
accepted: toAccepted(paymentRequirements),
payload: paymentPayload
},
paymentRequirements
};

console.log('=== VERIFY REQUEST ===');
const bodyString = JSON.stringify(requestBody);

Expand Down Expand Up @@ -111,23 +132,22 @@ const verifyPayment = async (paymentPayload, paymentRequirements) => {
req.end();
};

makeRequest('https://x402.org/facilitator/verify');
makeRequest(`${X402_CONFIG.facilitatorUrl}/verify`);
});
};

// Settle payment with x402.org facilitator
// Settle payment with x402.org facilitator (x402 v2 wire format)
const settlePayment = async (paymentPayload, paymentRequirements) => {
const requestBody = {
x402Version: 1,
x402Version: X402_VERSION,
paymentPayload: {
x402Version: 1,
scheme: X402_CONFIG.scheme,
network: X402_CONFIG.network,
x402Version: X402_VERSION,
accepted: toAccepted(paymentRequirements),
payload: paymentPayload
},
paymentRequirements
};

console.log('=== SETTLE REQUEST ===');
const bodyString = JSON.stringify(requestBody);

Expand Down Expand Up @@ -168,7 +188,7 @@ const settlePayment = async (paymentPayload, paymentRequirements) => {
req.end();
};

makeRequest('https://x402.org/facilitator/settle');
makeRequest(`${X402_CONFIG.facilitatorUrl}/settle`);
});
};

Expand All @@ -181,35 +201,27 @@ app.use('/generate_image', async (c, next) => {
// Use provided price or default estimate (in USDC wei)
const estimatedCost = price || '20000'; // ~$0.02 default

// Check for PAYMENT-SIGNATURE header (x402 v2 standard) or X-PAYMENT (legacy)
const paymentHeader = c.req.header('PAYMENT-SIGNATURE') || c.req.header('X-PAYMENT');

// x402 v2: client sends the signed payment in the PAYMENT-SIGNATURE header.
const paymentHeader = c.req.header('PAYMENT-SIGNATURE');
const resourceUrl = `${(process.env.GATEWAY_URL || 'https://example.com').replace(/\/$/, '')}/generate_image`;

if (!paymentHeader) {
const sellerWallet = process.env.SELLER_WALLET;
const paymentRequirements = {
scheme: X402_CONFIG.scheme,
network: X402_CONFIG.network,
maxAmountRequired: String(estimatedCost),
resource: `${(process.env.GATEWAY_URL || 'https://example.com').replace(/\/$/, '')}/generate_image`,
description: 'AI image generation with Nova Canvas',
mimeType: 'application/json',
outputSchema: { status: 'string', request_id: 'string', message: 'string' },
payTo: sellerWallet,
asset: X402_CONFIG.usdcBase,
maxTimeoutSeconds: 300,
extra: {
name: 'USDC',
version: '2',
chainId: 84532
}
};
const paymentRequirements = buildPaymentRequirements(estimatedCost, resourceUrl);
// x402 v2: resource metadata is hoisted to the top level of the 402 body.
const x402Response = {
x402Version: 1,
x402Version: X402_VERSION,
accepts: [paymentRequirements],
resource: {
url: resourceUrl,
description: 'AI image generation with Nova Canvas',
mimeType: 'application/json'
},
error: 'Payment required'
};
console.log('=== 402 RESPONSE ===');
console.log(JSON.stringify(x402Response, null, 2));
// x402 v2: also surface requirements in the PAYMENT-REQUIRED header (base64 JSON).
c.header('PAYMENT-REQUIRED', Buffer.from(JSON.stringify(x402Response)).toString('base64'));
return c.json(x402Response, 402);
}

Expand All @@ -225,45 +237,29 @@ app.use('/generate_image', async (c, next) => {
return c.json({ error: 'Invalid payment payload' }, 400);
}

// Extract authorization (handle both formats)
const authorization = paymentPayload.payload?.authorization || paymentPayload.authorization;
// x402 v2 payload shape: { x402Version, payload: { signature, authorization }, accepted }
const authorization = paymentPayload.payload?.authorization;
const authorizedValue = authorization?.value;
if (!authorizedValue) {
console.log('Missing authorization value');
return c.json({ error: 'Missing authorization value' }, 400);
}

// Idempotency check using nonce
const nonce = authorization?.nonce;
if (nonce && processedPayments.has(nonce)) {
return c.json({ error: 'Payment already processed' }, 409);
}

// Create payment requirements using EXACT value from authorization
const sellerWallet = process.env.SELLER_WALLET;
const paymentRequirements = {
scheme: X402_CONFIG.scheme,
network: X402_CONFIG.network,
maxAmountRequired: authorizedValue,
resource: `${(process.env.GATEWAY_URL || 'https://example.com').replace(/\/$/, '')}/generate_image`,
description: 'AI image generation with Nova Canvas',
mimeType: 'application/json',
outputSchema: { status: 'string', request_id: 'string', message: 'string' },
payTo: sellerWallet,
asset: X402_CONFIG.usdcBase,
maxTimeoutSeconds: 300,
extra: {
name: 'USDC',
version: '2',
chainId: 84532
}
};


// Rebuild payment requirements using the EXACT value from the authorization (do not
// trust the client's `accepted` copy — the seller is the source of truth).
const paymentRequirements = buildPaymentRequirements(authorizedValue, resourceUrl);

console.log('=== PAYMENT VERIFICATION ===');
console.log('Payment requirements:', JSON.stringify(paymentRequirements, null, 2));
console.log('Payment payload from client:', JSON.stringify(paymentPayload, null, 2));
console.log('Authorization value:', authorizedValue);
console.log('Seller wallet:', sellerWallet);
console.log('Seller wallet:', process.env.SELLER_WALLET);
console.log('Asset (USDC):', X402_CONFIG.usdcBase);

// Verify payment with x402.org facilitator (do NOT settle yet - settle after content delivery per x402 spec)
Expand Down
2 changes: 1 addition & 1 deletion agentic/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ coinbase-agentkit==0.7.4
requests
python-dotenv
web3>=6.0.0
x402>=0.1.0,<1.0
x402[httpx,evm]>=2.0.0,<3.0
httpx
bedrock-agentcore
Loading