Skip to content

Commit 7cb5f9a

Browse files
committed
feat: add x402 payment tool example
Demonstrates how to build a payment-enabled tool for NeMo Agent Toolkit that handles HTTP 402 (Payment Required) responses using the x402 protocol. When an agent calls a paid API and receives HTTP 402, this tool: - Parses x402 payment requirements from the response - Evaluates cost against configurable spending policy - Signs a payment proof using the agent's wallet (key isolated) - Retries the request with payment proof attached Uses @register_function with FunctionBaseConfig, valid NAT workflow configs (functions/llms/workflow with _type refs), and nat.components entry point registration. Tools: fetch_paid_api (402 handling + payment) and get_payment_status (spending awareness). Includes mock server for e2e testing. Related: NVIDIA/NeMo-Agent-Toolkit#1806 Signed-off-by: up2itnow0822 <up2itnow0822@users.noreply.github.com>
1 parent 27ac411 commit 7cb5f9a

8 files changed

Lines changed: 968 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
examples/x402_payment_tool/src/nat_x402_payment/configs/payment-agent.yml
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
<!--
2+
SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
# x402 Payment Tool for NeMo Agent Toolkit
19+
20+
## Overview
21+
22+
### The Problem
23+
24+
AI agents increasingly need to interact with paid APIs and services. When an agent encounters an HTTP 402 (Payment Required) response, it needs to evaluate the cost, authorize payment, and retry — all autonomously. Unlike regular API errors, payment operations are **irreversible**, requiring security guarantees beyond standard tool integrations.
25+
26+
### The Solution
27+
28+
This example implements a payment-enabled tool for NeMo Agent Toolkit using `@register_function` that handles [x402](https://github.com/coinbase/x402) payment negotiation within a ReAct agent loop.
29+
30+
### How It Works
31+
32+
```
33+
Agent receives user query
34+
35+
36+
fetch_paid_api tool called with target URL
37+
38+
39+
API responds with HTTP 402 + x402 payment requirements
40+
41+
42+
Tool checks spending policy (per-tx cap, daily limit, recipient allowlist)
43+
44+
├── DENIED → Return denial reason to agent
45+
46+
47+
Wallet signer (isolated process) signs the payment
48+
49+
50+
Tool retries request with X-PAYMENT header
51+
52+
53+
Agent receives data and continues reasoning
54+
```
55+
56+
## Best Practices for x402 Payment Tools
57+
58+
### 1. Isolate Wallet Keys from the Agent Process
59+
60+
Payment tools handle irreversible value transfer. The signing key must never live in the same process as the agent.
61+
62+
| Mode | Key Location | Use Case |
63+
|------|-------------|----------|
64+
| **Inline** (dev only) | `WALLET_PRIVATE_KEY` env var | Local testing with mock server |
65+
| **Remote signer** (production) | Separate process via `WALLET_SIGNER_URL` | Any deployment with real funds |
66+
67+
The remote signer pattern ensures that even if the agent process is compromised, the attacker cannot sign arbitrary transactions — the signer only accepts pre-validated payment requests.
68+
69+
### 2. Enforce Spending Policy Before Signing
70+
71+
Spending limits are checked **before** the wallet is asked to sign, not after. This prevents a compromised or hallucinating agent from constructing valid payment transactions that exceed budget.
72+
73+
```yaml
74+
# In your NAT workflow config:
75+
functions:
76+
fetch_paid_api:
77+
_type: fetch_paid_api
78+
max_per_transaction: 0.10 # Max USDC per payment
79+
max_daily_spend: 5.00 # Daily cap
80+
allowed_recipients: # Only these addresses can receive funds
81+
- "0x..."
82+
```
83+
84+
### 3. Allowlist Payment Recipients
85+
86+
Never allow an agent to pay arbitrary addresses. Configure `allowed_recipients` with the addresses of your known API providers. Any payment to an unlisted address is rejected before signing.
87+
88+
### 4. Log Every Payment Attempt
89+
90+
The `get_payment_status` tool provides the agent with spending awareness and gives operators a full audit trail. Every payment attempt (successful, denied, or failed) is logged with timestamp, amount, recipient, and transaction hash.
91+
92+
## Prerequisites
93+
94+
- Python >= 3.11
95+
- NeMo Agent Toolkit >= 1.4.0 (`pip install nvidia-nat[langchain]`)
96+
- An NVIDIA API key for NIM models (set `NVIDIA_API_KEY`)
97+
- For testing: no wallet needed (mock server accepts any signature)
98+
- For production: an Ethereum wallet with USDC on Base network
99+
100+
## Quick Start
101+
102+
### 1. Install the Example
103+
104+
```bash
105+
cd examples/x402_payment_tool
106+
pip install -e .
107+
```
108+
109+
### 2. Start the Mock x402 Server
110+
111+
```bash
112+
python scripts/mock_x402_server.py &
113+
# Server runs on http://localhost:8402
114+
# GET /v1/market-data → 402 (requires payment)
115+
# GET /v1/market-data + X-PAYMENT header → 200 (mock data)
116+
```
117+
118+
### 3. Configure and Run
119+
120+
```bash
121+
# Copy example config
122+
cp src/nat_x402_payment/configs/payment-agent.example.yml \
123+
src/nat_x402_payment/configs/payment-agent.yml
124+
125+
# Set wallet key (any hex string works with mock server)
126+
export WALLET_PRIVATE_KEY="0x0000000000000000000000000000000000000000000000000000000000000001"
127+
128+
# Set NVIDIA API key for the LLM
129+
export NVIDIA_API_KEY="your-nvidia-api-key"
130+
131+
# Run the agent
132+
nat run --config_file src/nat_x402_payment/configs/payment-agent.yml
133+
```
134+
135+
### 4. Test the Agent
136+
137+
Once the agent is running, try prompts like:
138+
139+
```
140+
> Fetch the premium market data from http://localhost:8402/v1/market-data
141+
```
142+
143+
The agent will:
144+
1. Call `fetch_paid_api` with the URL
145+
2. Receive a 402 response
146+
3. Evaluate the cost (0.05 USDC) against the spending policy
147+
4. Sign payment and retry
148+
5. Return the market data
149+
150+
```
151+
> What's my current payment spending status?
152+
```
153+
154+
The agent will call `get_payment_status` and report daily spend and recent transactions.
155+
156+
## File Structure
157+
158+
```
159+
x402_payment_tool/
160+
├── README.md
161+
├── pyproject.toml
162+
├── scripts/
163+
│ └── mock_x402_server.py # Mock paid API for testing
164+
└── src/nat_x402_payment/
165+
├── __init__.py
166+
├── register.py # NAT tool registration (@register_function)
167+
├── wallet.py # Wallet signing abstraction (inline/remote)
168+
└── configs/
169+
└── payment-agent.example.yml # NAT workflow configuration
170+
```
171+
172+
## Configuration Reference
173+
174+
### Spending Policy
175+
176+
| Parameter | Default | Description |
177+
|-----------|---------|-------------|
178+
| `max_per_transaction` | 0.10 | Maximum USDC per single payment |
179+
| `max_daily_spend` | 5.00 | Daily spending cap in USDC |
180+
| `allowed_recipients` | [] | Allowlisted addresses (empty = allow all) |
181+
| `wallet_signer_url` | "" | Remote signer URL (empty = inline mode) |
182+
| `request_timeout` | 30.0 | HTTP request timeout in seconds |
183+
184+
### Wallet Modes
185+
186+
**Inline (development):**
187+
```bash
188+
export WALLET_PRIVATE_KEY="0x..."
189+
```
190+
191+
**Remote signer (production):**
192+
```yaml
193+
functions:
194+
fetch_paid_api:
195+
_type: fetch_paid_api
196+
wallet_signer_url: "http://localhost:8900"
197+
```
198+
199+
The remote signer exposes three endpoints:
200+
- `GET /address` — wallet public address
201+
- `GET /balance?asset=...&network=...` — token balance
202+
- `POST /sign` — sign a payment request
203+
204+
## References
205+
206+
- [x402 Protocol (Coinbase)](https://github.com/coinbase/x402) — HTTP 402 payment standard
207+
- [agent-wallet-sdk](https://github.com/up2itnow0822/agent-wallet-sdk) — Non-custodial agent wallets with on-chain spending policies
208+
- [agentpay-mcp](https://github.com/up2itnow0822/agentpay-mcp) — MCP payment server implementation
209+
- Related issue: [NVIDIA/NeMo-Agent-Toolkit#1806](https://github.com/NVIDIA/NeMo-Agent-Toolkit/issues/1806)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
[build-system]
17+
build-backend = "setuptools.build_meta"
18+
requires = ["setuptools >= 64", "setuptools-scm>=8"]
19+
20+
[tool.setuptools_scm]
21+
git_describe_command = "git describe --long --first-parent"
22+
root = "../.."
23+
24+
[project]
25+
name = "nat_x402_payment"
26+
dynamic = ["version"]
27+
dependencies = [
28+
"nvidia-nat[langchain]>=1.4.0a0,<1.5.0",
29+
"httpx>=0.27.0",
30+
"eth-account>=0.13.0",
31+
]
32+
requires-python = ">=3.11,<3.14"
33+
description = "x402 payment-enabled tool for NeMo Agent Toolkit — handle HTTP 402 payment negotiation in agent loops"
34+
keywords = ["ai", "payments", "x402", "nvidia", "agents", "wallet"]
35+
classifiers = ["Programming Language :: Python"]
36+
37+
[project.entry-points.'nat.components']
38+
nat_x402_payment = "nat_x402_payment.register"
39+
40+
[tool.setuptools.packages.find]
41+
where = ["src"]
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""
18+
Mock x402 server for testing the payment tool.
19+
20+
Simulates a paid API that:
21+
- Returns 402 with x402 payment requirements for unauthenticated requests
22+
- Returns 200 with mock data when valid payment proof is provided
23+
24+
Usage:
25+
python scripts/mock_x402_server.py
26+
# Server runs on http://localhost:8402
27+
"""
28+
29+
import json
30+
from http.server import HTTPServer, BaseHTTPRequestHandler
31+
32+
MOCK_DATA = {
33+
"market_data": {
34+
"symbol": "NVDA",
35+
"price": 142.50,
36+
"volume": 45_000_000,
37+
"change_24h": 3.2,
38+
"source": "premium-data-provider",
39+
"timestamp": "2026-03-18T12:00:00Z",
40+
}
41+
}
42+
43+
PAYMENT_REQUIREMENTS = {
44+
"x402Version": 1,
45+
"accepts": [
46+
{
47+
"scheme": "exact",
48+
"network": "base",
49+
"maxAmountRequired": "50000", # 0.05 USDC (6 decimals)
50+
"resource": "http://localhost:8402/v1/market-data",
51+
"description": "Premium market data access",
52+
"mimeType": "application/json",
53+
"payTo": "0x1234567890abcdef1234567890abcdef12345678",
54+
"maxTimeoutSeconds": 300,
55+
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base
56+
}
57+
],
58+
}
59+
60+
MOCK_RECIPIENT = "0x1234567890abcdef1234567890abcdef12345678"
61+
62+
63+
class X402Handler(BaseHTTPRequestHandler):
64+
def do_GET(self): # noqa: N802
65+
if self.path.startswith("/v1/market-data"):
66+
payment_header = self.headers.get("X-PAYMENT", "")
67+
68+
if payment_header:
69+
# Validate payment proof (mock: just check it's non-empty JSON)
70+
try:
71+
payment = json.loads(payment_header)
72+
if payment.get("payTo") == MOCK_RECIPIENT:
73+
self.send_response(200)
74+
self.send_header("Content-Type", "application/json")
75+
self.end_headers()
76+
self.wfile.write(json.dumps(MOCK_DATA).encode())
77+
return
78+
except (json.JSONDecodeError, KeyError):
79+
pass
80+
81+
# Invalid payment
82+
self.send_response(400)
83+
self.send_header("Content-Type", "application/json")
84+
self.end_headers()
85+
self.wfile.write(json.dumps({"error": "Invalid payment proof"}).encode())
86+
return
87+
88+
# No payment — return 402
89+
self.send_response(402)
90+
self.send_header("Content-Type", "application/json")
91+
self.end_headers()
92+
self.wfile.write(json.dumps(PAYMENT_REQUIREMENTS).encode())
93+
return
94+
95+
# 404 for unknown paths
96+
self.send_response(404)
97+
self.send_header("Content-Type", "application/json")
98+
self.end_headers()
99+
self.wfile.write(json.dumps({"error": "Not found"}).encode())
100+
101+
def log_message(self, format, *args):
102+
"""Custom log format."""
103+
print(f"[x402-mock] {args[0]}")
104+
105+
106+
def main():
107+
port = 8402
108+
server = HTTPServer(("localhost", port), X402Handler)
109+
print(f"Mock x402 server running on http://localhost:{port}")
110+
print(f" GET /v1/market-data → 402 (requires payment)")
111+
print(f" GET /v1/market-data + X-PAYMENT header → 200 (mock data)")
112+
print()
113+
try:
114+
server.serve_forever()
115+
except KeyboardInterrupt:
116+
print("\nShutting down.")
117+
server.server_close()
118+
119+
120+
if __name__ == "__main__":
121+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0

0 commit comments

Comments
 (0)