diff --git a/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt b/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt index d910d8b..eb29f25 100644 --- a/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt +++ b/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt @@ -142,3 +142,12 @@ VectorDB XGBoost zsh Zep +Arbitrum +Bittensor +[Bb]lockchain(s?) +[Cc]rypto +[Mm]icropayment(s?) +[Rr]esend(s?) +Solana +[Ss]praay +Unichain diff --git a/examples/spraay_crypto_payments/README.md b/examples/spraay_crypto_payments/README.md new file mode 100644 index 0000000..026e3aa --- /dev/null +++ b/examples/spraay_crypto_payments/README.md @@ -0,0 +1,170 @@ + + +# 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 [NVIDIA build portal](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 + | + v +ReAct Agent (Llama 3.1) + | + v +spraay function group (shared client) + | + +-- spraay__health + +-- spraay__routes + +-- spraay__chains + +-- spraay__balance + +-- spraay__price + | + v +HTTP + x402 + | + v +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_group` | +| `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..264ab94 --- /dev/null +++ b/examples/spraay_crypto_payments/configs/config.yml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# 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. + +function_groups: + # All Spraay gateway tools share a single client (one gateway connection). + # Free query tools - no x402 payment required. + spraay: + _type: spraay + gateway_url: ${SPRAAY_GATEWAY_URL:-https://gateway.spraay.app} + +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..3bcc1c3 --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. 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..7925a2e --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/register.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Spraay x402 Gateway tools - NeMo Agent Toolkit registration. + +Registers the Spraay gateway tools as a single function group so that all +tools share one Spraay client instance. The group is referenced by _type in +the function_groups section of a workflow config.yml. +""" + +import logging + +from pydantic import Field + +from nat.builder.builder import Builder +from nat.builder.function import FunctionGroup +from nat.cli.register_workflow import register_function_group +from nat.data_models.function import FunctionGroupBaseConfig + +from .spraay_client import SpraayClient + +logger = logging.getLogger(__name__) + + +class SpraayToolsGroupConfig(FunctionGroupBaseConfig, name="spraay"): + """Configuration for the Spraay x402 gateway function group. + + All tools in this group share a single SpraayClient, pointed at the + configured gateway URL. + """ + + gateway_url: str = Field( + default="https://gateway.spraay.app", + description="Base URL of the Spraay x402 gateway", + ) + + +@register_function_group(config_type=SpraayToolsGroupConfig) +async def spraay(config: SpraayToolsGroupConfig, _builder: Builder): + """Register the Spraay gateway tools as a group sharing one client.""" + + # One shared client for every tool in the group. + client = SpraayClient(gateway_url=config.gateway_url) + + group = FunctionGroup(config=config) + + async def health(query: str = "") -> str: + """Check the health status of the Spraay x402 gateway. + + This is a free query - no x402 USDC payment is required. + + Args: + query: Unused; the endpoint is fixed. Provided for agent compatibility. + + Returns: + JSON string with the gateway health status. + """ + return await client.get("/health") + + async def routes(query: str = "") -> str: + """List all available Spraay gateway routes with pricing info. + + This is a free query - no x402 USDC payment is required. + + Args: + query: Unused; the endpoint is fixed. Provided for agent compatibility. + + Returns: + JSON string with the list of gateway routes. + """ + return await client.get("/v1/routes") + + async def chains(query: str = "") -> str: + """List all supported blockchains on the Spraay gateway. + + This is a free query - no x402 USDC payment is required. + + Args: + query: Unused; the endpoint is fixed. Provided for agent compatibility. + + Returns: + JSON string with the list of supported chains. + """ + return await client.get("/v1/chains") + + async def 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. + """ + parts = query.strip().split() + address = parts[0] if parts else query.strip() + chain = "base" + token = "USDC" + + 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 + }, + ) + + async def 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 + }, + ) + + group.add_function("health", health, description=health.__doc__) + group.add_function("routes", routes, description=routes.__doc__) + group.add_function("chains", chains, description=chains.__doc__) + group.add_function("balance", balance, description=balance.__doc__) + group.add_function("price", price, description=price.__doc__) + + yield group 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..758d90b --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_client.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""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)})