├── .dockerignore ├── .env.example ├── .gitignore ├── DEPLOYING_TO_EIGENCOMPUTE.md ├── Dockerfile ├── LICENSE ├── PAYMENT_FLOW_EXPLANATION.md ├── QUICKSTART.md ├── README.md ├── RPC_CONFIGURATION.md ├── TESTING.md ├── check-wallet.js ├── header.jpg ├── package-lock.json ├── package.json ├── setup.sh ├── src ├── ExampleService.ts ├── MerchantExecutor.ts ├── server.ts ├── testClient.ts └── x402Types.ts ├── test-agent.sh ├── test-facilitator.js ├── test-request.sh └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | .env 5 | .env.* 6 | .DS_Store 7 | coverage 8 | tmp 9 | .turbo 10 | *.local 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | PORT=3000 3 | 4 | # Payment Configuration 5 | # The wallet address that will receive payments via the facilitator 6 | PAY_TO_ADDRESS=0xYourWalletAddress 7 | 8 | # Network Configuration 9 | # Options: "base", "base-sepolia", "ethereum", "polygon", "polygon-amoy" 10 | NETWORK=base-sepolia 11 | 12 | # OpenAI Configuration 13 | # Your OpenAI API key for AI processing 14 | OPENAI_API_KEY=your_openai_api_key_here 15 | # Optional: override the OpenAI base URL (advanced) 16 | # OPENAI_BASE_URL=https://api.openai.com/v1 17 | 18 | # AI Provider Configuration (default is openai) 19 | # Options: "openai" or "eigenai" 20 | # AI_PROVIDER=eigenai 21 | # AI_MODEL=gpt-oss-120b-f16 22 | # AI_TEMPERATURE=0.7 23 | # AI_MAX_TOKENS=500 24 | # AI_SEED=42 25 | 26 | # Test Client Configuration (optional - only needed for testing with payments) 27 | # CLIENT_PRIVATE_KEY=your_test_wallet_private_key_here 28 | # AGENT_URL=http://localhost:3000 29 | 30 | # Optional: Facilitator configuration 31 | # FACILITATOR_URL=https://your-custom-facilitator.com 32 | # FACILITATOR_API_KEY=your_api_key_if_required 33 | 34 | # Optional: EigenAI configuration (required if AI_PROVIDER=eigenai) 35 | # EIGENAI_API_KEY=your_eigenai_api_key_here 36 | # EIGENAI_BASE_URL=https://eigenai-sepolia.eigencloud.xyz/v1 37 | 38 | # Optional: Local (direct) settlement configuration 39 | # SETTLEMENT_MODE=local 40 | # PRIVATE_KEY=your_private_key_here 41 | # RPC_URL=https://base-sepolia.g.alchemy.com/v2/your-api-key 42 | 43 | # Optional: Custom network configuration (required if NETWORK is not built-in) 44 | # ASSET_ADDRESS=0xTokenAddress 45 | # ASSET_NAME=USDC 46 | # EXPLORER_URL=https://explorer.your-network.org 47 | # CHAIN_ID=84532 48 | 49 | # Optional: Public service URL used in payment requirements 50 | # SERVICE_URL=http://localhost:3000/process 51 | 52 | # Optional: Debug logging 53 | X402_DEBUG=true 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | 7 | # Environment variables 8 | .env 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | 15 | # OS files 16 | .DS_Store 17 | .DS_Store? 18 | ._* 19 | 20 | # IDE 21 | .vscode/ 22 | .idea/ 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # TypeScript 28 | *.tsbuildinfo 29 | -------------------------------------------------------------------------------- /DEPLOYING_TO_EIGENCOMPUTE.md: -------------------------------------------------------------------------------- 1 | # Deploying to EigenCompute with EigenX 2 | 3 | EigenX enables deployment of containerized applications to Trusted Execution Environments (TEEs) with built-in private key management and hardware-level isolation. 4 | 5 | ## Prerequisites 6 | 7 | 1. **Allowlisted Account** - Submit an [onboarding request](https://forms.gle/eigenx-onboarding) with your Ethereum address 8 | 2. **Docker** - For building and pushing application images 9 | 3. **Sepolia ETH** - For deployment transactions ([Google Cloud Faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) | [Alchemy Faucet](https://www.alchemy.com/faucets/ethereum-sepolia)) 10 | 11 | ## Installation 12 | 13 | **macOS/Linux:** 14 | ```bash 15 | curl -fsSL https://eigenx-scripts.s3.us-east-1.amazonaws.com/install-eigenx.sh | bash 16 | ``` 17 | 18 | **Windows:** 19 | ```powershell 20 | curl -fsSL https://eigenx-scripts.s3.us-east-1.amazonaws.com/install-eigenx.ps1 | powershell - 21 | ``` 22 | 23 | ## Deployment Steps 24 | 25 | ### 1. Initial Setup 26 | 27 | ```bash 28 | # Login to Docker registry 29 | docker login 30 | 31 | # Authenticate with EigenX (existing key) 32 | eigenx auth login 33 | 34 | # Or generate new key 35 | eigenx auth generate --store 36 | 37 | # Verify authentication 38 | eigenx auth whoami 39 | ``` 40 | 41 | ### 2. Create New Application 42 | 43 | ```bash 44 | # Create from template (typescript | python | golang | rust) 45 | eigenx app create my-app typescript 46 | cd my-app 47 | 48 | # Configure environment variables 49 | cp .env.example .env 50 | # Edit .env with your configuration 51 | ``` 52 | 53 | ### 3. Deploy to TEE 54 | 55 | ```bash 56 | eigenx app deploy 57 | ``` 58 | 59 | ### 4. Monitor Your Application 60 | 61 | ```bash 62 | # View app details 63 | eigenx app info 64 | 65 | # Stream logs 66 | eigenx app logs --watch 67 | ``` 68 | 69 | ## Deploying Existing Projects 70 | 71 | EigenX works with any Docker-based project: 72 | 73 | ```bash 74 | cd my-existing-project 75 | 76 | # Ensure you have: 77 | # - Dockerfile (must target linux/amd64, run as root) 78 | # - .env file (optional) 79 | 80 | eigenx app deploy 81 | ``` 82 | 83 | ## Key Features 84 | 85 | - **Secure Execution** - Intel TDX hardware isolation 86 | - **Auto-Generated Wallet** - Access via `process.env.MNEMONIC` 87 | - **Private Environment Variables** - Encrypted within TEE 88 | - **Public Variables** - Suffix with `_PUBLIC` for transparency 89 | 90 | ## Common Commands 91 | 92 | ```bash 93 | eigenx app list # List all apps 94 | eigenx app upgrade my-app # Update deployment 95 | eigenx app stop my-app # Stop app 96 | eigenx app start my-app # Start app 97 | eigenx app terminate my-app # Remove app permanently 98 | ``` 99 | 100 | ## TLS/HTTPS Setup (Optional) 101 | 102 | ```bash 103 | # Add TLS configuration 104 | eigenx app configure tls 105 | 106 | # Configure DNS A record pointing to instance IP 107 | # Set in .env: 108 | # DOMAIN=yourdomain.com 109 | # APP_PORT=3000 110 | # ACME_STAGING=true # Test first 111 | 112 | eigenx app upgrade 113 | ``` 114 | 115 | ## Advanced: Manual Image Deployment 116 | 117 | ```bash 118 | # Build and push manually 119 | docker build --platform linux/amd64 -t myregistry/myapp:v1.0 . 120 | docker push myregistry/myapp:v1.0 121 | 122 | # Deploy using image reference 123 | eigenx app deploy myregistry/myapp:v1.0 124 | ``` 125 | 126 | ## Important Notes 127 | 128 | ⚠️ **Mainnet Alpha Limitations:** 129 | - Not recommended for significant customer funds 130 | - Developer is still trusted (full verifiability coming later) 131 | - No SLA guarantees 132 | 133 | ⚠️ **Alpha Software:** 134 | - Under active development, not audited 135 | - Use for testing only, not production 136 | - Breaking changes expected 137 | 138 | --- 139 | 140 | **Need Help?** 141 | - Check authentication: `eigenx auth whoami` 142 | - View app status: `eigenx app info --watch` 143 | - Report issues: https://github.com/Layr-Labs/eigenx-cli/issues 144 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS base 2 | WORKDIR /app 3 | 4 | FROM base AS deps 5 | COPY package*.json ./ 6 | RUN apk add --no-cache python3 make g++ \ 7 | && ln -sf python3 /usr/bin/python 8 | RUN npm ci 9 | 10 | FROM deps AS builder 11 | COPY tsconfig.json ./ 12 | COPY src ./src 13 | RUN npm run build 14 | RUN npm prune --omit=dev 15 | 16 | FROM base AS runner 17 | COPY --from=builder /app/node_modules ./node_modules 18 | COPY --from=builder /app/dist ./dist 19 | COPY package*.json ./ 20 | ENV NODE_ENV=production 21 | EXPOSE 3000 22 | CMD ["node", "dist/server.js"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Nader Dabit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PAYMENT_FLOW_EXPLANATION.md: -------------------------------------------------------------------------------- 1 | # Payment Flow Explanation 2 | 3 | ## Current Status 4 | 5 | Your wallet is funded and ready: 6 | - **Client Wallet**: `0xf59B3Cd80021b77c43EA011356567095C4E45b0e` 7 | - ETH: 0.0948 (for gas) 8 | - USDC: 19.17 (enough for ~191 tests) 9 | - **Merchant Wallet**: `0x3B9b10B8a63B93Ae8F447A907FD1EF067153c4e5` 10 | - ETH: 0.0549 11 | - USDC: 2.0 12 | 13 | ## Why No Transactions Are Showing 14 | 15 | The x402 protocol uses **EIP-3009** (transferWithAuthorization), which is different from a regular ERC-20 transfer: 16 | 17 | 1. **Traditional Transfer**: Client directly calls `transfer()` on the USDC contract 18 | 2. **EIP-3009**: Client signs an authorization, then the facilitator calls `transferWithAuthorization()` 19 | 20 | ### The Flow: 21 | 22 | ``` 23 | 1. Client → API: "Process my request" 24 | 2. API → Client: 402 Payment Required (with payment requirements) 25 | 3. Client → Signs payment authorization (NO blockchain transaction yet) 26 | 4. Client → API: "Here's my request with signed payment" 27 | 5. API → Verifies signature is valid 28 | 6. API → Blockchain: Calls transferWithAuthorization() directly ← ACTUAL TRANSACTION 29 | 7. API → Processes request 30 | 8. API → Client: Returns service response 31 | ``` 32 | 33 | ## Current Test Behavior 34 | 35 | The test is correctly: 36 | - ✅ Signing the payment authorization 37 | - ✅ Submitting it to the API 38 | - ✅ API is verifying the signature 39 | 40 | However: 41 | - ❌ The actual blockchain transaction (step 6) is not happening 42 | - ❌ No transaction hash is being returned 43 | 44 | ## Why? 45 | 46 | The `MerchantExecutor` handles the payment verification and settlement. Looking at the server logs: 47 | ``` 48 | 💰 Payment required for request processing 49 | ``` 50 | 51 | This confirms the API is correctly requiring payment, and the `MerchantExecutor` handles the payment flow for verification and settlement. 52 | 53 | ## Solution: Understanding MerchantExecutor 54 | 55 | The `MerchantExecutor` handles: 56 | 1. Payment requirements generation 57 | 2. Payment signature verification 58 | 3. Payment settlement on blockchain 59 | 60 | The payment flow works as follows: 61 | 1. Request without payment → returns 402 with payment requirements 62 | 2. Request with payment → verifies signature locally 63 | 3. If valid → settles payment via `transferWithAuthorization()` on USDC contract 64 | 4. Then passes control to `ExampleService` to process the request 65 | 66 | ## How to See Real Transactions 67 | 68 | To see actual blockchain transactions on Base Sepolia: 69 | 1. The API accepts the signed authorization 70 | 2. Calls `transferWithAuthorization()` on the USDC contract directly 71 | 3. Returns a transaction hash 72 | 73 | The transaction would show on Base Sepolia: 74 | - https://sepolia.basescan.org/address/0xf59B3Cd80021b77c43EA011356567095C4E45b0e (client) 75 | - https://sepolia.basescan.org/address/0x3B9b10B8a63B93Ae8F447A907FD1EF067153c4e5 (merchant) 76 | 77 | ## Next Steps 78 | 79 | 1. **Configure PRIVATE_KEY**: Set the merchant private key to enable direct settlement 80 | 2. **Add RPC_URL (optional)**: Configure a custom RPC endpoint for blockchain interaction 81 | 3. **Run tests**: Execute the test client to see the full payment flow with settlement 82 | 83 | ## Testing the Full Flow 84 | 85 | Run the test with the server logs visible: 86 | 87 | **Terminal 1** (Server): 88 | ```bash 89 | npm run dev 90 | ``` 91 | 92 | **Terminal 2** (Test): 93 | ```bash 94 | npm test 95 | ``` 96 | 97 | Watch for: 98 | - "💰 Settling payment..." in the server logs 99 | - "✅ Payment settlement result" with transaction hash 100 | - "Transaction: 0x..." if successful 101 | 102 | If you see a transaction hash, check it on BaseScan: 103 | ``` 104 | https://sepolia.basescan.org/tx/[TRANSACTION_HASH] 105 | ``` 106 | 107 | ## Understanding the Test Output 108 | 109 | Expected test output: 110 | ``` 111 | 💳 Payment required! 112 | 🔐 Signing payment... 113 | ✅ Payment signed successfully 114 | ✅ Payment accepted and request processed! 115 | 🎉 SUCCESS! Response: [actual response] 116 | ``` 117 | 118 | The test flow: 119 | 1. First request without payment → receives 402 120 | 2. Client signs payment authorization 121 | 3. Second request with payment → payment verified 122 | 4. API settles payment on blockchain 123 | 5. API processes request and returns response 124 | 125 | ## Monitoring 126 | 127 | To monitor your wallets for transactions: 128 | ```bash 129 | node check-wallet.js 130 | ``` 131 | 132 | This will show current balances and transaction links. 133 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | Get the x402 payment API running in 5 minutes! 4 | 5 | ## Prerequisites 6 | 7 | - Node.js 18+ installed 8 | - OpenAI API key ([Get one here](https://platform.openai.com/api-keys)) 9 | - A wallet address to receive USDC payments 10 | 11 | ## Setup Steps 12 | 13 | ### 1. Run the setup script 14 | 15 | ```bash 16 | ./setup.sh 17 | ``` 18 | 19 | This will: 20 | - Install dependencies 21 | - Create a .env file from template 22 | - Build the API 23 | 24 | ### 2. Configure environment variables 25 | 26 | Edit the `.env` file: 27 | 28 | ```bash 29 | nano .env 30 | ``` 31 | 32 | **Required variables:** 33 | 34 | ```env 35 | OPENAI_API_KEY=sk-your-openai-api-key 36 | PAY_TO_ADDRESS=0xYourWalletAddress 37 | ``` 38 | 39 | **Optional variables:** 40 | 41 | ```env 42 | PORT=3000 43 | NETWORK=base-sepolia # for testing 44 | PRIVATE_KEY=your_private_key # if needed 45 | X402_DEBUG=true # for detailed logs 46 | ``` 47 | 48 | ### 3. Start the API 49 | 50 | ```bash 51 | npm start 52 | ``` 53 | 54 | You should see: 55 | 56 | ``` 57 | 🚀 x402 Payment API initialized 58 | 💰 Payment address: 0xYourAddress... 59 | 🌐 Network: base-sepolia 60 | 💵 Price per request: $0.10 USDC 61 | 62 | ✅ Server running on http://localhost:3000 63 | 📖 Health check: http://localhost:3000/health 64 | 🧪 Test endpoint: POST http://localhost:3000/test 65 | 🚀 Main endpoint: POST http://localhost:3000/process 66 | ``` 67 | 68 | ### 4. Test the API 69 | 70 | In a new terminal: 71 | 72 | ```bash 73 | ./test-request.sh 74 | ``` 75 | 76 | You should see a `402 Payment Required` response with payment details! 77 | 78 | ## What happens next? 79 | 80 | 1. **Without payment**: The API returns 402 with payment requirements 81 | 2. **With payment**: A client signs a payment, sends it, and receives the service response 82 | 83 | ## Testing with payment 84 | 85 | To actually process requests (with payment), you need: 86 | 87 | 1. An x402-compatible client library 88 | 2. A wallet with USDC and gas tokens 89 | 3. Testnet setup (recommended: base-sepolia) 90 | 91 | See the [full README](./README.md) for complete documentation. 92 | 93 | ## Common Issues 94 | 95 | ### "OPENAI_API_KEY is required" 96 | 97 | Make sure `.env` file exists and has `OPENAI_API_KEY` set. 98 | 99 | ### "PAY_TO_ADDRESS is required" 100 | 101 | Add your wallet address to `.env`: 102 | 103 | ```env 104 | PAY_TO_ADDRESS=0xYourAddress 105 | ``` 106 | 107 | ### Build fails 108 | 109 | Try cleaning and rebuilding: 110 | 111 | ```bash 112 | npm run clean 113 | npm install 114 | npm run build 115 | ``` 116 | 117 | ### Can't connect to server 118 | 119 | Check if the port is already in use: 120 | 121 | ```bash 122 | lsof -i :3000 123 | ``` 124 | 125 | Change the port in `.env`: 126 | 127 | ```env 128 | PORT=3001 129 | ``` 130 | 131 | ## Next Steps 132 | 133 | - Read the [full README](./README.md) for detailed documentation 134 | - Check the [x402 package](https://www.npmjs.com/package/x402) for payment integration 135 | - Build a client to interact with your API 136 | - Replace the OpenAI example with your own service logic 137 | - Deploy to production 138 | 139 | ## Support 140 | 141 | For issues or questions: 142 | - Check the README.md 143 | - Review the x402 library docs 144 | - Open an issue on GitHub 145 | 146 | Happy building! 🚀 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![x402 Starter Kit](header.jpg) 2 | 3 | # x402 Starter Kit 4 | 5 | A starter kit for building paid APIs using the x402 payment protocol. 6 | 7 | ## Overview 8 | 9 | This starter kit demonstrates how to build paid APIs using x402. It: 10 | 11 | 1. Receives API requests 12 | 2. Requires payment (in this example of $0.10 USDC) before processing 13 | 3. Verifies and settles payments through the x402 facilitator (defaulting to [https://x402.org/facilitator](https://docs.cdp.coinbase.com/x402/network-support#x402-org-facilitator)) 14 | 4. Processes requests (using OpenAI/EigenAI as configurable examples) 15 | 5. Returns responses after payment is confirmed 16 | 17 | ## Architecture 18 | 19 | The API consists of three main components: 20 | 21 | - **ExampleService**: Example service logic that processes requests using OpenAI or EigenAI (replace with your own service implementation) 22 | - **MerchantExecutor**: Calls the x402 facilitator service for verification/settlement (defaults to `https://x402.org/facilitator`, configurable via `FACILITATOR_URL`) 23 | - **Server**: Express HTTP server that orchestrates payment validation and request processing 24 | 25 | ## Prerequisites 26 | 27 | - Node.js 18 or higher 28 | - A wallet with some ETH for gas fees (on your chosen network) 29 | - An OpenAI or EigenAI API key (for the example implementation - replace with your own API) 30 | - A wallet address to receive USDC payments 31 | - Optional: to deploy to EigenCompute (for Verifiable Runtime), follow [these steps](DEPLOYING_TO_EIGENCOMPUTE.md). To sign up for EigenAI (for Verifiable Inference), start [here](https://docs.eigencloud.xyz/products/eigenai/eigenai-overview) 32 | 33 | ## Setup 34 | 35 | ### 1. Install Dependencies 36 | 37 | ```bash 38 | npm install 39 | ``` 40 | 41 | ### 2. Configure Environment Variables 42 | 43 | Copy the example environment file: 44 | 45 | ```bash 46 | cp .env.example .env 47 | ``` 48 | 49 | Edit `.env` and fill in your values: 50 | 51 | ```env 52 | # Server Configuration 53 | PORT=3000 54 | 55 | # Payment Configuration 56 | # Wallet address that will receive USDC payments 57 | PAY_TO_ADDRESS=0xYourWalletAddress 58 | 59 | # Network Configuration 60 | # Built-in options: "base", "base-sepolia", "polygon", "polygon-amoy", "avalanche", 61 | # "avalanche-fuji", "iotex", "sei", "sei-testnet", "peaq", "solana", "solana-devnet" 62 | # For a custom network, set NETWORK to an identifier of your choice and provide 63 | # ASSET_ADDRESS, ASSET_NAME, and (for EVM networks) CHAIN_ID. Direct settlement is 64 | # available on EVM networks only. 65 | NETWORK=base-sepolia 66 | 67 | # OpenAI Configuration 68 | # Your OpenAI API key for the example service (replace with your own API configuration) 69 | OPENAI_API_KEY=your_openai_api_key_here 70 | # Optional: override the OpenAI base URL 71 | # OPENAI_BASE_URL=https://api.openai.com/v1 72 | 73 | # AI Provider Configuration (default is openai) 74 | # Options: "openai" or "eigenai" 75 | # AI_PROVIDER=eigenai 76 | # AI_MODEL=gpt-oss-120b-f16 77 | # AI_TEMPERATURE=0.7 78 | # AI_MAX_TOKENS=500 79 | # AI_SEED=42 80 | 81 | # EigenAI Configuration (required if AI_PROVIDER=eigenai) 82 | # EIGENAI_API_KEY=your_eigenai_api_key_here 83 | # EIGENAI_BASE_URL=https://eigenai.eigencloud.xyz/v1 84 | 85 | # Facilitator Configuration (optional) 86 | # FACILITATOR_URL=https://your-custom-facilitator.com 87 | # FACILITATOR_API_KEY=your_api_key_if_required 88 | 89 | # Local Settlement (optional) 90 | # SETTLEMENT_MODE=local 91 | # PRIVATE_KEY=your_private_key_here 92 | # RPC_URL=https://base-sepolia.g.alchemy.com/v2/your-api-key 93 | 94 | # Custom Network Details (required if NETWORK is not base/base-sepolia/polygon/polygon-amoy) 95 | # ASSET_ADDRESS=0xTokenAddress 96 | # ASSET_NAME=USDC 97 | # EXPLORER_URL=https://explorer.your-network.org 98 | # CHAIN_ID=84532 99 | 100 | # Public Service URL (optional) 101 | # Used in payment requirements so the facilitator sees a fully-qualified resource URL 102 | # SERVICE_URL=http://localhost:3000/process 103 | 104 | # Test Client Configuration (optional - only needed for end-to-end payment testing) 105 | # CLIENT_PRIVATE_KEY=your_test_wallet_private_key_here 106 | # AGENT_URL=http://localhost:3000 107 | 108 | # Optional: Debug logging 109 | X402_DEBUG=true 110 | ``` 111 | 112 | ## Quickstart 113 | 114 | 1. **Run the API** 115 | ```bash 116 | npm run dev 117 | ``` 118 | 2. **Run the test suite (in another terminal)** 119 | ```bash 120 | npm test 121 | ``` 122 | 123 | **Settlement Modes:** 124 | - Default: no extra config, uses the hosted facilitator at `https://x402.org/facilitator` 125 | - Local (direct): set `SETTLEMENT_MODE=local`, provide `PRIVATE_KEY`, and optionally override `RPC_URL` for your network 126 | - Custom facilitator: set `FACILITATOR_URL` (and `FACILITATOR_API_KEY` if needed) to call a different facilitator endpoint (e.g., one you host yourself) 127 | - Update `SERVICE_URL` if clients reach your API through a different hostname so the payment requirement has a fully-qualified resource URL 128 | - If you set `NETWORK` to something other than `base`, `base-sepolia`, `polygon`, or `polygon-amoy`, provide `ASSET_ADDRESS`, `ASSET_NAME`, and (for local settlement) `CHAIN_ID` 129 | 130 | **AI Provider:** 131 | - Default: `AI_PROVIDER=openai` (requires `OPENAI_API_KEY`) 132 | - EigenAI: set `AI_PROVIDER=eigenai`, provide `EIGENAI_API_KEY`, and optionally override `EIGENAI_BASE_URL` 133 | - Use `AI_MODEL`, `AI_TEMPERATURE`, `AI_MAX_TOKENS`, and `AI_SEED` to tune inference behaviour for either provider 134 | 135 | **Important:** 136 | - `PAY_TO_ADDRESS` should be your wallet address where you want to receive USDC payments 137 | - `NETWORK` should match where you want to receive payments (recommend `base-sepolia` for testing) 138 | - `OPENAI_API_KEY` is required unless `AI_PROVIDER=eigenai` (then provide `EIGENAI_API_KEY`) 139 | - Never commit your `.env` file to version control 140 | 141 | ## Running the API 142 | 143 | ### Development Mode 144 | 145 | ```bash 146 | npm run dev 147 | ``` 148 | 149 | ### Production Mode 150 | 151 | ```bash 152 | npm run build 153 | npm start 154 | ``` 155 | 156 | The server will start on `http://localhost:3000` (or your configured PORT). 157 | 158 | ### Docker 159 | 160 | ```bash 161 | # Build the image 162 | docker build -t x402-starter . 163 | 164 | # Run the container (make sure .env has the required variables) 165 | docker run --env-file .env -p 3000:3000 x402-starter 166 | ``` 167 | 168 | ## Usage 169 | 170 | ### Health Check 171 | 172 | Check if the API is running: 173 | 174 | ```bash 175 | curl http://localhost:3000/health 176 | ``` 177 | 178 | Response: 179 | ```json 180 | { 181 | "status": "healthy", 182 | "service": "x402-payment-api", 183 | "version": "1.0.0", 184 | "payment": { 185 | "address": "0xYourAddress...", 186 | "network": "base-sepolia", 187 | "price": "$0.10" 188 | } 189 | } 190 | ``` 191 | 192 | ### Testing the API 193 | 194 | We provide multiple ways to test the API: 195 | 196 | #### 1. Quick Test Script 197 | 198 | Run the simple shell test: 199 | 200 | ```bash 201 | ./test-request.sh 202 | ``` 203 | 204 | This tests the health endpoint and payment requirement flow. 205 | 206 | #### 2. Full Test Suite 207 | 208 | Run the comprehensive test client: 209 | 210 | ```bash 211 | npm test 212 | ``` 213 | 214 | This will: 215 | - Check API health 216 | - Test unpaid requests (returns 402) 217 | - Test paid requests (if CLIENT_PRIVATE_KEY is configured) 218 | - Show the complete payment flow 219 | 220 | See [TESTING.md](./TESTING.md) for detailed testing documentation. 221 | 222 | #### 3. Manual Testing (Simple) 223 | 224 | For quick testing without the full A2A protocol: 225 | 226 | ```bash 227 | curl -X POST http://localhost:3000/test \ 228 | -H "Content-Type: application/json" \ 229 | -d '{"text": "Tell me a joke about programming"}' 230 | ``` 231 | 232 | This will return a payment required error since no payment was made. 233 | 234 | #### Main Endpoint (A2A Compatible) 235 | 236 | Send a request using the A2A message format: 237 | 238 | ```bash 239 | curl -X POST http://localhost:3000/process \ 240 | -H "Content-Type: application/json" \ 241 | -d '{ 242 | "message": { 243 | "parts": [ 244 | { 245 | "kind": "text", 246 | "text": "What is the meaning of life?" 247 | } 248 | ] 249 | } 250 | }' 251 | ``` 252 | 253 | **Expected Response (402 Payment Required):** 254 | 255 | ```json 256 | { 257 | "error": "Payment Required", 258 | "x402": { 259 | "x402Version": 1, 260 | "accepts": [ 261 | { 262 | "scheme": "exact", 263 | "network": "base-sepolia", 264 | "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", 265 | "payTo": "0xYourAddress...", 266 | "maxAmountRequired": "100000", 267 | "resource": "/process-request", 268 | "description": "AI request processing service", 269 | "mimeType": "application/json", 270 | "maxTimeoutSeconds": 3600, 271 | "extra": { 272 | "name": "USDC", 273 | "version": "2" 274 | } 275 | } 276 | ], 277 | "error": "Payment required for service: /process-request" 278 | } 279 | } 280 | ``` 281 | 282 | To complete the payment and process the request, you'll need to: 283 | 284 | 1. Create a payment payload using the x402 client library 285 | 2. Sign the payment with your wallet 286 | 3. Submit the payment back to the `/process` endpoint 287 | 288 | For a complete client example, see the [`x402` library documentation](https://www.npmjs.com/package/x402). 289 | 290 | ## How It Works 291 | 292 | ### Payment Flow 293 | 294 | 1. **Client sends request** → API receives the request 295 | 2. **API requires payment** → Returns 402 with payment requirements 296 | 3. **Client signs payment** → Creates EIP-3009 authorization 297 | 4. **Client submits payment** → Sends signed payment back to API 298 | 5. **API verifies payment** → Checks signature and authorization 299 | 6. **API processes request** → Calls your service (OpenAI in this example) 300 | 7. **API settles payment** → Completes blockchain transaction 301 | 8. **API returns response** → Sends the service response 302 | 303 | ### Payment Verification 304 | 305 | `src/MerchantExecutor.ts` sends the payment payload either to the configured x402 facilitator **or** verifies/settles locally, depending on the settlement mode: 306 | 307 | - **Facilitator mode** (default): forwards payloads to `https://x402.org/facilitator` or the URL set in `FACILITATOR_URL` 308 | - **Local mode**: verifies signatures with `ethers.verifyTypedData` and submits `transferWithAuthorization` via your configured RPC/PRIVATE_KEY 309 | 310 | Make sure `SERVICE_URL` reflects the public URL of your paid endpoint so the facilitator can validate the `resource` field when using facilitator mode. 311 | 312 | ### Error Handling 313 | 314 | - **Missing payment**: Returns 402 Payment Required 315 | - **Invalid payment**: Returns payment verification failure 316 | - **OpenAI error**: Returns error message in task status 317 | - **Settlement failure**: Returns settlement error details 318 | 319 | ## Development 320 | 321 | ### Project Structure 322 | 323 | ``` 324 | x402-developer-starter-kit/ 325 | ├── src/ 326 | │ ├── server.ts # Express server and endpoints 327 | │ ├── ExampleService.ts # Example service logic (replace with your own) 328 | │ ├── MerchantExecutor.ts # Payment verification & settlement helpers 329 | │ ├── x402Types.ts # Shared task/message types 330 | │ └── testClient.ts # Test client for development 331 | ├── package.json 332 | ├── tsconfig.json 333 | ├── .env.example 334 | ├── README.md 335 | ├── TESTING.md 336 | └── test-request.sh 337 | ``` 338 | 339 | ### Building 340 | 341 | ```bash 342 | npm run build 343 | ``` 344 | 345 | Compiled files will be in the `dist/` directory. 346 | 347 | ### Cleaning 348 | 349 | ```bash 350 | npm run clean 351 | ``` 352 | 353 | ## Testing with Real Payments 354 | 355 | To test with real USDC payments: 356 | 357 | 1. Switch to a testnet (e.g., `base-sepolia`) 358 | 2. Get testnet USDC from a faucet 359 | 3. Use a client that implements the x402 protocol 360 | 4. Make sure your wallet has testnet ETH for gas 361 | 362 | ## Troubleshooting 363 | 364 | ### "OPENAI_API_KEY is required" 365 | 366 | Make sure you've set `OPENAI_API_KEY` in your `.env` file. 367 | 368 | ### "PAY_TO_ADDRESS is required" 369 | 370 | Make sure you've set `PAY_TO_ADDRESS` in your `.env` file to your wallet address. 371 | 372 | ### Payment verification fails 373 | 374 | - Check that you're using the correct network 375 | - Verify your wallet has USDC approval set 376 | - Make sure the payment amount matches ($0.10) 377 | - If signature verification fails, review the logged invalid reason and confirm the client signed the latest payment requirements 378 | - For facilitator settlement errors, confirm the facilitator is reachable and that any `FACILITATOR_URL` / `FACILITATOR_API_KEY` settings are correct 379 | - For local settlement errors, ensure your `PRIVATE_KEY` has gas and that the configured `RPC_URL` (or the network default) is responsive 380 | 381 | ### OpenAI rate limits 382 | 383 | If you hit OpenAI rate limits, consider: 384 | - Using `gpt-3.5-turbo` instead of `gpt-4o-mini` 385 | - Implementing request queuing 386 | - Adding rate limiting to your API 387 | - Replacing OpenAI with your own service 388 | 389 | ## Security Considerations 390 | 391 | - Never commit your `.env` file 392 | - Keep your private key secure 393 | - Use testnet for development 394 | - Validate all payment data before processing 395 | - Implement rate limiting for production 396 | - Monitor for failed payment attempts 397 | 398 | ## Next Steps 399 | 400 | - Replace the example OpenAI service with your own API logic 401 | - Implement request queuing for high volume 402 | - Add support for different payment tiers 403 | - Create a web client interface 404 | - Add analytics and monitoring 405 | - Implement caching for common requests 406 | - Add support for streaming responses 407 | 408 | ## License 409 | 410 | ISC 411 | 412 | ## Resources 413 | 414 | - [x402 Package on npm](https://www.npmjs.com/package/x402) 415 | - [A2A Specification](https://github.com/google/a2a) 416 | - [OpenAI API Documentation](https://platform.openai.com/docs) 417 | -------------------------------------------------------------------------------- /RPC_CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # RPC Configuration Guide 2 | 3 | ## How RPC URLs Work in x402 AI Agent 4 | 5 | ### Default Behavior 6 | 7 | By default, **you don't need to configure an RPC URL**. The agent uses the default x402 facilitator service at `https://x402.org/facilitator`, which handles all blockchain interactions for you: 8 | 9 | ``` 10 | Your Agent → x402 Facilitator → Blockchain (via Facilitator's RPC) 11 | ``` 12 | 13 | The facilitator service: 14 | - Manages RPC connections 15 | - Verifies payment signatures 16 | - Executes blockchain transactions 17 | - Handles gas management 18 | - Returns transaction receipts 19 | 20 | ### Architecture Overview 21 | 22 | ``` 23 | ┌─────────────────┐ 24 | │ Your Agent │ 25 | │ (server.ts) │ 26 | └────────┬────────┘ 27 | │ 28 | │ Payment verification/settlement 29 | ▼ 30 | ┌─────────────────┐ 31 | │ Facilitator │ 32 | │ (x402.org or │ 33 | │ custom) │ 34 | └────────┬────────┘ 35 | │ 36 | │ RPC calls 37 | ▼ 38 | ┌─────────────────┐ 39 | │ Blockchain │ 40 | │ (Base, Polygon, │ 41 | │ etc.) │ 42 | └─────────────────┘ 43 | ``` 44 | 45 | ## Configuration Options 46 | 47 | ### Option 1: Default (Recommended) 48 | 49 | **No configuration needed!** Just leave `.env` as is: 50 | 51 | ```env 52 | # No FACILITATOR_URL needed 53 | # Uses https://x402.org/facilitator by default 54 | ``` 55 | 56 | The default facilitator handles everything for you. 57 | 58 | ### Option 2: Local Settlement (Direct Mode) 59 | 60 | If you want to keep settlement inside this server (no facilitator call), enable the built-in direct flow: 61 | 62 | ```env 63 | SETTLEMENT_MODE=local 64 | PRIVATE_KEY=your_private_key_here 65 | # Optional - override default RPC endpoint for the selected network 66 | RPC_URL=https://base-sepolia.g.alchemy.com/v2/YOUR_KEY 67 | # Provide ASSET_ADDRESS/ASSET_NAME if using a non-built-in network 68 | ASSET_ADDRESS=0xTokenAddress 69 | ASSET_NAME=USDC 70 | CHAIN_ID=84532 71 | ``` 72 | 73 | With these variables set, the agent: 74 | - Verifies the EIP-3009 payload locally 75 | - Uses your RPC endpoint to call `transferWithAuthorization` on the USDC contract 76 | 77 | Make sure the wallet behind `PRIVATE_KEY` holds the gas token for the selected network. 78 | 79 | ### Option 3: Custom Facilitator 80 | 81 | If you want to use a different facilitator service, set: 82 | 83 | ```env 84 | FACILITATOR_URL=https://your-custom-facilitator.com 85 | FACILITATOR_API_KEY=your_api_key_if_required 86 | # Provide ASSET_ADDRESS/ASSET_NAME if the facilitator expects a different asset for your network 87 | ASSET_ADDRESS=0xTokenAddress 88 | ASSET_NAME=USDC 89 | CHAIN_ID=84532 90 | ``` 91 | 92 | Your custom facilitator would need to implement the x402 facilitator API: 93 | - `POST /verify` - Verify payment signatures 94 | - `POST /settle` - Settle payments on-chain 95 | 96 | ### Option 4: Self-Hosted Facilitator 97 | 98 | To run your own facilitator with custom RPC: 99 | 100 | 1. **Deploy your own facilitator service** (see x402 facilitator repo) 101 | 2. **Configure the facilitator** with your RPC URL: 102 | ```env 103 | # In your facilitator's config 104 | RPC_URL=https://base-sepolia.g.alchemy.com/v2/YOUR_KEY 105 | ``` 106 | 3. **Point this agent to your facilitator** (for example, if it runs locally at `http://localhost:4000`): 107 | ```env 108 | # In agent/.env 109 | FACILITATOR_URL=https://your-facilitator.yoursite.com 110 | ``` 111 | 112 | ### Option 5: Direct Blockchain Integration (Advanced) 113 | 114 | Local settlement mode already performs on-chain verification/settlement for USDC via EIP-3009. If you need more control (additional assets, alternative schemes), you can still implement your own executor: 115 | 116 | 1. Replace or extend `src/MerchantExecutor.ts` 117 | 2. Use `ethers` (or another SDK) together with your RPC URL(s) 118 | 3. Add any custom verification/settlement logic your flow requires 119 | 120 | Use the bundled executor as a reference for how to construct payment requirements, log settlement details, and surface errors back to the server. 121 | 122 | ## RPC Providers 123 | 124 | If you need to configure an RPC URL (for custom facilitator or direct integration), here are popular providers: 125 | 126 | ### Alchemy 127 | ```env 128 | RPC_URL=https://base-sepolia.g.alchemy.com/v2/YOUR_API_KEY 129 | ``` 130 | Sign up: https://www.alchemy.com 131 | 132 | ### Infura 133 | ```env 134 | RPC_URL=https://base-sepolia.infura.io/v3/YOUR_PROJECT_ID 135 | ``` 136 | Sign up: https://www.infura.io 137 | 138 | ### QuickNode 139 | ```env 140 | RPC_URL=https://your-endpoint.base-sepolia.quiknode.pro/YOUR_TOKEN/ 141 | ``` 142 | Sign up: https://www.quicknode.com 143 | 144 | ### Public RPCs (Not recommended for production) 145 | ```env 146 | # Base Sepolia 147 | RPC_URL=https://sepolia.base.org 148 | 149 | # Base Mainnet 150 | RPC_URL=https://mainnet.base.org 151 | ``` 152 | 153 | ## Network-Specific RPC URLs 154 | 155 | ### Base Sepolia (Testnet) 156 | - Alchemy: `https://base-sepolia.g.alchemy.com/v2/YOUR_KEY` 157 | - Public: `https://sepolia.base.org` 158 | - Chain ID: 84532 159 | 160 | ### Base Mainnet 161 | - Alchemy: `https://base-mainnet.g.alchemy.com/v2/YOUR_KEY` 162 | - Public: `https://mainnet.base.org` 163 | - Chain ID: 8453 164 | 165 | ### Polygon Amoy (Testnet) 166 | - Alchemy: `https://polygon-amoy.g.alchemy.com/v2/YOUR_KEY` 167 | - Chain ID: 80002 168 | 169 | ### Ethereum Sepolia 170 | - Alchemy: `https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY` 171 | - Infura: `https://sepolia.infura.io/v3/YOUR_PROJECT_ID` 172 | - Chain ID: 11155111 173 | 174 | ## Current Implementation 175 | 176 | The agent currently uses: 177 | 178 | **File: src/server.ts** 179 | ```typescript 180 | const merchantOptions = { 181 | payToAddress: PAY_TO_ADDRESS, 182 | network: resolvedNetwork, 183 | price: 0.1, 184 | facilitatorUrl: FACILITATOR_URL, 185 | facilitatorApiKey: FACILITATOR_API_KEY, 186 | }; 187 | 188 | const merchantExecutor = new MerchantExecutor(merchantOptions); 189 | 190 | if (FACILITATOR_URL) { 191 | console.log(`🌐 Using custom facilitator: ${FACILITATOR_URL}`); 192 | } else { 193 | console.log('🌐 Using default facilitator: https://x402.org/facilitator'); 194 | } 195 | ``` 196 | 197 | ## Recommendations 198 | 199 | ### For Development/Testing 200 | ✅ **Use the default facilitator** (`https://x402.org/facilitator`) 201 | - No configuration needed 202 | - Works out of the box 203 | - Handles testnet transactions 204 | 205 | ### For Production 206 | Consider these options: 207 | 208 | 1. **Default facilitator** (easiest) 209 | - Managed service 210 | - No infrastructure to maintain 211 | - May have rate limits 212 | 213 | 2. **Custom facilitator** (recommended) 214 | - Your own RPC endpoints 215 | - Better control and monitoring 216 | - Can optimize for your needs 217 | - Set up failover/redundancy 218 | 219 | 3. **Direct integration** (advanced) 220 | - Maximum control 221 | - Requires blockchain expertise 222 | - More maintenance 223 | 224 | ## Troubleshooting 225 | 226 | ### "Network error" during payment 227 | - Check facilitator URL is accessible 228 | - Verify API key if using custom facilitator 229 | - Check RPC endpoint is responding (if self-hosting) 230 | 231 | ### "Settlement failed" 232 | - If you're using the hosted facilitator, retry later or contact support if the status page reports issues 233 | - If you're running a custom facilitator, ensure its RPC URL matches the selected network, the settlement wallet has gas, and the RPC endpoint is healthy 234 | 235 | ### "Invalid signature" 236 | - Network mismatch (e.g., mainnet signature on testnet) 237 | - Check NETWORK env variable matches RPC network 238 | 239 | ## Summary 240 | 241 | **Quick Answer:** 242 | - RPC URL is **not required** for basic setup 243 | - The default facilitator at `https://x402.org/facilitator` handles blockchain interactions 244 | - Set `SETTLEMENT_MODE=local` (with `PRIVATE_KEY`/`RPC_URL`) for on-chain settlement inside this server 245 | - Use `FACILITATOR_URL` to point at a custom facilitator endpoint 246 | 247 | **Environment Variables:** 248 | ```env 249 | # Required 250 | OPENAI_API_KEY=your_key 251 | PAY_TO_ADDRESS=0xYourAddress 252 | 253 | # Optional - local (direct) settlement 254 | SETTLEMENT_MODE=local 255 | PRIVATE_KEY=your_private_key 256 | RPC_URL=https://base-sepolia.g.alchemy.com/v2/YOUR_KEY 257 | CHAIN_ID=84532 258 | 259 | # Optional - custom facilitator endpoint 260 | FACILITATOR_URL=https://your-facilitator.com 261 | FACILITATOR_API_KEY=your_key 262 | 263 | # Optional - asset overrides (required if NETWORK isn't base/base-sepolia/polygon/polygon-amoy) 264 | ASSET_ADDRESS=0xTokenAddress 265 | ASSET_NAME=USDC 266 | EXPLORER_URL=https://explorer.your-network.org 267 | 268 | # Optional - ensure payment requirements include a fully-qualified endpoint URL 269 | SERVICE_URL=https://your-domain.com/process 270 | ``` 271 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing Guide 2 | 3 | Complete guide for testing the x402 payment API starter kit. 4 | 5 | ## Test Scripts Available 6 | 7 | ### 1. Basic Health Check 8 | Simple curl test to verify the API is running: 9 | 10 | ```bash 11 | curl http://localhost:3000/health 12 | ``` 13 | 14 | ### 2. Simple Request Test 15 | Test the payment requirement flow: 16 | 17 | ```bash 18 | ./test-request.sh 19 | ``` 20 | 21 | This will: 22 | - Check API health 23 | - Send a request without payment 24 | - Show the 402 Payment Required response 25 | 26 | ### 3. Full Test Client 27 | Comprehensive test with payment signing: 28 | 29 | ```bash 30 | npm test 31 | ``` 32 | 33 | or 34 | 35 | ```bash 36 | ./test-request.sh 37 | ``` 38 | 39 | ## Test Scenarios 40 | 41 | ### Test 1: API Health Check 42 | 43 | **What it tests:** API is running and configured properly 44 | 45 | ```bash 46 | curl http://localhost:3000/health 47 | ``` 48 | 49 | **Expected output:** 50 | ```json 51 | { 52 | "status": "healthy", 53 | "service": "x402-payment-api", 54 | "version": "1.0.0", 55 | "payment": { 56 | "address": "0xYourAddress...", 57 | "network": "base-sepolia", 58 | "price": "$0.10" 59 | } 60 | } 61 | ``` 62 | 63 | ### Test 2: Payment Required Flow 64 | 65 | **What it tests:** API correctly requests payment 66 | 67 | ```bash 68 | curl -X POST http://localhost:3000/process \ 69 | -H "Content-Type: application/json" \ 70 | -d '{ 71 | "message": { 72 | "parts": [{"kind": "text", "text": "Hello!"}] 73 | } 74 | }' 75 | ``` 76 | 77 | **Expected output:** HTTP 402 with payment requirements 78 | ```json 79 | { 80 | "error": "Payment Required", 81 | "x402": { 82 | "x402Version": 1, 83 | "accepts": [{ 84 | "scheme": "eip3009", 85 | "network": "base-sepolia", 86 | "asset": "USDC", 87 | "payTo": "0xYourAddress...", 88 | "maxAmountRequired": "100000", 89 | "resource": "/process-request", 90 | "description": "AI request processing service" 91 | }], 92 | "error": "Payment required for service: /process-request" 93 | } 94 | } 95 | ``` 96 | 97 | ### Test 3: Complete Payment Flow 98 | 99 | **What it tests:** Full payment and request processing 100 | 101 | **Prerequisites:** 102 | - Test wallet with USDC 103 | - Test wallet with gas tokens 104 | - USDC approval set for the facilitator 105 | 106 | **Setup:** 107 | ```bash 108 | # Add to .env 109 | CLIENT_PRIVATE_KEY=your_test_wallet_private_key 110 | ``` 111 | 112 | **Run:** 113 | ```bash 114 | npm test 115 | ``` 116 | 117 | **What happens:** 118 | 1. Client sends request 119 | 2. API returns 402 Payment Required 120 | 3. Client signs payment with wallet 121 | 4. Client submits signed payment 122 | 5. API verifies payment signature 123 | 6. API processes request (calls OpenAI in this example) 124 | 7. API settles payment on blockchain 125 | 8. API returns response 126 | 127 | **Expected output:** 128 | ``` 129 | 🧪 x402 Payment API Test Client 130 | ================================ 131 | 132 | 🏥 Checking API health... 133 | ✅ API is healthy 134 | Service: x402-payment-api 135 | Payment address: 0x... 136 | Network: base-sepolia 137 | Price: $0.10 138 | 139 | 📋 TEST 1: Request without payment 140 | ===================================== 141 | 📤 Sending request: "What is 2+2?" 142 | 💳 Payment required! 143 | ✅ Correctly received payment requirement 144 | 145 | 📋 TEST 2: Request with payment 146 | ===================================== 147 | 💼 Client wallet: 0x... 148 | 149 | === STEP 1: Initial Request === 150 | 📤 Sending request: "Tell me a joke about TypeScript!" 151 | 💳 Payment required! 152 | 153 | === STEP 2: Processing Payment === 154 | Payment options: 1 155 | First option: USDC on base-sepolia 156 | Amount: 100000 (micro units) 157 | 🔐 Signing payment... 158 | ✅ Payment signed successfully 159 | Payment payload created for base-sepolia 160 | 161 | === STEP 3: Submitting Payment === 162 | ✅ Payment accepted and request processed! 163 | 164 | 🎉 SUCCESS! Response from AI: 165 | ----------------------------------- 166 | Why do TypeScript developers prefer dark mode? 167 | Because light attracts bugs! 🐛 168 | ----------------------------------- 169 | 170 | ✅ Tests complete! 171 | ``` 172 | 173 | ## Test Client Configuration 174 | 175 | The test client (`src/testClient.ts`) supports: 176 | 177 | ### Environment Variables 178 | 179 | ```env 180 | # Required for the API 181 | OPENAI_API_KEY=your_openai_api_key 182 | PAY_TO_ADDRESS=0xYourMerchantAddress 183 | NETWORK=base-sepolia 184 | 185 | # Optional for testing with payments 186 | CLIENT_PRIVATE_KEY=your_test_wallet_private_key 187 | API_URL=http://localhost:3000 188 | ``` 189 | 190 | ### Test Client API 191 | 192 | You can also use the test client programmatically: 193 | 194 | ```typescript 195 | import { TestClient } from './testClient.js'; 196 | 197 | const client = new TestClient(privateKey); 198 | 199 | // Check health 200 | await client.checkHealth(); 201 | 202 | // Send request without payment 203 | const response1 = await client.sendRequest('What is 2+2?'); 204 | 205 | // Send request with payment 206 | const response2 = await client.sendPaidRequest('Tell me a joke!'); 207 | ``` 208 | 209 | ## Setting Up for Full Payment Testing 210 | 211 | ### 1. Get a Test Wallet 212 | 213 | Create a new wallet for testing: 214 | 215 | ```typescript 216 | import { Wallet } from 'ethers'; 217 | const wallet = Wallet.createRandom(); 218 | console.log('Address:', wallet.address); 219 | console.log('Private Key:', wallet.privateKey); 220 | ``` 221 | 222 | Or use an existing test wallet. 223 | 224 | ### 2. Get Testnet Tokens 225 | 226 | For Base Sepolia: 227 | 228 | **Get testnet ETH:** 229 | - https://www.alchemy.com/faucets/base-sepolia 230 | - https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet 231 | 232 | **Get testnet USDC:** 233 | - Swap testnet ETH for USDC on Uniswap testnet 234 | - Or use a testnet USDC faucet 235 | 236 | ### 3. Set USDC Approval 237 | 238 | Your test wallet needs to approve the facilitator to spend USDC: 239 | 240 | ```typescript 241 | import { ethers } from 'ethers'; 242 | 243 | const provider = new ethers.JsonRpcProvider('https://sepolia.base.org'); 244 | const wallet = new ethers.Wallet(privateKey, provider); 245 | 246 | // USDC contract on Base Sepolia 247 | const usdcAddress = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; 248 | const facilitatorAddress = '0x...'; // Get from facilitator docs 249 | 250 | const usdc = new ethers.Contract( 251 | usdcAddress, 252 | ['function approve(address spender, uint256 amount) returns (bool)'], 253 | wallet 254 | ); 255 | 256 | // Approve facilitator to spend USDC (approve large amount for testing) 257 | const tx = await usdc.approve(facilitatorAddress, ethers.parseUnits('1000', 6)); 258 | await tx.wait(); 259 | console.log('Approval granted!'); 260 | ``` 261 | 262 | ### 4. Configure Test Client 263 | 264 | Add to `.env`: 265 | 266 | ```env 267 | CLIENT_PRIVATE_KEY=0x1234...your_test_wallet_private_key 268 | ``` 269 | 270 | ### 5. Run Tests 271 | 272 | ```bash 273 | npm test 274 | ``` 275 | 276 | ## Troubleshooting Tests 277 | 278 | ### "API is not running" 279 | 280 | Start the API first: 281 | ```bash 282 | npm start 283 | ``` 284 | 285 | In another terminal, run tests: 286 | ```bash 287 | npm test 288 | ``` 289 | 290 | ### "CLIENT_PRIVATE_KEY not configured" 291 | 292 | Test 2 (paid requests) will be skipped without a client wallet. This is expected. 293 | 294 | To test with payments, add `CLIENT_PRIVATE_KEY` to `.env`. 295 | 296 | ### "Payment verification failed" 297 | 298 | Check: 299 | - Wallet has USDC tokens 300 | - Wallet has gas tokens (ETH) 301 | - USDC approval is set for the facilitator 302 | - Network matches (testnet vs mainnet) 303 | 304 | ### "OpenAI API error" 305 | 306 | Check: 307 | - `OPENAI_API_KEY` is valid 308 | - OpenAI account has credits 309 | - Not hitting rate limits 310 | 311 | ### "Network mismatch" 312 | 313 | Ensure: 314 | - API's `NETWORK` setting matches your test wallet's network 315 | - Client wallet is funded on the correct network 316 | - USDC contract address matches the network 317 | 318 | ## CI/CD Testing 319 | 320 | For automated testing without payments: 321 | 322 | ```yaml 323 | # .github/workflows/test.yml 324 | name: Test Payment API 325 | 326 | on: [push, pull_request] 327 | 328 | jobs: 329 | test: 330 | runs-on: ubuntu-latest 331 | steps: 332 | - uses: actions/checkout@v2 333 | - uses: actions/setup-node@v2 334 | with: 335 | node-version: '18' 336 | 337 | - name: Install dependencies 338 | run: npm install 339 | 340 | - name: Build 341 | run: npm run build 342 | 343 | - name: Start API 344 | run: npm start & 345 | env: 346 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 347 | PAY_TO_ADDRESS: "0x0000000000000000000000000000000000000000" 348 | 349 | - name: Wait for API 350 | run: sleep 5 351 | 352 | - name: Test health endpoint 353 | run: curl -f http://localhost:3000/health 354 | 355 | - name: Test payment requirement 356 | run: | 357 | curl -X POST http://localhost:3000/process \ 358 | -H "Content-Type: application/json" \ 359 | -d '{"message":{"parts":[{"kind":"text","text":"test"}]}}' \ 360 | | grep -q "Payment Required" 361 | ``` 362 | 363 | ## Manual Testing Checklist 364 | 365 | - [ ] API starts without errors 366 | - [ ] Health endpoint returns 200 OK 367 | - [ ] Request without payment returns 402 368 | - [ ] Payment requirements include correct network 369 | - [ ] Payment requirements include correct amount ($0.10 = 100000 micro USDC) 370 | - [ ] Test client can sign payment 371 | - [ ] API accepts signed payment 372 | - [ ] API verifies payment signature locally 373 | - [ ] API processes request (calls service) 374 | - [ ] API settles payment on blockchain 375 | - [ ] API returns service response 376 | - [ ] Response includes transaction hash 377 | - [ ] USDC transferred to merchant wallet 378 | 379 | ## Performance Testing 380 | 381 | Test API under load: 382 | 383 | ```bash 384 | # Install apache bench 385 | brew install ab # macOS 386 | apt-get install apache2-utils # Linux 387 | 388 | # Test 100 requests, 10 concurrent 389 | ab -n 100 -c 10 -p request.json -T application/json http://localhost:3000/process 390 | ``` 391 | 392 | Create `request.json`: 393 | ```json 394 | {"message":{"parts":[{"kind":"text","text":"test"}]}} 395 | ``` 396 | 397 | ## Security Testing 398 | 399 | - [ ] API rejects requests without payment 400 | - [ ] API validates payment signatures 401 | - [ ] API checks payment amounts 402 | - [ ] API verifies network matches 403 | - [ ] API prevents replay attacks (nonce checking) 404 | - [ ] Private keys never logged or exposed 405 | - [ ] HTTPS in production 406 | - [ ] Rate limiting implemented 407 | 408 | ## Next Steps 409 | 410 | After successful testing: 411 | 412 | 1. Deploy to staging environment 413 | 2. Test with real testnet USDC 414 | 3. Monitor facilitator responses 415 | 4. Check blockchain transactions 416 | 5. Verify merchant receives payments 417 | 6. Deploy to production 418 | 7. Monitor production metrics 419 | 420 | ## Support 421 | 422 | If tests fail: 423 | - Check the [README.md](./README.md) 424 | - Review [RPC_CONFIGURATION.md](./RPC_CONFIGURATION.md) 425 | - Check API logs 426 | - Verify environment variables 427 | - Test blockchain connectivity 428 | -------------------------------------------------------------------------------- /check-wallet.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; 7 | const PAY_TO_ADDRESS = process.env.PAY_TO_ADDRESS; 8 | const NETWORK = process.env.NETWORK || 'base-sepolia'; 9 | 10 | // Network configurations 11 | const networks = { 12 | 'base-sepolia': { 13 | rpc: 'https://sepolia.base.org', 14 | usdc: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', 15 | chainId: 84532, 16 | name: 'Base Sepolia', 17 | explorer: 'https://sepolia.basescan.org', 18 | faucets: [ 19 | 'https://www.alchemy.com/faucets/base-sepolia', 20 | 'https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet', 21 | ], 22 | }, 23 | 'base': { 24 | rpc: 'https://mainnet.base.org', 25 | usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 26 | chainId: 8453, 27 | name: 'Base Mainnet', 28 | explorer: 'https://basescan.org', 29 | }, 30 | }; 31 | 32 | async function checkWallet() { 33 | console.log('🔍 Wallet Balance Checker'); 34 | console.log('========================\n'); 35 | 36 | if (!CLIENT_PRIVATE_KEY) { 37 | console.log('❌ CLIENT_PRIVATE_KEY not set in .env'); 38 | console.log(' This is the wallet that will pay for requests'); 39 | return; 40 | } 41 | 42 | const networkConfig = networks[NETWORK]; 43 | if (!networkConfig) { 44 | console.log(`❌ Unknown network: ${NETWORK}`); 45 | return; 46 | } 47 | 48 | console.log(`📡 Network: ${networkConfig.name}`); 49 | console.log(`🔗 RPC: ${networkConfig.rpc}\n`); 50 | 51 | try { 52 | const provider = new ethers.JsonRpcProvider(networkConfig.rpc); 53 | const clientWallet = new ethers.Wallet(CLIENT_PRIVATE_KEY, provider); 54 | 55 | console.log(`💼 Client Wallet: ${clientWallet.address}`); 56 | console.log(`💰 Merchant Wallet: ${PAY_TO_ADDRESS}\n`); 57 | 58 | // Check ETH balance 59 | console.log('=== Client Wallet Balances ==='); 60 | const ethBalance = await provider.getBalance(clientWallet.address); 61 | const ethFormatted = ethers.formatEther(ethBalance); 62 | console.log(`ETH: ${ethFormatted}`); 63 | 64 | if (parseFloat(ethFormatted) === 0) { 65 | console.log('⚠️ No ETH for gas! Get testnet ETH from:'); 66 | networkConfig.faucets?.forEach(f => console.log(` - ${f}`)); 67 | } else { 68 | console.log('✅ ETH balance looks good'); 69 | } 70 | 71 | // Check USDC balance 72 | const usdcContract = new ethers.Contract( 73 | networkConfig.usdc, 74 | [ 75 | 'function balanceOf(address) view returns (uint256)', 76 | 'function decimals() view returns (uint8)', 77 | 'function symbol() view returns (string)', 78 | ], 79 | provider 80 | ); 81 | 82 | const usdcBalance = await usdcContract.balanceOf(clientWallet.address); 83 | const decimals = await usdcContract.decimals(); 84 | const usdcFormatted = ethers.formatUnits(usdcBalance, decimals); 85 | console.log(`USDC: ${usdcFormatted}`); 86 | 87 | if (parseFloat(usdcFormatted) === 0) { 88 | console.log('⚠️ No USDC! You need at least $0.10 USDC to test'); 89 | if (networkConfig.faucets) { 90 | console.log(' Get USDC by:'); 91 | console.log(' 1. Get ETH from faucets above'); 92 | console.log(' 2. Swap ETH for USDC on a testnet DEX'); 93 | } 94 | } else { 95 | const canPay = parseFloat(usdcFormatted) >= 0.10; 96 | if (canPay) { 97 | const numTests = Math.floor(parseFloat(usdcFormatted) / 0.10); 98 | console.log(`✅ USDC balance sufficient! Can run ~${numTests} tests`); 99 | } else { 100 | console.log('⚠️ USDC balance too low (need at least $0.10)'); 101 | } 102 | } 103 | 104 | // Check merchant wallet 105 | console.log('\n=== Merchant Wallet Balances ==='); 106 | const merchantEthBalance = await provider.getBalance(PAY_TO_ADDRESS); 107 | const merchantEthFormatted = ethers.formatEther(merchantEthBalance); 108 | console.log(`ETH: ${merchantEthFormatted}`); 109 | 110 | const merchantUsdcBalance = await usdcContract.balanceOf(PAY_TO_ADDRESS); 111 | const merchantUsdcFormatted = ethers.formatUnits(merchantUsdcBalance, decimals); 112 | console.log(`USDC: ${merchantUsdcFormatted}`); 113 | 114 | console.log('\n=== Next Steps ==='); 115 | if (parseFloat(ethFormatted) > 0 && parseFloat(usdcFormatted) >= 0.10) { 116 | console.log('✅ Your wallet is ready to test!'); 117 | console.log('\nRun the test:'); 118 | console.log(' npm test'); 119 | console.log('\nMonitor transactions:'); 120 | console.log(` ${networkConfig.explorer}/address/${clientWallet.address}`); 121 | console.log(` ${networkConfig.explorer}/address/${PAY_TO_ADDRESS}`); 122 | } else { 123 | console.log('📝 To test with real payments, you need:'); 124 | console.log(`1. ETH for gas on ${networkConfig.name}`); 125 | console.log('2. At least $0.10 USDC'); 126 | console.log('\nWithout these, the test will only demonstrate the payment signing,'); 127 | console.log('but the server will skip submitting the on-chain transfer.'); 128 | } 129 | 130 | } catch (error) { 131 | console.error('\n❌ Error:', error.message); 132 | } 133 | } 134 | 135 | checkWallet().catch(console.error); 136 | -------------------------------------------------------------------------------- /header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/x402-starter-kit/a2e6ba89231f9e2164695afa84b98df896f26398/header.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x402-developer-starter-kit", 3 | "version": "1.0.0", 4 | "description": "Starter kit for building paid APIs using the x402 payment protocol", 5 | "main": "dist/server.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/server.js", 10 | "dev": "tsc && node dist/server.js", 11 | "test": "npm run build && node dist/testClient.js", 12 | "test:shell": "bash test-request.sh", 13 | "clean": "rm -rf dist" 14 | }, 15 | "keywords": [ 16 | "x402", 17 | "payment-api", 18 | "payments", 19 | "usdc", 20 | "eip-3009" 21 | ], 22 | "author": "", 23 | "license": "MIT", 24 | "dependencies": { 25 | "x402": "^0.7.0", 26 | "dotenv": "^16.4.5", 27 | "ethers": "^6.15.0", 28 | "express": "^4.21.2", 29 | "openai": "^4.78.0", 30 | "x402": "^0.7.0" 31 | }, 32 | "devDependencies": { 33 | "@types/express": "^4.17.21", 34 | "@types/node": "^24.6.2", 35 | "typescript": "^5.9.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # x402 AI Agent Setup Script 4 | 5 | set -e 6 | 7 | echo "🚀 Setting up x402 AI Agent..." 8 | echo "" 9 | 10 | # Check if we're in the agent directory 11 | if [ ! -f "package.json" ]; then 12 | echo "❌ Error: Please run this script from the agent directory" 13 | exit 1 14 | fi 15 | 16 | # Step 1: Install agent dependencies 17 | echo "📦 Step 1: Installing agent dependencies..." 18 | npm install 19 | 20 | # Step 2: Check for .env file 21 | echo "" 22 | if [ ! -f ".env" ]; then 23 | echo "⚠️ Step 2: No .env file found" 24 | echo " Creating .env from .env.example..." 25 | cp .env.example .env 26 | echo "" 27 | echo "📝 Please edit the .env file and add your configuration:" 28 | echo " - OPENAI_API_KEY" 29 | echo " - PAY_TO_ADDRESS" 30 | echo " - PRIVATE_KEY (optional)" 31 | echo " - NETWORK (default: base-sepolia)" 32 | echo "" 33 | else 34 | echo "✅ Step 2: .env file already exists" 35 | fi 36 | 37 | # Step 3: Build the agent 38 | echo "" 39 | echo "🔨 Step 3: Building agent..." 40 | npm run build 41 | 42 | echo "" 43 | echo "✅ Setup complete!" 44 | echo "" 45 | echo "Next steps:" 46 | echo "1. Edit .env file with your configuration" 47 | echo "2. Run 'npm start' to start the agent" 48 | echo "3. Visit http://localhost:3000/health to check status" 49 | echo "" 50 | -------------------------------------------------------------------------------- /src/ExampleService.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { EventQueue, RequestContext, TaskState } from './x402Types.js'; 3 | 4 | type AiProvider = 'openai' | 'eigenai'; 5 | 6 | interface ExampleServiceOptions { 7 | apiKey?: string; 8 | baseUrl?: string; 9 | defaultHeaders?: Record; 10 | provider: AiProvider; 11 | payToAddress: string; 12 | network: string; 13 | model?: string; 14 | temperature?: number; 15 | maxTokens?: number; 16 | seed?: number; 17 | } 18 | 19 | /** 20 | * ExampleService - A sample service implementation using an OpenAI-compatible API 21 | * 22 | * This is a demonstration of how to process paid requests. 23 | * Replace this with your own service logic (database queries, computations, API calls, etc.) 24 | * 25 | * Payment validation is handled by the server before this service is invoked. 26 | */ 27 | export class ExampleService { 28 | private openai: OpenAI; 29 | private payToAddress: string; 30 | private network: string; 31 | private readonly model: string; 32 | private readonly temperature: number; 33 | private readonly maxTokens?: number; 34 | private readonly seed?: number; 35 | private readonly provider: AiProvider; 36 | 37 | constructor({ 38 | apiKey, 39 | baseUrl, 40 | defaultHeaders, 41 | provider, 42 | payToAddress, 43 | network, 44 | model, 45 | temperature = 0.7, 46 | maxTokens = 500, 47 | seed, 48 | }: ExampleServiceOptions) { 49 | const clientOptions: ConstructorParameters[0] = {}; 50 | 51 | if (provider === 'openai') { 52 | if (!apiKey) { 53 | throw new Error('OPENAI_API_KEY is required when using the OpenAI provider'); 54 | } 55 | clientOptions.apiKey = apiKey; 56 | } else if (provider === 'eigenai') { 57 | if (!defaultHeaders?.['x-api-key']) { 58 | throw new Error('EIGENAI_API_KEY is required when using the EigenAI provider'); 59 | } 60 | } 61 | 62 | if (baseUrl) { 63 | clientOptions.baseURL = baseUrl; 64 | } 65 | 66 | if (defaultHeaders && Object.keys(defaultHeaders).length > 0) { 67 | clientOptions.defaultHeaders = defaultHeaders; 68 | } 69 | 70 | this.openai = new OpenAI(clientOptions); 71 | this.payToAddress = payToAddress; 72 | this.network = network; 73 | this.model = model ?? (provider === 'eigenai' ? 'gpt-oss-120b-f16' : 'gpt-4o-mini'); 74 | this.temperature = temperature; 75 | this.maxTokens = maxTokens; 76 | this.seed = seed; 77 | this.provider = provider; 78 | } 79 | 80 | async execute(context: RequestContext, eventQueue: EventQueue): Promise { 81 | const task = context.currentTask; 82 | 83 | if (!task) { 84 | throw new Error('No task found in context'); 85 | } 86 | console.log('✅ Payment verified, processing request...'); 87 | 88 | // Extract user message from the context 89 | const userMessage = context.message?.parts 90 | ?.filter((part: any) => part.kind === 'text') 91 | .map((part: any) => part.text) 92 | .join(' ') || 'Hello'; 93 | 94 | console.log(`📝 User request: ${userMessage}`); 95 | 96 | try { 97 | // Call OpenAI API to process the request 98 | // REPLACE THIS with your own service logic 99 | const completion = await this.openai.chat.completions.create({ 100 | model: this.model, 101 | messages: [ 102 | { 103 | role: 'system', 104 | content: 'You are a helpful AI assistant. Provide concise and accurate responses.', 105 | }, 106 | { 107 | role: 'user', 108 | content: userMessage, 109 | }, 110 | ], 111 | temperature: this.temperature, 112 | max_tokens: this.maxTokens, 113 | ...(this.provider === 'eigenai' && this.seed !== undefined 114 | ? { seed: this.seed } 115 | : {}), 116 | }); 117 | 118 | const response = completion.choices[0]?.message?.content || 'No response generated'; 119 | 120 | console.log(`🤖 Service response: ${response}`); 121 | 122 | // Update task with the response 123 | task.status.state = TaskState.COMPLETED; 124 | task.status.message = { 125 | messageId: `msg-${Date.now()}`, 126 | role: 'agent', 127 | parts: [ 128 | { 129 | kind: 'text', 130 | text: response, 131 | }, 132 | ], 133 | }; 134 | 135 | // Enqueue the completed task 136 | await eventQueue.enqueueEvent(task); 137 | 138 | console.log('✨ Request processed successfully'); 139 | } catch (error) { 140 | console.error('❌ Error processing request:', error); 141 | 142 | // Update task with error 143 | task.status.state = TaskState.FAILED; 144 | task.status.message = { 145 | messageId: `msg-${Date.now()}`, 146 | role: 'agent', 147 | parts: [ 148 | { 149 | kind: 'text', 150 | text: `Error processing request: ${error instanceof Error ? error.message : 'Unknown error'}`, 151 | }, 152 | ], 153 | }; 154 | 155 | await eventQueue.enqueueEvent(task); 156 | throw error; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/MerchantExecutor.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Network, 3 | PaymentPayload, 4 | PaymentRequirements, 5 | } from 'x402/types'; 6 | import { ethers } from 'ethers'; 7 | 8 | const DEFAULT_FACILITATOR_URL = 'https://x402.org/facilitator'; 9 | 10 | const TRANSFER_AUTH_TYPES = { 11 | TransferWithAuthorization: [ 12 | { name: 'from', type: 'address' }, 13 | { name: 'to', type: 'address' }, 14 | { name: 'value', type: 'uint256' }, 15 | { name: 'validAfter', type: 'uint256' }, 16 | { name: 'validBefore', type: 'uint256' }, 17 | { name: 'nonce', type: 'bytes32' }, 18 | ], 19 | }; 20 | 21 | export type SettlementMode = 'facilitator' | 'direct'; 22 | 23 | type BuiltInNetwork = 24 | | 'base' 25 | | 'base-sepolia' 26 | | 'polygon' 27 | | 'polygon-amoy' 28 | | 'avalanche-fuji' 29 | | 'avalanche' 30 | | 'iotex' 31 | | 'sei' 32 | | 'sei-testnet' 33 | | 'peaq' 34 | | 'solana-devnet' 35 | | 'solana'; 36 | 37 | const BUILT_IN_NETWORKS: Record< 38 | BuiltInNetwork, 39 | { 40 | chainId?: number; 41 | assetAddress: string; 42 | assetName: string; 43 | explorer?: string; 44 | } 45 | > = { 46 | base: { 47 | chainId: 8453, 48 | assetAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 49 | assetName: 'USD Coin', 50 | explorer: 'https://basescan.org', 51 | }, 52 | 'base-sepolia': { 53 | chainId: 84532, 54 | assetAddress: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', 55 | assetName: 'USDC', 56 | explorer: 'https://sepolia.basescan.org', 57 | }, 58 | polygon: { 59 | chainId: 137, 60 | assetAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', 61 | assetName: 'USD Coin', 62 | explorer: 'https://polygonscan.com', 63 | }, 64 | 'polygon-amoy': { 65 | chainId: 80002, 66 | assetAddress: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582', 67 | assetName: 'USDC', 68 | explorer: 'https://amoy.polygonscan.com', 69 | }, 70 | 'avalanche-fuji': { 71 | chainId: 43113, 72 | assetAddress: '0x5425890298aed601595a70AB815c96711a31Bc65', 73 | assetName: 'USD Coin', 74 | explorer: 'https://testnet.snowtrace.io', 75 | }, 76 | avalanche: { 77 | chainId: 43114, 78 | assetAddress: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', 79 | assetName: 'USD Coin', 80 | explorer: 'https://snowtrace.io', 81 | }, 82 | iotex: { 83 | chainId: 4689, 84 | assetAddress: '0xcdf79194c6c285077a58da47641d4dbe51f63542', 85 | assetName: 'Bridged USDC', 86 | explorer: 'https://iotexscan.io', 87 | }, 88 | sei: { 89 | chainId: 1329, 90 | assetAddress: '0xe15fc38f6d8c56af07bbcbe3baf5708a2bf42392', 91 | assetName: 'USDC', 92 | explorer: 'https://sei.explorers.guru', 93 | }, 94 | 'sei-testnet': { 95 | chainId: 1328, 96 | assetAddress: '0x4fcf1784b31630811181f670aea7a7bef803eaed', 97 | assetName: 'USDC', 98 | explorer: 'https://testnet.sei.explorers.guru', 99 | }, 100 | peaq: { 101 | chainId: 3338, 102 | assetAddress: '0xbbA60da06c2c5424f03f7434542280FCAd453d10', 103 | assetName: 'USDC', 104 | explorer: 'https://scan.peaq.network', 105 | }, 106 | 'solana-devnet': { 107 | assetAddress: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', 108 | assetName: 'USDC', 109 | explorer: 'https://explorer.solana.com/?cluster=devnet', 110 | }, 111 | solana: { 112 | assetAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 113 | assetName: 'USDC', 114 | explorer: 'https://explorer.solana.com', 115 | }, 116 | }; 117 | 118 | export interface MerchantExecutorOptions { 119 | payToAddress: string; 120 | network: Network; 121 | price: number; 122 | facilitatorUrl?: string; 123 | facilitatorApiKey?: string; 124 | resourceUrl?: string; 125 | settlementMode?: SettlementMode; 126 | rpcUrl?: string; 127 | privateKey?: string; 128 | assetAddress?: string; 129 | assetName?: string; 130 | explorerUrl?: string; 131 | chainId?: number; 132 | } 133 | 134 | export interface VerifyResult { 135 | isValid: boolean; 136 | payer?: string; 137 | invalidReason?: string; 138 | } 139 | 140 | export interface SettlementResult { 141 | success: boolean; 142 | transaction?: string; 143 | network: string; 144 | payer?: string; 145 | errorReason?: string; 146 | } 147 | 148 | export class MerchantExecutor { 149 | private readonly requirements: PaymentRequirements; 150 | private readonly explorerUrl?: string; 151 | private readonly mode: SettlementMode; 152 | private readonly facilitatorUrl?: string; 153 | private readonly facilitatorApiKey?: string; 154 | private settlementProvider?: ethers.JsonRpcProvider; 155 | private settlementWallet?: ethers.Wallet; 156 | private readonly network: Network; 157 | private readonly assetName: string; 158 | private readonly chainId?: number; 159 | 160 | constructor(options: MerchantExecutorOptions) { 161 | const builtinConfig = BUILT_IN_NETWORKS[ 162 | options.network as BuiltInNetwork 163 | ] as (typeof BUILT_IN_NETWORKS)[BuiltInNetwork] | undefined; 164 | 165 | const assetAddress = 166 | options.assetAddress ?? builtinConfig?.assetAddress; 167 | const assetName = options.assetName ?? builtinConfig?.assetName; 168 | const chainId = options.chainId ?? builtinConfig?.chainId; 169 | const explorerUrl = options.explorerUrl ?? builtinConfig?.explorer; 170 | 171 | if (!assetAddress) { 172 | throw new Error( 173 | `Asset address must be provided for network "${options.network}". Set ASSET_ADDRESS in the environment.` 174 | ); 175 | } 176 | 177 | if (!assetName) { 178 | throw new Error( 179 | `Asset name must be provided for network "${options.network}". Set ASSET_NAME in the environment.` 180 | ); 181 | } 182 | 183 | this.network = options.network; 184 | this.assetName = assetName; 185 | this.chainId = chainId; 186 | this.explorerUrl = explorerUrl; 187 | 188 | this.requirements = { 189 | scheme: 'exact', 190 | network: options.network, 191 | asset: assetAddress, 192 | payTo: options.payToAddress, 193 | maxAmountRequired: this.getAtomicAmount(options.price), 194 | resource: options.resourceUrl || 'https://merchant.local/process', 195 | description: 'AI request processing service', 196 | mimeType: 'application/json', 197 | maxTimeoutSeconds: 600, 198 | extra: { 199 | name: assetName, 200 | version: '2', 201 | }, 202 | }; 203 | 204 | this.mode = 205 | options.settlementMode ?? 206 | (options.facilitatorUrl || !options.privateKey ? 'facilitator' : 'direct'); 207 | 208 | if (this.mode === 'direct') { 209 | if (options.network === 'solana' || options.network === 'solana-devnet') { 210 | throw new Error( 211 | 'Direct settlement is only supported on EVM networks.' 212 | ); 213 | } 214 | 215 | if (!options.privateKey) { 216 | throw new Error( 217 | 'Direct settlement requires PRIVATE_KEY to be configured.' 218 | ); 219 | } 220 | 221 | const normalizedKey = options.privateKey.startsWith('0x') 222 | ? options.privateKey 223 | : `0x${options.privateKey}`; 224 | 225 | const rpcUrl = options.rpcUrl || this.getDefaultRpcUrl(options.network); 226 | 227 | if (!rpcUrl) { 228 | throw new Error( 229 | `Direct settlement requires an RPC URL for network "${options.network}".` 230 | ); 231 | } 232 | 233 | if (typeof chainId !== 'number') { 234 | throw new Error( 235 | `Direct settlement requires a numeric CHAIN_ID for network "${options.network}".` 236 | ); 237 | } 238 | 239 | try { 240 | this.settlementProvider = new ethers.JsonRpcProvider(rpcUrl); 241 | this.settlementWallet = new ethers.Wallet( 242 | normalizedKey, 243 | this.settlementProvider 244 | ); 245 | console.log('⚡ Local settlement enabled via RPC provider'); 246 | } catch (error) { 247 | throw new Error( 248 | `Failed to initialize direct settlement: ${ 249 | error instanceof Error ? error.message : String(error) 250 | }` 251 | ); 252 | } 253 | } else { 254 | this.facilitatorUrl = options.facilitatorUrl || DEFAULT_FACILITATOR_URL; 255 | this.facilitatorApiKey = options.facilitatorApiKey; 256 | } 257 | } 258 | 259 | getPaymentRequirements(): PaymentRequirements { 260 | return this.requirements; 261 | } 262 | 263 | createPaymentRequiredResponse() { 264 | return { 265 | x402Version: 1, 266 | accepts: [this.requirements], 267 | error: 'Payment required for service: /process-request', 268 | }; 269 | } 270 | 271 | async verifyPayment(payload: PaymentPayload): Promise { 272 | console.log('\n🔍 Verifying payment...'); 273 | console.log(` Network: ${payload.network}`); 274 | console.log(` Scheme: ${payload.scheme}`); 275 | console.log(` From: ${(payload.payload as any).authorization?.from}`); 276 | console.log(` To: ${this.requirements.payTo}`); 277 | console.log(` Amount: ${this.requirements.maxAmountRequired}`); 278 | 279 | try { 280 | const result = 281 | this.mode === 'direct' 282 | ? this.verifyPaymentLocally(payload, this.requirements) 283 | : await this.callFacilitator('verify', payload); 284 | 285 | console.log('\n📋 Verification result:'); 286 | console.log(` Valid: ${result.isValid}`); 287 | if (!result.isValid) { 288 | console.log(` ❌ Reason: ${result.invalidReason}`); 289 | } 290 | 291 | return result; 292 | } catch (error) { 293 | const message = 294 | error instanceof Error ? error.message : 'Unknown verification error'; 295 | console.error(` ❌ Verification failed: ${message}`); 296 | return { 297 | isValid: false, 298 | invalidReason: message, 299 | }; 300 | } 301 | } 302 | 303 | async settlePayment(payload: PaymentPayload): Promise { 304 | console.log('\n💰 Settling payment...'); 305 | console.log(` Network: ${this.requirements.network}`); 306 | console.log( 307 | ` Amount: ${this.requirements.maxAmountRequired} (micro units)` 308 | ); 309 | console.log(` Pay to: ${this.requirements.payTo}`); 310 | 311 | try { 312 | const result = 313 | this.mode === 'direct' 314 | ? await this.settleOnChain(payload, this.requirements) 315 | : await this.callFacilitator('settle', payload); 316 | 317 | console.log('\n✅ Payment settlement result:'); 318 | console.log(` Success: ${result.success}`); 319 | console.log(` Network: ${result.network}`); 320 | if (result.transaction) { 321 | console.log(` Transaction: ${result.transaction}`); 322 | if (this.explorerUrl) { 323 | console.log( 324 | ` Explorer: ${this.explorerUrl}/tx/${result.transaction}` 325 | ); 326 | } 327 | } 328 | if (result.payer) { 329 | console.log(` Payer: ${result.payer}`); 330 | } 331 | if (result.errorReason) { 332 | console.log(` Error: ${result.errorReason}`); 333 | } 334 | 335 | return result; 336 | } catch (error) { 337 | const message = 338 | error instanceof Error ? error.message : 'Unknown settlement error'; 339 | console.error(` ❌ Settlement failed: ${message}`); 340 | return { 341 | success: false, 342 | network: this.requirements.network, 343 | errorReason: message, 344 | }; 345 | } 346 | } 347 | 348 | private verifyPaymentLocally( 349 | payload: PaymentPayload, 350 | requirements: PaymentRequirements 351 | ): VerifyResult { 352 | const exactPayload = payload.payload as any; 353 | const authorization = exactPayload?.authorization; 354 | const signature = exactPayload?.signature; 355 | 356 | if (!authorization || !signature) { 357 | return { 358 | isValid: false, 359 | invalidReason: 'Missing payment authorization data', 360 | }; 361 | } 362 | 363 | if (payload.network !== requirements.network) { 364 | return { 365 | isValid: false, 366 | invalidReason: `Network mismatch: ${payload.network} vs ${requirements.network}`, 367 | }; 368 | } 369 | 370 | if ( 371 | authorization.to?.toLowerCase() !== requirements.payTo.toLowerCase() 372 | ) { 373 | return { 374 | isValid: false, 375 | invalidReason: 'Authorization recipient does not match payment requirement', 376 | }; 377 | } 378 | 379 | try { 380 | const requiredAmount = BigInt(requirements.maxAmountRequired); 381 | const authorizedAmount = BigInt(authorization.value); 382 | if (authorizedAmount < requiredAmount) { 383 | return { 384 | isValid: false, 385 | invalidReason: 'Authorized amount is less than required amount', 386 | }; 387 | } 388 | } catch { 389 | return { 390 | isValid: false, 391 | invalidReason: 'Invalid payment amount provided', 392 | }; 393 | } 394 | 395 | const validAfterNum = Number(authorization.validAfter ?? 0); 396 | const validBeforeNum = Number(authorization.validBefore ?? 0); 397 | if (Number.isNaN(validAfterNum) || Number.isNaN(validBeforeNum)) { 398 | return { 399 | isValid: false, 400 | invalidReason: 'Invalid authorization timing fields', 401 | }; 402 | } 403 | 404 | const now = Math.floor(Date.now() / 1000); 405 | if (validAfterNum > now) { 406 | return { 407 | isValid: false, 408 | invalidReason: 'Payment authorization is not yet valid', 409 | }; 410 | } 411 | if (validBeforeNum <= now) { 412 | return { 413 | isValid: false, 414 | invalidReason: 'Payment authorization has expired', 415 | }; 416 | } 417 | 418 | try { 419 | const domain = this.buildEip712Domain(requirements); 420 | const recovered = ethers.verifyTypedData( 421 | domain, 422 | TRANSFER_AUTH_TYPES, 423 | { 424 | from: authorization.from, 425 | to: authorization.to, 426 | value: authorization.value, 427 | validAfter: authorization.validAfter, 428 | validBefore: authorization.validBefore, 429 | nonce: authorization.nonce, 430 | }, 431 | signature 432 | ); 433 | 434 | if (recovered.toLowerCase() !== authorization.from.toLowerCase()) { 435 | return { 436 | isValid: false, 437 | invalidReason: 'Signature does not match payer address', 438 | }; 439 | } 440 | 441 | return { 442 | isValid: true, 443 | payer: recovered, 444 | }; 445 | } catch (error) { 446 | return { 447 | isValid: false, 448 | invalidReason: `Signature verification failed: ${ 449 | error instanceof Error ? error.message : String(error) 450 | }`, 451 | }; 452 | } 453 | } 454 | 455 | private async settleOnChain( 456 | payload: PaymentPayload, 457 | requirements: PaymentRequirements 458 | ): Promise { 459 | if (!this.settlementWallet) { 460 | return { 461 | success: false, 462 | network: requirements.network, 463 | errorReason: 'Settlement wallet not configured', 464 | }; 465 | } 466 | 467 | const exactPayload = payload.payload as any; 468 | const authorization = exactPayload?.authorization; 469 | const signature = exactPayload?.signature; 470 | 471 | if (!authorization || !signature) { 472 | return { 473 | success: false, 474 | network: requirements.network, 475 | errorReason: 'Missing payment authorization data', 476 | }; 477 | } 478 | 479 | try { 480 | const usdcContract = new ethers.Contract( 481 | requirements.asset, 482 | [ 483 | 'function transferWithAuthorization(' + 484 | 'address from,' + 485 | 'address to,' + 486 | 'uint256 value,' + 487 | 'uint256 validAfter,' + 488 | 'uint256 validBefore,' + 489 | 'bytes32 nonce,' + 490 | 'uint8 v,' + 491 | 'bytes32 r,' + 492 | 'bytes32 s' + 493 | ') external returns (bool)', 494 | ], 495 | this.settlementWallet 496 | ); 497 | 498 | const parsedSignature = ethers.Signature.from(signature); 499 | const tx = await usdcContract.transferWithAuthorization( 500 | authorization.from, 501 | authorization.to, 502 | authorization.value, 503 | authorization.validAfter, 504 | authorization.validBefore, 505 | authorization.nonce, 506 | parsedSignature.v, 507 | parsedSignature.r, 508 | parsedSignature.s 509 | ); 510 | 511 | const receipt = await tx.wait(); 512 | const success = receipt?.status === 1; 513 | 514 | return { 515 | success, 516 | transaction: receipt?.hash, 517 | network: requirements.network, 518 | payer: authorization.from, 519 | errorReason: success ? undefined : 'Transaction reverted', 520 | }; 521 | } catch (error) { 522 | return { 523 | success: false, 524 | network: requirements.network, 525 | payer: authorization.from, 526 | errorReason: 527 | error instanceof Error ? error.message : String(error), 528 | }; 529 | } 530 | } 531 | 532 | private getAtomicAmount(priceUsd: number): string { 533 | const atomicUnits = Math.floor(priceUsd * 1_000_000); 534 | return atomicUnits.toString(); 535 | } 536 | 537 | private buildHeaders() { 538 | const headers: Record = { 539 | 'Content-Type': 'application/json', 540 | }; 541 | 542 | if (this.facilitatorApiKey) { 543 | headers.Authorization = `Bearer ${this.facilitatorApiKey}`; 544 | } 545 | 546 | return headers; 547 | } 548 | 549 | private async callFacilitator( 550 | endpoint: 'verify' | 'settle', 551 | payload: PaymentPayload 552 | ): Promise { 553 | if (!this.facilitatorUrl) { 554 | throw new Error('Facilitator URL is not configured.'); 555 | } 556 | 557 | const response = await fetch(`${this.facilitatorUrl}/${endpoint}`, { 558 | method: 'POST', 559 | headers: this.buildHeaders(), 560 | body: JSON.stringify({ 561 | x402Version: payload.x402Version ?? 1, 562 | paymentPayload: payload, 563 | paymentRequirements: this.requirements, 564 | }), 565 | }); 566 | 567 | if (!response.ok) { 568 | const text = await response.text(); 569 | throw new Error( 570 | `Facilitator ${endpoint} failed (${response.status}): ${ 571 | text || response.statusText 572 | }` 573 | ); 574 | } 575 | 576 | return (await response.json()) as T; 577 | } 578 | 579 | private buildEip712Domain(requirements: PaymentRequirements) { 580 | return { 581 | name: requirements.extra?.name || this.assetName, 582 | version: requirements.extra?.version || '2', 583 | chainId: this.chainId, 584 | verifyingContract: requirements.asset, 585 | }; 586 | } 587 | 588 | private getDefaultRpcUrl(network: Network): string | undefined { 589 | switch (network) { 590 | case 'base': 591 | return 'https://mainnet.base.org'; 592 | case 'base-sepolia': 593 | return 'https://sepolia.base.org'; 594 | case 'polygon': 595 | return 'https://polygon-rpc.com'; 596 | case 'polygon-amoy': 597 | return 'https://rpc-amoy.polygon.technology'; 598 | case 'avalanche': 599 | return 'https://api.avax.network/ext/bc/C/rpc'; 600 | case 'avalanche-fuji': 601 | return 'https://api.avax-test.network/ext/bc/C/rpc'; 602 | case 'iotex': 603 | return 'https://rpc.ankr.com/iotex'; 604 | case 'sei': 605 | return 'https://sei-rpc.publicnode.com'; 606 | case 'sei-testnet': 607 | return 'https://sei-testnet-rpc.publicnode.com'; 608 | case 'peaq': 609 | return 'https://erpc.peaq.network'; 610 | default: 611 | return undefined; 612 | } 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import { ExampleService } from './ExampleService.js'; 4 | import { MerchantExecutor, type MerchantExecutorOptions } from './MerchantExecutor.js'; 5 | import type { Network, PaymentPayload } from 'x402/types'; 6 | import { 7 | EventQueue, 8 | Message, 9 | RequestContext, 10 | Task, 11 | TaskState, 12 | } from './x402Types.js'; 13 | 14 | // Load environment variables 15 | dotenv.config(); 16 | 17 | const app = express(); 18 | app.use(express.json()); 19 | 20 | // Configuration 21 | const PORT = process.env.PORT || 3000; 22 | const PAY_TO_ADDRESS = process.env.PAY_TO_ADDRESS; 23 | const NETWORK = process.env.NETWORK || 'base-sepolia'; 24 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY; 25 | const FACILITATOR_URL = process.env.FACILITATOR_URL; 26 | const FACILITATOR_API_KEY = process.env.FACILITATOR_API_KEY; 27 | const SERVICE_URL = 28 | process.env.SERVICE_URL || `http://localhost:${PORT}/process`; 29 | const PRIVATE_KEY = process.env.PRIVATE_KEY; 30 | const RPC_URL = process.env.RPC_URL; 31 | const SETTLEMENT_MODE_ENV = process.env.SETTLEMENT_MODE?.toLowerCase(); 32 | const ASSET_ADDRESS = process.env.ASSET_ADDRESS; 33 | const ASSET_NAME = process.env.ASSET_NAME; 34 | const EXPLORER_URL = process.env.EXPLORER_URL; 35 | const CHAIN_ID = process.env.CHAIN_ID 36 | ? Number.parseInt(process.env.CHAIN_ID, 10) 37 | : undefined; 38 | const AI_PROVIDER = (process.env.AI_PROVIDER || 'openai').toLowerCase(); 39 | const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL; 40 | const EIGENAI_BASE_URL = 41 | process.env.EIGENAI_BASE_URL || 'https://eigenai.eigencloud.xyz/v1'; 42 | const EIGENAI_API_KEY = process.env.EIGENAI_API_KEY; 43 | const AI_MODEL = process.env.AI_MODEL; 44 | const AI_TEMPERATURE = process.env.AI_TEMPERATURE 45 | ? Number.parseFloat(process.env.AI_TEMPERATURE) 46 | : undefined; 47 | const AI_MAX_TOKENS = process.env.AI_MAX_TOKENS 48 | ? Number.parseInt(process.env.AI_MAX_TOKENS, 10) 49 | : undefined; 50 | const AI_SEED = process.env.AI_SEED 51 | ? Number.parseInt(process.env.AI_SEED, 10) 52 | : undefined; 53 | const SUPPORTED_NETWORKS: Network[] = [ 54 | 'base', 55 | 'base-sepolia', 56 | 'polygon', 57 | 'polygon-amoy', 58 | 'avalanche', 59 | 'avalanche-fuji', 60 | 'iotex', 61 | 'sei', 62 | 'sei-testnet', 63 | 'peaq', 64 | 'solana', 65 | 'solana-devnet', 66 | ]; 67 | 68 | // Validate environment variables 69 | if (AI_PROVIDER === 'openai') { 70 | if (!OPENAI_API_KEY) { 71 | console.error('❌ OPENAI_API_KEY is required when AI_PROVIDER=openai'); 72 | process.exit(1); 73 | } 74 | } else if (AI_PROVIDER === 'eigenai') { 75 | if (!EIGENAI_API_KEY && !OPENAI_API_KEY) { 76 | console.error('❌ EIGENAI_API_KEY (or OPENAI_API_KEY fallback) is required when AI_PROVIDER=eigenai'); 77 | process.exit(1); 78 | } 79 | } else { 80 | console.error( 81 | `❌ AI_PROVIDER "${AI_PROVIDER}" is not supported. Supported providers: openai, eigenai` 82 | ); 83 | process.exit(1); 84 | } 85 | 86 | if (!PAY_TO_ADDRESS) { 87 | console.error('❌ PAY_TO_ADDRESS is required'); 88 | process.exit(1); 89 | } 90 | 91 | if (!SUPPORTED_NETWORKS.includes(NETWORK as Network)) { 92 | console.error( 93 | `❌ NETWORK "${NETWORK}" is not supported. Supported networks: ${SUPPORTED_NETWORKS.join( 94 | ', ' 95 | )}` 96 | ); 97 | process.exit(1); 98 | } 99 | 100 | const resolvedNetwork = NETWORK as Network; 101 | 102 | let settlementMode: 'facilitator' | 'direct'; 103 | if (SETTLEMENT_MODE_ENV === 'local' || SETTLEMENT_MODE_ENV === 'direct') { 104 | settlementMode = 'direct'; 105 | } else if (SETTLEMENT_MODE_ENV === 'facilitator') { 106 | settlementMode = 'facilitator'; 107 | } else if (FACILITATOR_URL) { 108 | settlementMode = 'facilitator'; 109 | } else if (PRIVATE_KEY) { 110 | settlementMode = 'direct'; 111 | } else { 112 | settlementMode = 'facilitator'; 113 | } 114 | 115 | if (settlementMode === 'direct' && !PRIVATE_KEY) { 116 | console.error('❌ SETTLEMENT_MODE=local requires PRIVATE_KEY to be configured'); 117 | process.exit(1); 118 | } 119 | 120 | const exampleService = new ExampleService({ 121 | provider: AI_PROVIDER === 'eigenai' ? 'eigenai' : 'openai', 122 | apiKey: AI_PROVIDER === 'openai' ? OPENAI_API_KEY : undefined, 123 | baseUrl: 124 | AI_PROVIDER === 'eigenai' 125 | ? EIGENAI_BASE_URL 126 | : OPENAI_BASE_URL || undefined, 127 | defaultHeaders: 128 | AI_PROVIDER === 'eigenai' 129 | ? { 'x-api-key': (EIGENAI_API_KEY || OPENAI_API_KEY)! } 130 | : undefined, 131 | payToAddress: PAY_TO_ADDRESS, 132 | network: resolvedNetwork, 133 | model: 134 | AI_MODEL ?? 135 | (AI_PROVIDER === 'eigenai' ? 'gpt-oss-120b-f16' : 'gpt-4o-mini'), 136 | temperature: AI_TEMPERATURE ?? 0.7, 137 | maxTokens: AI_MAX_TOKENS ?? 500, 138 | seed: AI_PROVIDER === 'eigenai' ? AI_SEED : undefined, 139 | }); 140 | 141 | // Initialize the example service (replace with your own service) 142 | const merchantOptions: MerchantExecutorOptions = { 143 | payToAddress: PAY_TO_ADDRESS, 144 | network: resolvedNetwork, 145 | price: 0.1, 146 | facilitatorUrl: FACILITATOR_URL, 147 | facilitatorApiKey: FACILITATOR_API_KEY, 148 | resourceUrl: SERVICE_URL, 149 | settlementMode, 150 | rpcUrl: RPC_URL, 151 | privateKey: PRIVATE_KEY, 152 | assetAddress: ASSET_ADDRESS, 153 | assetName: ASSET_NAME, 154 | explorerUrl: EXPLORER_URL, 155 | chainId: CHAIN_ID, 156 | }; 157 | 158 | const merchantExecutor = new MerchantExecutor(merchantOptions); 159 | 160 | if (settlementMode === 'direct') { 161 | console.log('🧩 Using local settlement (direct EIP-3009 via RPC)'); 162 | if (RPC_URL) { 163 | console.log(`🔌 RPC endpoint: ${RPC_URL}`); 164 | } else { 165 | console.log('🔌 RPC endpoint: using default for selected network'); 166 | } 167 | } else if (FACILITATOR_URL) { 168 | console.log(`🌐 Using custom facilitator: ${FACILITATOR_URL}`); 169 | } else { 170 | console.log('🌐 Using default facilitator: https://x402.org/facilitator'); 171 | } 172 | 173 | console.log('🚀 x402 Payment API initialized'); 174 | console.log(`💰 Payment address: ${PAY_TO_ADDRESS}`); 175 | console.log(`🌐 Network: ${resolvedNetwork}`); 176 | console.log(`💵 Price per request: $0.10 USDC`); 177 | 178 | /** 179 | * Health check endpoint 180 | */ 181 | app.get('/health', (req, res) => { 182 | res.json({ 183 | status: 'healthy', 184 | service: 'x402-payment-api', 185 | version: '1.0.0', 186 | payment: { 187 | address: PAY_TO_ADDRESS, 188 | network: NETWORK, 189 | price: '$0.10', 190 | }, 191 | }); 192 | }); 193 | 194 | /** 195 | * Main endpoint to process paid requests 196 | * This endpoint accepts A2A-compatible task submissions with x402 payments 197 | */ 198 | app.post('/process', async (req, res) => { 199 | try { 200 | console.log('\n📥 Received request'); 201 | console.log('Request body:', JSON.stringify(req.body, null, 2)); 202 | 203 | // Parse the incoming request 204 | const { message, taskId, contextId, metadata } = req.body; 205 | 206 | if (!message) { 207 | return res.status(400).json({ 208 | error: 'Missing message in request body', 209 | }); 210 | } 211 | 212 | // Create a task from the request 213 | const task: Task = { 214 | id: taskId || `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 215 | contextId: contextId || `context-${Date.now()}`, 216 | status: { 217 | state: TaskState.INPUT_REQUIRED, 218 | message: message, 219 | }, 220 | metadata: metadata || {}, 221 | }; 222 | 223 | // Create request context 224 | const context: RequestContext = { 225 | taskId: task.id, 226 | contextId: task.contextId, 227 | currentTask: task, 228 | message: message, 229 | }; 230 | 231 | // Create event queue to collect responses 232 | const events: Task[] = []; 233 | const eventQueue: EventQueue = { 234 | enqueueEvent: async (event: Task) => { 235 | events.push(event); 236 | }, 237 | }; 238 | 239 | const paymentPayload = message.metadata?.['x402.payment.payload'] as 240 | | PaymentPayload 241 | | undefined; 242 | const paymentStatus = message.metadata?.['x402.payment.status']; 243 | 244 | if (!paymentPayload || paymentStatus !== 'payment-submitted') { 245 | const paymentRequired = merchantExecutor.createPaymentRequiredResponse(); 246 | 247 | const responseMessage: Message = { 248 | messageId: `msg-${Date.now()}`, 249 | role: 'agent', 250 | parts: [ 251 | { 252 | kind: 'text', 253 | text: 'Payment required. Please submit payment to continue.', 254 | }, 255 | ], 256 | metadata: { 257 | 'x402.payment.required': paymentRequired, 258 | 'x402.payment.status': 'payment-required', 259 | }, 260 | }; 261 | 262 | task.status.state = TaskState.INPUT_REQUIRED; 263 | task.status.message = responseMessage; 264 | task.metadata = { 265 | ...(task.metadata || {}), 266 | 'x402.payment.required': paymentRequired, 267 | 'x402.payment.status': 'payment-required', 268 | }; 269 | 270 | events.push(task); 271 | console.log('💰 Payment required for request processing'); 272 | 273 | return res.json({ 274 | success: false, 275 | error: 'Payment Required', 276 | task, 277 | events, 278 | }); 279 | } 280 | 281 | const verifyResult = await merchantExecutor.verifyPayment(paymentPayload); 282 | 283 | if (!verifyResult.isValid) { 284 | const errorReason = verifyResult.invalidReason || 'Invalid payment'; 285 | task.status.state = TaskState.FAILED; 286 | task.status.message = { 287 | messageId: `msg-${Date.now()}`, 288 | role: 'agent', 289 | parts: [ 290 | { 291 | kind: 'text', 292 | text: `Payment verification failed: ${errorReason}`, 293 | }, 294 | ], 295 | metadata: { 296 | 'x402.payment.status': 'payment-rejected', 297 | 'x402.payment.error': errorReason, 298 | }, 299 | }; 300 | task.metadata = { 301 | ...(task.metadata || {}), 302 | 'x402.payment.status': 'payment-rejected', 303 | 'x402.payment.error': errorReason, 304 | }; 305 | 306 | events.push(task); 307 | 308 | return res.status(402).json({ 309 | error: 'Payment verification failed', 310 | reason: errorReason, 311 | task, 312 | events, 313 | }); 314 | } 315 | 316 | task.metadata = { 317 | ...(task.metadata || {}), 318 | 'x402_payment_verified': true, 319 | 'x402.payment.status': 'payment-verified', 320 | ...(verifyResult.payer ? { 'x402.payment.payer': verifyResult.payer } : {}), 321 | }; 322 | 323 | await exampleService.execute(context, eventQueue); 324 | 325 | const settlement = await merchantExecutor.settlePayment(paymentPayload); 326 | 327 | task.metadata = { 328 | ...(task.metadata || {}), 329 | 'x402.payment.status': settlement.success ? 'payment-completed' : 'payment-failed', 330 | ...(settlement.transaction 331 | ? { 'x402.payment.receipts': [settlement] } 332 | : {}), 333 | ...(settlement.errorReason 334 | ? { 'x402.payment.error': settlement.errorReason } 335 | : {}), 336 | }; 337 | 338 | if (events.length === 0) { 339 | events.push(task); 340 | } 341 | 342 | console.log('📤 Sending response\n'); 343 | 344 | return res.json({ 345 | success: settlement.success, 346 | task: events[events.length - 1], 347 | events, 348 | settlement, 349 | }); 350 | } catch (error: any) { 351 | console.error('❌ Error processing request:', error); 352 | 353 | return res.status(500).json({ 354 | error: error.message || 'Internal server error', 355 | }); 356 | } 357 | }); 358 | 359 | /** 360 | * Simple test endpoint to try the agent 361 | */ 362 | app.post('/test', async (req, res) => { 363 | const message: Message = { 364 | messageId: `msg-${Date.now()}`, 365 | role: 'user', 366 | parts: [ 367 | { 368 | kind: 'text', 369 | text: req.body.text || 'Hello, tell me a joke!', 370 | }, 371 | ], 372 | }; 373 | 374 | try { 375 | const response = await fetch(`http://localhost:${PORT}/process`, { 376 | method: 'POST', 377 | headers: { 378 | 'Content-Type': 'application/json', 379 | }, 380 | body: JSON.stringify({ message }), 381 | }); 382 | 383 | const data = await response.json(); 384 | res.json(data); 385 | } catch (error: any) { 386 | res.status(500).json({ 387 | error: error.message, 388 | }); 389 | } 390 | }); 391 | 392 | // Start the server 393 | app.listen(PORT, () => { 394 | console.log(`\n✅ Server running on http://localhost:${PORT}`); 395 | console.log(`📖 Health check: http://localhost:${PORT}/health`); 396 | console.log(`🧪 Test endpoint: POST http://localhost:${PORT}/test`); 397 | console.log(`🚀 Main endpoint: POST http://localhost:${PORT}/process\n`); 398 | }); 399 | -------------------------------------------------------------------------------- /src/testClient.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { Wallet } from 'ethers'; 3 | import dotenv from 'dotenv'; 4 | import type { PaymentPayload, PaymentRequirements } from 'x402/types'; 5 | import { Message, Task } from './x402Types.js'; 6 | 7 | dotenv.config(); 8 | 9 | const AGENT_URL = process.env.AGENT_URL || 'http://localhost:3000'; 10 | const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; 11 | 12 | interface AgentResponse { 13 | success?: boolean; 14 | task?: Task; 15 | events?: Task[]; 16 | error?: string; 17 | x402?: any; 18 | settlement?: any; 19 | } 20 | 21 | const TRANSFER_AUTH_TYPES = { 22 | TransferWithAuthorization: [ 23 | { name: 'from', type: 'address' }, 24 | { name: 'to', type: 'address' }, 25 | { name: 'value', type: 'uint256' }, 26 | { name: 'validAfter', type: 'uint256' }, 27 | { name: 'validBefore', type: 'uint256' }, 28 | { name: 'nonce', type: 'bytes32' }, 29 | ], 30 | }; 31 | 32 | const CHAIN_IDS: Record = { 33 | base: 8453, 34 | 'base-sepolia': 84532, 35 | ethereum: 1, 36 | polygon: 137, 37 | 'polygon-amoy': 80002, 38 | }; 39 | 40 | function selectPaymentRequirement(paymentRequired: any): PaymentRequirements { 41 | const accepts = paymentRequired?.accepts; 42 | if (!Array.isArray(accepts) || accepts.length === 0) { 43 | throw new Error('No payment requirements provided by the agent'); 44 | } 45 | return accepts[0] as PaymentRequirements; 46 | } 47 | 48 | function generateNonce(): string { 49 | return `0x${randomBytes(32).toString('hex')}`; 50 | } 51 | 52 | function getChainId(network: string): number { 53 | const chainId = CHAIN_IDS[network]; 54 | if (!chainId) { 55 | throw new Error(`Unsupported network "${network}"`); 56 | } 57 | return chainId; 58 | } 59 | 60 | async function createPaymentPayload( 61 | paymentRequired: any, 62 | wallet: Wallet 63 | ): Promise { 64 | const requirement = selectPaymentRequirement(paymentRequired); 65 | 66 | const now = Math.floor(Date.now() / 1000); 67 | const authorization = { 68 | from: wallet.address, 69 | to: requirement.payTo, 70 | value: requirement.maxAmountRequired, 71 | validAfter: '0', 72 | validBefore: String(now + requirement.maxTimeoutSeconds), 73 | nonce: generateNonce(), 74 | }; 75 | 76 | const domain = { 77 | name: requirement.extra?.name || 'USDC', 78 | version: requirement.extra?.version || '2', 79 | chainId: getChainId(requirement.network), 80 | verifyingContract: requirement.asset, 81 | }; 82 | 83 | const signature = await wallet.signTypedData( 84 | domain, 85 | TRANSFER_AUTH_TYPES, 86 | authorization 87 | ); 88 | 89 | return { 90 | x402Version: paymentRequired.x402Version ?? 1, 91 | scheme: requirement.scheme, 92 | network: requirement.network, 93 | payload: { 94 | signature, 95 | authorization, 96 | }, 97 | }; 98 | } 99 | 100 | /** 101 | * Test client that can interact with the x402 AI agent 102 | * This demonstrates the complete payment flow 103 | */ 104 | export class TestClient { 105 | private wallet?: Wallet; 106 | private agentUrl: string; 107 | 108 | constructor(privateKey?: string, agentUrl: string = AGENT_URL) { 109 | if (privateKey) { 110 | this.wallet = new Wallet(privateKey); 111 | console.log(`💼 Client wallet: ${this.wallet.address}`); 112 | } 113 | this.agentUrl = agentUrl; 114 | } 115 | 116 | /** 117 | * Send a request to the agent 118 | */ 119 | async sendRequest(text: string): Promise { 120 | const message: Message = { 121 | messageId: `msg-${Date.now()}`, 122 | role: 'user', 123 | parts: [ 124 | { 125 | kind: 'text', 126 | text: text, 127 | }, 128 | ], 129 | }; 130 | 131 | console.log(`\n📤 Sending request: "${text}"`); 132 | 133 | const response = await fetch(`${this.agentUrl}/process`, { 134 | method: 'POST', 135 | headers: { 136 | 'Content-Type': 'application/json', 137 | }, 138 | body: JSON.stringify({ message }), 139 | }); 140 | 141 | const data = await response.json() as any; 142 | 143 | // Check for A2A-style payment requirement in task metadata 144 | if (data.task?.status?.message?.metadata?.['x402.payment.required']) { 145 | console.log('💳 Payment required (A2A style)!'); 146 | return { 147 | error: 'Payment Required', 148 | x402: data.task.status.message.metadata['x402.payment.required'], 149 | task: data.task 150 | }; 151 | } 152 | 153 | // Check for HTTP 402 style (legacy) 154 | if (response.status === 402) { 155 | console.log('💳 Payment required (HTTP 402)!'); 156 | return { error: 'Payment Required', x402: data.x402 }; 157 | } 158 | 159 | return data as AgentResponse; 160 | } 161 | 162 | /** 163 | * Send a paid request (with payment) 164 | */ 165 | async sendPaidRequest(text: string): Promise { 166 | if (!this.wallet) { 167 | throw new Error('Client wallet not configured. Set CLIENT_PRIVATE_KEY in .env'); 168 | } 169 | 170 | // Step 1: Send initial request 171 | console.log('\n=== STEP 1: Initial Request ==='); 172 | const initialResponse = await this.sendRequest(text); 173 | 174 | if (!initialResponse.x402) { 175 | console.log('✅ Request processed without payment (unexpected)'); 176 | return initialResponse; 177 | } 178 | 179 | // Step 2: Process payment requirement 180 | console.log('\n=== STEP 2: Processing Payment ==='); 181 | const paymentRequired = initialResponse.x402; 182 | console.log(`Payment options: ${paymentRequired.accepts.length}`); 183 | console.log(`First option: ${paymentRequired.accepts[0].asset} on ${paymentRequired.accepts[0].network}`); 184 | console.log(`Amount: ${paymentRequired.accepts[0].maxAmountRequired} (micro units)`); 185 | 186 | try { 187 | // Process the payment (sign it) 188 | console.log('🔐 Signing payment...'); 189 | const paymentPayload = await createPaymentPayload(paymentRequired, this.wallet); 190 | console.log('✅ Payment signed successfully'); 191 | 192 | console.log(`Payment payload created for ${paymentPayload.network}`); 193 | 194 | // Step 3: Submit payment with original message 195 | console.log('\n=== STEP 3: Submitting Payment ==='); 196 | 197 | // Use the taskId and contextId from the initial response if available 198 | const taskId = (initialResponse as any).task?.id || `task-${Date.now()}`; 199 | const contextId = (initialResponse as any).task?.contextId || `context-${Date.now()}`; 200 | 201 | // Create message with payment metadata embedded 202 | const message: Message = { 203 | messageId: `msg-${Date.now()}`, 204 | role: 'user', 205 | parts: [ 206 | { 207 | kind: 'text', 208 | text: text, 209 | }, 210 | ], 211 | metadata: { 212 | 'x402.payment.payload': paymentPayload, 213 | 'x402.payment.status': 'payment-submitted', 214 | }, 215 | }; 216 | 217 | const paidResponse = await fetch(`${this.agentUrl}/process`, { 218 | method: 'POST', 219 | headers: { 220 | 'Content-Type': 'application/json', 221 | }, 222 | body: JSON.stringify({ 223 | message, 224 | taskId: taskId, 225 | contextId: contextId, 226 | }), 227 | }); 228 | 229 | const paidData = await paidResponse.json() as any; 230 | 231 | if (paidResponse.ok) { 232 | console.log('✅ Payment accepted and request processed!'); 233 | return paidData as AgentResponse; 234 | } else { 235 | console.log(`❌ Payment failed: ${paidData.error || 'Unknown error'}`); 236 | return paidData as AgentResponse; 237 | } 238 | } catch (error) { 239 | console.error('❌ Error processing payment:', error); 240 | throw error; 241 | } 242 | } 243 | 244 | /** 245 | * Check agent health 246 | */ 247 | async checkHealth(): Promise { 248 | console.log('\n🏥 Checking agent health...'); 249 | const response = await fetch(`${this.agentUrl}/health`); 250 | const data = await response.json() as any; 251 | 252 | if (response.ok) { 253 | console.log('✅ Agent is healthy'); 254 | console.log(` Service: ${data.service}`); 255 | console.log(` Payment address: ${data.payment.address}`); 256 | console.log(` Network: ${data.payment.network}`); 257 | console.log(` Price: ${data.payment.price}`); 258 | } else { 259 | console.log('❌ Agent is not healthy'); 260 | } 261 | 262 | return data; 263 | } 264 | } 265 | 266 | /** 267 | * Main test function 268 | */ 269 | async function main() { 270 | console.log('🧪 x402 AI Agent Test Client'); 271 | console.log('================================\n'); 272 | 273 | const client = new TestClient(CLIENT_PRIVATE_KEY); 274 | 275 | // Check agent health 276 | await client.checkHealth(); 277 | 278 | // Test 1: Request without payment 279 | console.log('\n\n📋 TEST 1: Request without payment'); 280 | console.log('====================================='); 281 | try { 282 | const response = await client.sendRequest('What is 2+2?'); 283 | if (response.x402) { 284 | console.log('✅ Correctly received payment requirement'); 285 | } else { 286 | console.log('❌ Expected payment requirement'); 287 | } 288 | } catch (error) { 289 | console.error('❌ Test 1 failed:', error); 290 | } 291 | 292 | // Test 2: Request with payment (only if wallet configured) 293 | if (CLIENT_PRIVATE_KEY) { 294 | console.log('\n\n📋 TEST 2: Request with payment'); 295 | console.log('====================================='); 296 | try { 297 | const response = await client.sendPaidRequest('Tell me a joke about TypeScript!'); 298 | 299 | if (response.success && response.task) { 300 | console.log('\n🎉 SUCCESS! Response from AI:'); 301 | console.log('-----------------------------------'); 302 | const aiResponse = response.task.status.message?.parts 303 | ?.filter((p: any) => p.kind === 'text') 304 | .map((p: any) => p.text) 305 | .join(' '); 306 | console.log(aiResponse); 307 | console.log('-----------------------------------'); 308 | } else { 309 | console.log('❌ Request failed:', response.error); 310 | } 311 | } catch (error) { 312 | console.error('❌ Test 2 failed:', error); 313 | } 314 | } else { 315 | console.log('\n\n⚠️ TEST 2: Skipped (no CLIENT_PRIVATE_KEY configured)'); 316 | console.log('====================================='); 317 | console.log('To test with payment, set CLIENT_PRIVATE_KEY in .env'); 318 | console.log('This wallet needs:'); 319 | console.log(' - USDC tokens (testnet or mainnet)'); 320 | console.log(' - USDC approval for transfers'); 321 | console.log(' - Gas tokens (ETH) for the network'); 322 | } 323 | 324 | console.log('\n\n✅ Tests complete!'); 325 | } 326 | 327 | // Run if called directly 328 | if (import.meta.url === `file://${process.argv[1]}`) { 329 | main().catch(console.error); 330 | } 331 | 332 | export { main as runTests }; 333 | -------------------------------------------------------------------------------- /src/x402Types.ts: -------------------------------------------------------------------------------- 1 | export enum TaskState { 2 | SUBMITTED = 'submitted', 3 | WORKING = 'working', 4 | INPUT_REQUIRED = 'input-required', 5 | COMPLETED = 'completed', 6 | FAILED = 'failed', 7 | } 8 | 9 | export interface TextPart { 10 | kind: 'text'; 11 | text: string; 12 | } 13 | 14 | export interface Message { 15 | messageId: string; 16 | taskId?: string; 17 | role: 'user' | 'agent'; 18 | parts: TextPart[]; 19 | metadata?: Record; 20 | } 21 | 22 | export interface TaskStatus { 23 | state: TaskState; 24 | message?: Message; 25 | } 26 | 27 | export interface Task { 28 | id: string; 29 | contextId?: string; 30 | status: TaskStatus; 31 | metadata?: Record; 32 | artifacts?: any[]; 33 | } 34 | 35 | export interface RequestContext { 36 | taskId: string; 37 | contextId?: string; 38 | currentTask?: Task; 39 | message: Message; 40 | } 41 | 42 | export interface EventQueue { 43 | enqueueEvent(event: Task): Promise; 44 | } 45 | -------------------------------------------------------------------------------- /test-agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Complete test script for x402 AI Agent 4 | # This script tests both unpaid and paid request flows 5 | 6 | set -e 7 | 8 | echo "🧪 x402 AI Agent Test Suite" 9 | echo "============================" 10 | echo "" 11 | 12 | # Check if agent is running 13 | echo "1️⃣ Checking if agent is running..." 14 | if ! curl -s http://localhost:3000/health > /dev/null 2>&1; then 15 | echo "❌ Agent is not running!" 16 | echo "" 17 | echo "Please start the agent first:" 18 | echo " npm start" 19 | echo "" 20 | exit 1 21 | fi 22 | 23 | echo "✅ Agent is running" 24 | echo "" 25 | 26 | # Check if .env exists 27 | if [ ! -f ".env" ]; then 28 | echo "⚠️ Warning: No .env file found" 29 | echo "Creating .env from .env.example..." 30 | cp .env.example .env 31 | echo "" 32 | echo "Please edit .env and add your configuration, then run this script again." 33 | exit 1 34 | fi 35 | 36 | # Build the test client if needed 37 | if [ ! -d "dist" ]; then 38 | echo "2️⃣ Building project..." 39 | npm run build 40 | echo "" 41 | fi 42 | 43 | # Run the test client 44 | echo "3️⃣ Running test client..." 45 | echo "" 46 | node dist/testClient.js 47 | 48 | echo "" 49 | echo "✅ Test suite complete!" 50 | -------------------------------------------------------------------------------- /test-facilitator.js: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { Wallet } from 'ethers'; 3 | import dotenv from 'dotenv'; 4 | 5 | dotenv.config(); 6 | 7 | const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; 8 | const PAY_TO_ADDRESS = process.env.PAY_TO_ADDRESS; 9 | const NETWORK = process.env.NETWORK || 'base-sepolia'; 10 | 11 | const TRANSFER_AUTH_TYPES = { 12 | TransferWithAuthorization: [ 13 | { name: 'from', type: 'address' }, 14 | { name: 'to', type: 'address' }, 15 | { name: 'value', type: 'uint256' }, 16 | { name: 'validAfter', type: 'uint256' }, 17 | { name: 'validBefore', type: 'uint256' }, 18 | { name: 'nonce', type: 'bytes32' }, 19 | ], 20 | }; 21 | 22 | const CHAIN_IDS = { 23 | base: 8453, 24 | 'base-sepolia': 84532, 25 | ethereum: 1, 26 | polygon: 137, 27 | 'polygon-amoy': 80002, 28 | }; 29 | 30 | function selectPaymentRequirement(paymentRequired) { 31 | if (!Array.isArray(paymentRequired?.accepts) || paymentRequired.accepts.length === 0) { 32 | throw new Error('No payment requirements provided'); 33 | } 34 | return paymentRequired.accepts[0]; 35 | } 36 | 37 | function generateNonce() { 38 | return `0x${randomBytes(32).toString('hex')}`; 39 | } 40 | 41 | function getChainId(network) { 42 | const chainId = CHAIN_IDS[network]; 43 | if (!chainId) { 44 | throw new Error(`Unsupported network "${network}"`); 45 | } 46 | return chainId; 47 | } 48 | 49 | async function createPaymentPayload(paymentRequired, wallet) { 50 | const requirement = selectPaymentRequirement(paymentRequired); 51 | const now = Math.floor(Date.now() / 1000); 52 | 53 | const authorization = { 54 | from: wallet.address, 55 | to: requirement.payTo, 56 | value: requirement.maxAmountRequired, 57 | validAfter: '0', 58 | validBefore: String(now + requirement.maxTimeoutSeconds), 59 | nonce: generateNonce(), 60 | }; 61 | 62 | const domain = { 63 | name: requirement.extra?.name || 'USDC', 64 | version: requirement.extra?.version || '2', 65 | chainId: getChainId(requirement.network), 66 | verifyingContract: requirement.asset, 67 | }; 68 | 69 | const signature = await wallet.signTypedData(domain, TRANSFER_AUTH_TYPES, authorization); 70 | 71 | return { 72 | x402Version: paymentRequired.x402Version ?? 1, 73 | scheme: requirement.scheme, 74 | network: requirement.network, 75 | payload: { 76 | signature, 77 | authorization, 78 | }, 79 | }; 80 | } 81 | 82 | async function testFacilitator() { 83 | console.log('🧪 Testing Facilitator Communication'); 84 | console.log('====================================\n'); 85 | 86 | if (!CLIENT_PRIVATE_KEY) { 87 | console.error('❌ CLIENT_PRIVATE_KEY not set'); 88 | return; 89 | } 90 | 91 | const wallet = new Wallet(CLIENT_PRIVATE_KEY); 92 | console.log(`💼 Client wallet: ${wallet.address}`); 93 | console.log(`💰 Merchant wallet: ${PAY_TO_ADDRESS}`); 94 | console.log(`🌐 Network: ${NETWORK}\n`); 95 | 96 | // Create a payment requirement 97 | const paymentRequired = { 98 | x402Version: 1, 99 | accepts: [{ 100 | scheme: 'exact', 101 | network: NETWORK, 102 | asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', // USDC on Base Sepolia 103 | payTo: PAY_TO_ADDRESS, 104 | maxAmountRequired: '100000', // $0.10 105 | resource: '/test-resource', 106 | description: 'Test payment', 107 | mimeType: 'application/json', 108 | maxTimeoutSeconds: 600, 109 | extra: { 110 | name: 'USDC', 111 | version: '2', 112 | }, 113 | }], 114 | error: 'Payment required', 115 | }; 116 | 117 | console.log('📝 Payment requirements:'); 118 | console.log(JSON.stringify(paymentRequired, null, 2)); 119 | 120 | // Sign the payment 121 | console.log('\n🔐 Signing payment...'); 122 | const paymentPayload = await createPaymentPayload(paymentRequired, wallet); 123 | 124 | console.log('\n✅ Payment signed!'); 125 | console.log('Payment payload:'); 126 | console.log(JSON.stringify(paymentPayload, null, 2)); 127 | 128 | // Try to verify with the facilitator 129 | console.log('\n📡 Sending verification request to facilitator...'); 130 | console.log('URL: https://x402.org/facilitator/verify\n'); 131 | 132 | try { 133 | const verifyResponse = await fetch('https://x402.org/facilitator/verify', { 134 | method: 'POST', 135 | headers: { 136 | 'Content-Type': 'application/json', 137 | }, 138 | body: JSON.stringify({ 139 | payment: paymentPayload, 140 | requirements: paymentRequired.accepts[0], 141 | }), 142 | }); 143 | 144 | console.log(`Response status: ${verifyResponse.status} ${verifyResponse.statusText}`); 145 | 146 | const responseText = await verifyResponse.text(); 147 | console.log('\nResponse body:'); 148 | console.log(responseText); 149 | 150 | if (verifyResponse.ok) { 151 | try { 152 | const data = JSON.parse(responseText); 153 | console.log('\n✅ Parsed response:'); 154 | console.log(JSON.stringify(data, null, 2)); 155 | } catch (e) { 156 | // Response is not JSON 157 | } 158 | } else { 159 | console.log('\n❌ Facilitator returned an error'); 160 | 161 | // Try to parse error details 162 | try { 163 | const errorData = JSON.parse(responseText); 164 | console.log('\nError details:'); 165 | console.log(JSON.stringify(errorData, null, 2)); 166 | } catch (e) { 167 | console.log('\nRaw error response (not JSON):'); 168 | console.log(responseText); 169 | } 170 | } 171 | 172 | } catch (error) { 173 | console.error('\n❌ Error communicating with facilitator:', error); 174 | } 175 | 176 | // Also try the settle endpoint to see its format 177 | console.log('\n\n📡 Testing settle endpoint...'); 178 | console.log('URL: https://x402.org/facilitator/settle\n'); 179 | 180 | try { 181 | const settleResponse = await fetch('https://x402.org/facilitator/settle', { 182 | method: 'POST', 183 | headers: { 184 | 'Content-Type': 'application/json', 185 | }, 186 | body: JSON.stringify({ 187 | payment: paymentPayload, 188 | requirements: paymentRequired.accepts[0], 189 | }), 190 | }); 191 | 192 | console.log(`Response status: ${settleResponse.status} ${settleResponse.statusText}`); 193 | 194 | const responseText = await settleResponse.text(); 195 | console.log('\nResponse body:'); 196 | console.log(responseText); 197 | 198 | } catch (error) { 199 | console.error('\n❌ Error communicating with facilitator:', error); 200 | } 201 | } 202 | 203 | testFacilitator().catch(console.error); 204 | -------------------------------------------------------------------------------- /test-request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test script for the x402 AI Agent 4 | 5 | PORT=${PORT:-3000} 6 | HOST="http://localhost:${PORT}" 7 | 8 | echo "🧪 Testing x402 AI Agent" 9 | echo "" 10 | 11 | # Test 1: Health check 12 | echo "1️⃣ Testing health endpoint..." 13 | curl -s "${HOST}/health" | jq '.' || echo "❌ Health check failed" 14 | echo "" 15 | 16 | # Test 2: Simple request (should return 402 Payment Required) 17 | echo "" 18 | echo "2️⃣ Testing payment required flow..." 19 | echo "Sending request to /process endpoint..." 20 | echo "" 21 | 22 | curl -X POST "${HOST}/process" \ 23 | -H "Content-Type: application/json" \ 24 | -d '{ 25 | "message": { 26 | "parts": [ 27 | { 28 | "type": "text", 29 | "text": "What is 2+2?" 30 | } 31 | ] 32 | } 33 | }' | jq '.' || echo "❌ Request failed" 34 | 35 | echo "" 36 | echo "✅ Test complete!" 37 | echo "" 38 | echo "Expected: 402 Payment Required response with x402 payment details" 39 | echo "To complete the payment, you need to use an x402-compatible client" 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "lib": ["ES2022"], 6 | "moduleResolution": "nodenext", 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | --------------------------------------------------------------------------------