diff --git a/examples/spraay_crypto_payments/README.md b/examples/spraay_crypto_payments/README.md new file mode 100644 index 0000000..f0a8d6f --- /dev/null +++ b/examples/spraay_crypto_payments/README.md @@ -0,0 +1,156 @@ +# Spraay Crypto Payments Agent + +An AI agent that queries cryptocurrency data across 15 blockchains using the +[Spraay x402 gateway](https://gateway.spraay.app). The agent can check gateway +health, list supported chains and routes, look up wallet balances, and get +token prices — all through natural language. + +## Overview + +This example demonstrates how to build a **crypto query agent** using NeMo +Agent Toolkit with custom tools that interact with the Spraay x402 protocol +gateway. The agent uses a ReAct pattern to reason about queries and execute +them via HTTP API calls. + +### What is x402? + +The [x402 protocol](https://www.x402.org) enables AI agents to pay for API +services using USDC micropayments over HTTP. When an agent calls a paid +endpoint, the server returns HTTP 402 (Payment Required) with payment details. +The agent signs a USDC transaction, resends the request with payment proof, +and the server executes the operation. + +### Supported Chains + +Base · Ethereum · Arbitrum · Polygon · BNB Chain · Avalanche · Solana · +Bitcoin · Stacks · Unichain · Plasma · BOB · Bittensor · Stellar · XRP Ledger + +## Prerequisites + +- Python 3.11+ +- [uv](https://docs.astral.sh/uv/) package manager +- NVIDIA API key from [build.nvidia.com](https://build.nvidia.com) + +## Setup + +1. Clone this repository and navigate to the example: + +```bash +cd examples/spraay_crypto_payments +``` + +2. Install dependencies: + +```bash +uv pip install -e . +``` + +3. Set environment variables: + +```bash +export NVIDIA_API_KEY= +export SPRAAY_GATEWAY_URL=https://gateway.spraay.app # optional, this is the default +``` + +## Running the Example + +### Check gateway health + +```bash +nat run \ + --config_file configs/config.yml \ + --input "Is the Spraay gateway healthy?" +``` + +### List supported chains + +```bash +nat run \ + --config_file configs/config.yml \ + --input "What blockchains does Spraay support?" +``` + +### Get token price + +```bash +nat run \ + --config_file configs/config.yml \ + --input "What is the current price of ETH on Base?" +``` + +## Expected Output + +``` +$ nat run --config_file configs/config.yml --input "Is the Spraay gateway healthy?" + +Configuration Summary: +-------------------- +Workflow Type: react_agent +Number of Functions: 5 +Number of LLMs: 1 + +Agent's thoughts: +Thought: The user wants to check if the Spraay gateway is healthy. +I should use the spraay_health tool. +Action: spraay_health +Action Input: check health + +Observation: { + "status": "ok", + "version": "3.6.0", + "uptime": "..." +} + +Thought: The gateway is healthy and running. +Final Answer: Yes, the Spraay x402 gateway is healthy. It is running +version 3.6.0 and reporting an "ok" status. +------------------------------ +Workflow Result: ['Yes, the Spraay x402 gateway is healthy...'] +``` + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ NeMo Agent Toolkit │ +│ │ +│ ┌───────────────────────────────────┐ │ +│ │ ReAct Agent (Llama 3.1) │ │ +│ │ │ │ +│ │ Tools: │ │ +│ │ ├── spraay_health │ │ +│ │ ├── spraay_routes │ │ +│ │ ├── spraay_chains │ │ +│ │ ├── spraay_balance │ │ +│ │ └── spraay_price │ │ +│ └──────────────┬────────────────────┘ │ +│ │ │ +└─────────────────┼────────────────────────┘ + │ HTTP + x402 + ▼ + ┌────────────────────────┐ + │ Spraay x402 Gateway │ + │ gateway.spraay.app │ + │ │ + │ 84+ paid endpoints │ + │ 15 blockchains │ + │ USDC micropayments │ + └────────────────────────┘ +``` + +## Files + +| File | Description | +|------|-------------| +| `configs/config.yml` | NeMo Agent Toolkit workflow configuration | +| `src/spraay_crypto_payments/register.py` | Tool registration with `@register_function` | +| `src/spraay_crypto_payments/spraay_client.py` | Async HTTP client for the Spraay gateway | +| `src/spraay_crypto_payments/__init__.py` | Package init | +| `pyproject.toml` | Project dependencies and NAT entry points | + +## Links + +- [Spraay Gateway Docs](https://docs.spraay.app) +- [x402 Protocol](https://www.x402.org) +- [Spraay MCP Server](https://smithery.ai/server/@plagtech/spraay-x402-mcp) +- [NeMo Agent Toolkit Docs](https://docs.nvidia.com/nemo/agent-toolkit/latest/) diff --git a/examples/spraay_crypto_payments/configs/config.yml b/examples/spraay_crypto_payments/configs/config.yml new file mode 100644 index 0000000..46fbf84 --- /dev/null +++ b/examples/spraay_crypto_payments/configs/config.yml @@ -0,0 +1,56 @@ +# Spraay Crypto Payments Agent +# NeMo Agent Toolkit workflow configuration +# +# This agent uses the Spraay x402 gateway to execute cryptocurrency +# payments across 15 blockchains via USDC micropayments. + +functions: + # Free query tools (no x402 payment required) + spraay_health: + _type: spraay_gateway_tool + gateway_url: ${SPRAAY_GATEWAY_URL:-https://gateway.spraay.app} + endpoint: /health + method: GET + description: "Check the health status of the Spraay x402 gateway" + + spraay_routes: + _type: spraay_gateway_tool + gateway_url: ${SPRAAY_GATEWAY_URL:-https://gateway.spraay.app} + endpoint: /v1/routes + method: GET + description: "List all available Spraay gateway routes with pricing info" + + spraay_chains: + _type: spraay_gateway_tool + gateway_url: ${SPRAAY_GATEWAY_URL:-https://gateway.spraay.app} + endpoint: /v1/chains + method: GET + description: "List all supported blockchains on the Spraay gateway" + + spraay_balance: + _type: spraay_balance_tool + gateway_url: ${SPRAAY_GATEWAY_URL:-https://gateway.spraay.app} + description: "Check the token balance of a wallet address on a specific chain" + + spraay_price: + _type: spraay_price_tool + gateway_url: ${SPRAAY_GATEWAY_URL:-https://gateway.spraay.app} + description: "Get the current price of a token on a specific chain" + +llms: + nim_llm: + _type: nim + model_name: meta/llama-3.1-70b-instruct + temperature: 0.0 + +workflow: + _type: react_agent + tool_names: + - spraay_health + - spraay_routes + - spraay_chains + - spraay_balance + - spraay_price + llm_name: nim_llm + verbose: true + parse_agent_response_max_retries: 3 diff --git a/examples/spraay_crypto_payments/pyproject.toml b/examples/spraay_crypto_payments/pyproject.toml new file mode 100644 index 0000000..24e730e --- /dev/null +++ b/examples/spraay_crypto_payments/pyproject.toml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +[project] +name = "spraay-crypto-payments" +version = "0.1.0" +description = "Spraay x402 crypto payment tools for NVIDIA NeMo Agent Toolkit" +readme = "README.md" +license = { text = "Apache-2.0" } +requires-python = ">=3.11,<3.14" +dependencies = [ + "nvidia-nat>=1.3.0", + "httpx>=0.27.0", +] + +[build-system] +requires = ["setuptools>=75.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[project.entry-points.'nat.components'] +spraay_crypto_payments = "spraay_crypto_payments.register" diff --git a/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py b/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py new file mode 100644 index 0000000..a9c2f92 --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/spraay_crypto_payments/src/spraay_crypto_payments/register.py b/examples/spraay_crypto_payments/src/spraay_crypto_payments/register.py new file mode 100644 index 0000000..7b60f71 --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/register.py @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Spraay x402 Gateway tools — NeMo Agent Toolkit registration. + +Registers Spraay gateway tools using the @register_function decorator +so they can be referenced by _type in workflow config.yml files. + +Each tool is a separate registered function with its own config class, +following the NAT plugin pattern. +""" + +import logging + +from pydantic import Field + +from nat.builder.builder import Builder +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig + +from .spraay_client import SpraayClient + +logger = logging.getLogger(__name__) + + +# ── Configuration classes ──────────────────────────────────────────────────── +# Each _type in config.yml maps to a FunctionBaseConfig subclass via its name=. + + +class SpraayGatewayToolConfig(FunctionBaseConfig, name="spraay_gateway_tool"): + """Generic Spraay gateway GET endpoint tool.""" + + gateway_url: str = Field( + default="https://gateway.spraay.app", + description="Base URL of the Spraay x402 gateway", + ) + endpoint: str = Field( + description="API endpoint path (e.g., /health, /v1/routes, /v1/chains)", + ) + method: str = Field( + default="GET", + description="HTTP method (GET or POST)", + ) + + +class SpraayBalanceToolConfig(FunctionBaseConfig, name="spraay_balance_tool"): + """Tool to check wallet token balances via the Spraay gateway.""" + + gateway_url: str = Field( + default="https://gateway.spraay.app", + description="Base URL of the Spraay x402 gateway", + ) + + +class SpraayPriceToolConfig(FunctionBaseConfig, name="spraay_price_tool"): + """Tool to get token prices via the Spraay gateway.""" + + gateway_url: str = Field( + default="https://gateway.spraay.app", + description="Base URL of the Spraay x402 gateway", + ) + + +# ── Tool registrations ────────────────────────────────────────────────────── + + +@register_function(config_type=SpraayGatewayToolConfig) +async def spraay_gateway_tool(config: SpraayGatewayToolConfig, builder: Builder): + """Register a generic Spraay gateway query tool.""" + + client = SpraayClient(gateway_url=config.gateway_url) + endpoint = config.endpoint + + async def _query(query: str) -> str: + """Query the Spraay x402 gateway. + + Fetches data from the configured Spraay gateway endpoint. + This is a free query — no x402 USDC payment is required. + + Args: + query: A natural language description of what to look up + (the endpoint is pre-configured, so this is for context). + + Returns: + JSON string with the gateway response data. + """ + return await client.get(endpoint) + + yield FunctionInfo.from_fn( + _query, + description=config.description or f"Query Spraay gateway endpoint: {endpoint}", + ) + + +@register_function(config_type=SpraayBalanceToolConfig) +async def spraay_balance_tool(config: SpraayBalanceToolConfig, builder: Builder): + """Register the Spraay balance lookup tool.""" + + client = SpraayClient(gateway_url=config.gateway_url) + + async def _check_balance(query: str) -> str: + """Check the token balance of a wallet address on a specific blockchain. + + This is a free query — no x402 USDC payment is required. + + Args: + query: A string containing the wallet address and optionally + the chain and token, e.g.: + '0xAd62...c8 on base for USDC' + '0xAd62...c8' (defaults to Base/USDC) + + Returns: + JSON string with the wallet balance. + """ + # Parse the query to extract address, chain, and token + parts = query.strip().split() + address = parts[0] if parts else query.strip() + chain = "base" + token = "USDC" + + # Simple parsing: look for 'on ' and 'for ' + lower_parts = [p.lower() for p in parts] + if "on" in lower_parts: + idx = lower_parts.index("on") + if idx + 1 < len(parts): + chain = parts[idx + 1].lower() + if "for" in lower_parts: + idx = lower_parts.index("for") + if idx + 1 < len(parts): + token = parts[idx + 1].upper() + + return await client.get( + "/v1/balance", + params={"address": address, "chain": chain, "token": token}, + ) + + yield FunctionInfo.from_fn( + _check_balance, + description=config.description or "Check token balance of a wallet address on a specific chain", + ) + + +@register_function(config_type=SpraayPriceToolConfig) +async def spraay_price_tool(config: SpraayPriceToolConfig, builder: Builder): + """Register the Spraay token price lookup tool.""" + + client = SpraayClient(gateway_url=config.gateway_url) + + async def _get_price(query: str) -> str: + """Get the current price of a token on a specific blockchain. + + This is a free query — no x402 USDC payment is required. + + Args: + query: A string containing the token symbol and optionally + the chain, e.g.: + 'ETH on base' + 'USDC' (defaults to Base) + + Returns: + JSON string with the current token price. + """ + parts = query.strip().split() + token = parts[0].upper() if parts else "ETH" + chain = "base" + + lower_parts = [p.lower() for p in parts] + if "on" in lower_parts: + idx = lower_parts.index("on") + if idx + 1 < len(parts): + chain = parts[idx + 1].lower() + + return await client.get( + "/v1/price", + params={"token": token, "chain": chain}, + ) + + yield FunctionInfo.from_fn( + _get_price, + description=config.description or "Get the current price of a token on a specific chain", + ) diff --git a/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_client.py b/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_client.py new file mode 100644 index 0000000..6c63acb --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_client.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Spraay x402 Gateway HTTP client. + +Provides async HTTP methods for interacting with the Spraay x402 protocol +gateway, which enables AI agents to execute cryptocurrency payments across +15 blockchains using USDC micropayments. +""" + +import json +import logging + +import httpx + +logger = logging.getLogger(__name__) + + +class SpraayClient: + """Async HTTP client for the Spraay x402 gateway.""" + + def __init__(self, gateway_url: str, timeout: int = 30): + self.gateway_url = gateway_url.rstrip("/") + self.timeout = timeout + + async def get(self, path: str, params: dict | None = None) -> str: + """Make a GET request to the Spraay gateway. + + Args: + path: API endpoint path (e.g., '/health', '/v1/chains'). + params: Optional query parameters. + + Returns: + JSON string with the gateway response. + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get( + f"{self.gateway_url}{path}", + params=params, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + return json.dumps(response.json(), indent=2) + except httpx.HTTPStatusError as e: + logger.error("Spraay gateway HTTP error: %s %s", e.response.status_code, path) + return json.dumps({"error": f"HTTP {e.response.status_code}", "path": path}) + except Exception as e: + logger.error("Spraay gateway request failed: %s", e) + return json.dumps({"error": str(e)}) + + async def post(self, path: str, data: dict) -> str: + """Make a POST request to the Spraay gateway. + + Args: + path: API endpoint path (e.g., '/v1/batch-send'). + data: JSON body to send. + + Returns: + JSON string with the gateway response. + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.post( + f"{self.gateway_url}{path}", + json=data, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + return json.dumps(response.json(), indent=2) + except httpx.HTTPStatusError as e: + logger.error("Spraay gateway HTTP error: %s %s", e.response.status_code, path) + return json.dumps({"error": f"HTTP {e.response.status_code}", "path": path}) + except Exception as e: + logger.error("Spraay gateway request failed: %s", e) + return json.dumps({"error": str(e)})