From db8354e1b0779348d99e53f944bd4e99cbf0a707 Mon Sep 17 00:00:00 2001 From: plagtech Date: Sat, 21 Mar 2026 23:42:40 -0700 Subject: [PATCH 1/2] feat: add spraay crypto payments agent example --- examples/spraay_crypto_payments/README.md | 110 +++++++++ .../spraay_crypto_payments/configs/config.yml | 63 +++++ .../spraay_crypto_payments/pyproject.toml | 18 ++ .../src/spraay_crypto_payments/__init__.py | 61 +++++ .../spraay_crypto_payments/spraay_tools.py | 233 ++++++++++++++++++ 5 files changed, 485 insertions(+) create mode 100644 examples/spraay_crypto_payments/README.md create mode 100644 examples/spraay_crypto_payments/configs/config.yml create mode 100644 examples/spraay_crypto_payments/pyproject.toml create mode 100644 examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py create mode 100644 examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py diff --git a/examples/spraay_crypto_payments/README.md b/examples/spraay_crypto_payments/README.md new file mode 100644 index 0000000..6d8a475 --- /dev/null +++ b/examples/spraay_crypto_payments/README.md @@ -0,0 +1,110 @@ +# Spraay Crypto Payments Agent + +An AI agent that executes cryptocurrency payments across 13 blockchains using the [Spraay x402 gateway](https://gateway.spraay.app). The agent can batch-send tokens, create escrow contracts, check balances, get token prices, and hire robots via the Robot Task Protocol (RTP). + +## Overview + +This example demonstrates how to build a **crypto payment 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 payment tasks 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 the payment proof, and the server executes the operation. + +### Supported Chains + +Base · Ethereum · Arbitrum · Polygon · BNB Chain · Avalanche · Solana · Bitcoin · Stacks · Unichain · Plasma · BOB · Bittensor + +## 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 venv --python 3.12 --seed .venv +source .venv/bin/activate # Linux/Mac +# .venv\Scripts\activate # Windows +uv pip install nvidia-nat httpx +``` + +3. Set environment variables: + +```bash +export NVIDIA_API_KEY= +export SPRAAY_GATEWAY_URL=https://gateway.spraay.app +``` + +## Running the Example + +### Using the CLI + +```bash +nat run --config_file configs/config.yml --input "What chains does Spraay support and what is the current price of ETH on Base?" +``` + +### Example Prompts + +- `"Check the USDC balance for address 0xAd62f03C7514bb8c51f1eA70C2b75C37404695c8 on Base"` +- `"What is the current price of ETH on Base?"` +- `"List all available Spraay gateway routes and their pricing"` +- `"Discover available robots on the RTP network"` + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ NeMo Agent Toolkit │ +│ │ +│ ┌───────────────────────────────────┐ │ +│ │ ReAct Agent (Nemotron) │ │ +│ │ │ │ +│ │ Tools: │ │ +│ │ ├── spraay_health │ │ +│ │ ├── spraay_routes │ │ +│ │ ├── spraay_chains │ │ +│ │ ├── spraay_balance │ │ +│ │ ├── spraay_price │ │ +│ │ ├── spraay_batch_send │ │ +│ │ ├── spraay_escrow_create │ │ +│ │ └── spraay_rtp_discover │ │ +│ └──────────────┬────────────────────┘ │ +│ │ │ +└─────────────────┼────────────────────────┘ + │ HTTP + x402 + ▼ + ┌────────────────────────┐ + │ Spraay x402 Gateway │ + │ gateway.spraay.app │ + │ │ + │ 76+ paid endpoints │ + │ 13 blockchains │ + │ USDC micropayments │ + └────────────────────────┘ +``` + +## Files + +| File | Description | +|------|-------------| +| `configs/config.yml` | NeMo Agent Toolkit workflow configuration | +| `src/spraay_crypto_payments/spraay_tools.py` | Custom Spraay gateway tools | +| `src/spraay_crypto_payments/__init__.py` | Package init with tool registration | +| `pyproject.toml` | Project dependencies | + +## 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/) +- [Spraay on OpenShell](https://github.com/NVIDIA/OpenShell-Community/pull/50) diff --git a/examples/spraay_crypto_payments/configs/config.yml b/examples/spraay_crypto_payments/configs/config.yml new file mode 100644 index 0000000..c16854b --- /dev/null +++ b/examples/spraay_crypto_payments/configs/config.yml @@ -0,0 +1,63 @@ +# Spraay Crypto Payments Agent +# NeMo Agent Toolkit workflow configuration +# +# This agent uses the Spraay x402 gateway to execute cryptocurrency +# payments across 13 blockchains via USDC micropayments. + +functions: + # Query tools (free endpoints — no x402 payment required) + spraay_health: + _type: spraay_health + description: "Check the health status of the Spraay x402 gateway" + + spraay_routes: + _type: spraay_routes + description: "List all available Spraay gateway routes with pricing info" + + spraay_chains: + _type: spraay_chains + description: "List all supported blockchains on the Spraay gateway" + + spraay_balance: + _type: spraay_balance + description: "Check the token balance of a wallet address on a specific chain" + + spraay_price: + _type: spraay_price + description: "Get the current price of a token on a specific chain" + + # Action tools (paid endpoints — requires x402 USDC payment) + spraay_batch_send: + _type: spraay_batch_send + description: "Send tokens to multiple recipients in a single batch transaction" + + spraay_escrow_create: + _type: spraay_escrow_create + description: "Create an escrow contract with milestone-based fund releases" + + spraay_rtp_discover: + _type: spraay_rtp_discover + description: "Discover available robots and IoT devices on the RTP network" + +llms: + nim_llm: + _type: nim + model_name: nvidia/nemotron-3-nano-30b-a3b + temperature: 0.0 + chat_template_kwargs: + enable_thinking: false + +workflow: + _type: react_agent + tool_names: + - spraay_health + - spraay_routes + - spraay_chains + - spraay_balance + - spraay_price + - spraay_batch_send + - spraay_escrow_create + - spraay_rtp_discover + 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..8dd616e --- /dev/null +++ b/examples/spraay_crypto_payments/pyproject.toml @@ -0,0 +1,18 @@ +[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.5.0", + "httpx>=0.27.0", +] + +[build-system] +requires = ["setuptools>=75.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] 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..6fc6319 --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Spraay Crypto Payments — NeMo Agent Toolkit plugin. + +Registers Spraay x402 gateway tools for use in NeMo Agent Toolkit workflows. +""" + +from nat.components.functions.tool import tool +from nat.registry import register + +from .spraay_tools import ( + spraay_balance, + spraay_batch_send, + spraay_chains, + spraay_escrow_create, + spraay_health, + spraay_price, + spraay_routes, + spraay_rtp_discover, +) + + +@register("spraay_health") +def _register_health(**kwargs): + return tool(spraay_health, description=kwargs.get("description", "")) + + +@register("spraay_routes") +def _register_routes(**kwargs): + return tool(spraay_routes, description=kwargs.get("description", "")) + + +@register("spraay_chains") +def _register_chains(**kwargs): + return tool(spraay_chains, description=kwargs.get("description", "")) + + +@register("spraay_balance") +def _register_balance(**kwargs): + return tool(spraay_balance, description=kwargs.get("description", "")) + + +@register("spraay_price") +def _register_price(**kwargs): + return tool(spraay_price, description=kwargs.get("description", "")) + + +@register("spraay_batch_send") +def _register_batch_send(**kwargs): + return tool(spraay_batch_send, description=kwargs.get("description", "")) + + +@register("spraay_escrow_create") +def _register_escrow_create(**kwargs): + return tool(spraay_escrow_create, description=kwargs.get("description", "")) + + +@register("spraay_rtp_discover") +def _register_rtp_discover(**kwargs): + return tool(spraay_rtp_discover, description=kwargs.get("description", "")) diff --git a/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py b/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py new file mode 100644 index 0000000..4228afa --- /dev/null +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py @@ -0,0 +1,233 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Spraay x402 Gateway tools for NeMo Agent Toolkit. + +These tools enable AI agents to execute cryptocurrency payments across +13 blockchains using the Spraay x402 protocol gateway. +""" + +import json +import logging +import os + +import httpx + +logger = logging.getLogger(__name__) + +GATEWAY_URL = os.environ.get("SPRAAY_GATEWAY_URL", "https://gateway.spraay.app") + + +async def _gateway_get(path: str, params: dict | None = None) -> dict: + """Make a GET request to the Spraay gateway.""" + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{GATEWAY_URL}{path}", + params=params, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + return response.json() + + +async def _gateway_post(path: str, data: dict) -> dict: + """Make a POST request to the Spraay gateway.""" + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{GATEWAY_URL}{path}", + json=data, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + return response.json() + + +# ── Free query tools (no x402 payment required) ───────────────────────────── + + +async def spraay_health() -> str: + """Check the health status of the Spraay x402 gateway. + + Returns: + JSON string with gateway health status. + """ + try: + result = await _gateway_get("/health") + return json.dumps(result, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) + + +async def spraay_routes() -> str: + """List all available Spraay gateway routes with pricing information. + + Returns: + JSON string with all available routes and their x402 pricing. + """ + try: + result = await _gateway_get("/v1/routes") + return json.dumps(result, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) + + +async def spraay_chains() -> str: + """List all supported blockchains on the Spraay gateway. + + Returns: + JSON string with supported chains (Base, Ethereum, Arbitrum, + Polygon, BNB, Avalanche, Solana, Bitcoin, Stacks, Unichain, + Plasma, BOB, Bittensor). + """ + try: + result = await _gateway_get("/v1/chains") + return json.dumps(result, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) + + +async def spraay_balance(address: str, chain: str = "base", token: str = "USDC") -> str: + """Check the token balance of a wallet address on a specific chain. + + Args: + address: The wallet address to check (e.g., 0x1234...). + chain: The blockchain to query (default: base). + token: The token symbol (default: USDC). + + Returns: + JSON string with the wallet balance. + """ + try: + result = await _gateway_get( + "/v1/balance", + params={"address": address, "chain": chain, "token": token}, + ) + return json.dumps(result, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) + + +async def spraay_price(token: str, chain: str = "base") -> str: + """Get the current price of a token on a specific chain. + + Args: + token: The token symbol (e.g., ETH, USDC, MATIC). + chain: The blockchain to query (default: base). + + Returns: + JSON string with the current token price. + """ + try: + result = await _gateway_get( + "/v1/price", + params={"token": token, "chain": chain}, + ) + return json.dumps(result, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) + + +# ── Paid action tools (x402 USDC micropayment required) ───────────────────── + + +async def spraay_batch_send( + recipients: str, + token: str = "USDC", + chain: str = "base", +) -> str: + """Send tokens to multiple recipients in a single batch transaction. + + This is a PAID endpoint — requires x402 USDC micropayment. + + Args: + recipients: JSON string of recipients array, each with + 'address' and 'amount' fields. + Example: '[{"address": "0x...", "amount": "10.0"}]' + token: The token to send (default: USDC). + chain: The blockchain to use (default: base). + + Returns: + JSON string with the transaction result including tx hash. + """ + try: + recipients_list = json.loads(recipients) + result = await _gateway_post( + "/v1/batch-send", + data={ + "recipients": recipients_list, + "token": token, + "chain": chain, + }, + ) + return json.dumps(result, indent=2) + except json.JSONDecodeError: + return json.dumps({"error": "Invalid recipients JSON format"}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +async def spraay_escrow_create( + depositor: str, + beneficiary: str, + total_amount: str, + milestones: str, + token: str = "USDC", + chain: str = "base", +) -> str: + """Create an escrow contract with milestone-based fund releases. + + This is a PAID endpoint — requires x402 USDC micropayment. + + Args: + depositor: Wallet address of the person depositing funds. + beneficiary: Wallet address of the person receiving funds. + total_amount: Total amount to escrow (e.g., "500.0"). + milestones: JSON string of milestones array, each with + 'description' and 'amount' fields. + token: The token to escrow (default: USDC). + chain: The blockchain to use (default: base). + + Returns: + JSON string with the escrow contract details. + """ + try: + milestones_list = json.loads(milestones) + result = await _gateway_post( + "/v1/escrow/create", + data={ + "depositor": depositor, + "beneficiary": beneficiary, + "totalAmount": total_amount, + "milestones": milestones_list, + "token": token, + "chain": chain, + }, + ) + return json.dumps(result, indent=2) + except json.JSONDecodeError: + return json.dumps({"error": "Invalid milestones JSON format"}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +async def spraay_rtp_discover(category: str = "") -> str: + """Discover available robots and IoT devices on the RTP network. + + The Robot Task Protocol (RTP) enables AI agents to hire robots + and physical devices via x402 USDC micropayments. + + Args: + category: Optional category filter (e.g., 'robotics', + 'sensing', 'delivery', 'manufacturing', 'compute'). + + Returns: + JSON string with available devices and their capabilities. + """ + try: + params = {} + if category: + params["category"] = category + result = await _gateway_get("/v1/rtp/discover", params=params) + return json.dumps(result, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) From 39dabb7c76c22c0627e76887eeb9afee337a8bf3 Mon Sep 17 00:00:00 2001 From: plagtech Date: Fri, 3 Apr 2026 08:43:19 -0700 Subject: [PATCH 2/2] fix: rewrite to use proper NAT register_function API --- examples/spraay_crypto_payments/README.md | 108 +++++--- .../spraay_crypto_payments/configs/config.yml | 45 ++-- .../spraay_crypto_payments/pyproject.toml | 8 +- .../src/spraay_crypto_payments/__init__.py | 61 +---- .../src/spraay_crypto_payments/register.py | 182 ++++++++++++++ .../spraay_crypto_payments/spraay_client.py | 76 ++++++ .../spraay_crypto_payments/spraay_tools.py | 233 ------------------ 7 files changed, 362 insertions(+), 351 deletions(-) create mode 100644 examples/spraay_crypto_payments/src/spraay_crypto_payments/register.py create mode 100644 examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_client.py delete mode 100644 examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py diff --git a/examples/spraay_crypto_payments/README.md b/examples/spraay_crypto_payments/README.md index 6d8a475..f0a8d6f 100644 --- a/examples/spraay_crypto_payments/README.md +++ b/examples/spraay_crypto_payments/README.md @@ -1,18 +1,29 @@ # Spraay Crypto Payments Agent -An AI agent that executes cryptocurrency payments across 13 blockchains using the [Spraay x402 gateway](https://gateway.spraay.app). The agent can batch-send tokens, create escrow contracts, check balances, get token prices, and hire robots via the Robot Task Protocol (RTP). +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 payment 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 payment tasks and execute them via HTTP API calls. +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 the payment proof, and the server executes the operation. +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 +Base · Ethereum · Arbitrum · Polygon · BNB Chain · Avalanche · Solana · +Bitcoin · Stacks · Unichain · Plasma · BOB · Bittensor · Stellar · XRP Ledger ## Prerequisites @@ -31,33 +42,71 @@ cd examples/spraay_crypto_payments 2. Install dependencies: ```bash -uv venv --python 3.12 --seed .venv -source .venv/bin/activate # Linux/Mac -# .venv\Scripts\activate # Windows -uv pip install nvidia-nat httpx +uv pip install -e . ``` 3. Set environment variables: ```bash export NVIDIA_API_KEY= -export SPRAAY_GATEWAY_URL=https://gateway.spraay.app +export SPRAAY_GATEWAY_URL=https://gateway.spraay.app # optional, this is the default ``` ## Running the Example -### Using the CLI +### Check gateway health ```bash -nat run --config_file configs/config.yml --input "What chains does Spraay support and what is the current price of ETH on Base?" +nat run \ + --config_file configs/config.yml \ + --input "Is the Spraay gateway healthy?" ``` -### Example Prompts +### List supported chains -- `"Check the USDC balance for address 0xAd62f03C7514bb8c51f1eA70C2b75C37404695c8 on Base"` -- `"What is the current price of ETH on Base?"` -- `"List all available Spraay gateway routes and their pricing"` -- `"Discover available robots on the RTP network"` +```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 @@ -66,17 +115,14 @@ nat run --config_file configs/config.yml --input "What chains does Spraay suppor │ NeMo Agent Toolkit │ │ │ │ ┌───────────────────────────────────┐ │ -│ │ ReAct Agent (Nemotron) │ │ +│ │ ReAct Agent (Llama 3.1) │ │ │ │ │ │ │ │ Tools: │ │ -│ │ ├── spraay_health │ │ -│ │ ├── spraay_routes │ │ -│ │ ├── spraay_chains │ │ -│ │ ├── spraay_balance │ │ -│ │ ├── spraay_price │ │ -│ │ ├── spraay_batch_send │ │ -│ │ ├── spraay_escrow_create │ │ -│ │ └── spraay_rtp_discover │ │ +│ │ ├── spraay_health │ │ +│ │ ├── spraay_routes │ │ +│ │ ├── spraay_chains │ │ +│ │ ├── spraay_balance │ │ +│ │ └── spraay_price │ │ │ └──────────────┬────────────────────┘ │ │ │ │ └─────────────────┼────────────────────────┘ @@ -86,8 +132,8 @@ nat run --config_file configs/config.yml --input "What chains does Spraay suppor │ Spraay x402 Gateway │ │ gateway.spraay.app │ │ │ - │ 76+ paid endpoints │ - │ 13 blockchains │ + │ 84+ paid endpoints │ + │ 15 blockchains │ │ USDC micropayments │ └────────────────────────┘ ``` @@ -97,9 +143,10 @@ nat run --config_file configs/config.yml --input "What chains does Spraay suppor | File | Description | |------|-------------| | `configs/config.yml` | NeMo Agent Toolkit workflow configuration | -| `src/spraay_crypto_payments/spraay_tools.py` | Custom Spraay gateway tools | -| `src/spraay_crypto_payments/__init__.py` | Package init with tool registration | -| `pyproject.toml` | Project dependencies | +| `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 @@ -107,4 +154,3 @@ nat run --config_file configs/config.yml --input "What chains does Spraay suppor - [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/) -- [Spraay on OpenShell](https://github.com/NVIDIA/OpenShell-Community/pull/50) diff --git a/examples/spraay_crypto_payments/configs/config.yml b/examples/spraay_crypto_payments/configs/config.yml index c16854b..46fbf84 100644 --- a/examples/spraay_crypto_payments/configs/config.yml +++ b/examples/spraay_crypto_payments/configs/config.yml @@ -2,50 +2,46 @@ # NeMo Agent Toolkit workflow configuration # # This agent uses the Spraay x402 gateway to execute cryptocurrency -# payments across 13 blockchains via USDC micropayments. +# payments across 15 blockchains via USDC micropayments. functions: - # Query tools (free endpoints — no x402 payment required) + # Free query tools (no x402 payment required) spraay_health: - _type: 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_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_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 + _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 + _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" - # Action tools (paid endpoints — requires x402 USDC payment) - spraay_batch_send: - _type: spraay_batch_send - description: "Send tokens to multiple recipients in a single batch transaction" - - spraay_escrow_create: - _type: spraay_escrow_create - description: "Create an escrow contract with milestone-based fund releases" - - spraay_rtp_discover: - _type: spraay_rtp_discover - description: "Discover available robots and IoT devices on the RTP network" - llms: nim_llm: _type: nim - model_name: nvidia/nemotron-3-nano-30b-a3b + model_name: meta/llama-3.1-70b-instruct temperature: 0.0 - chat_template_kwargs: - enable_thinking: false workflow: _type: react_agent @@ -55,9 +51,6 @@ workflow: - spraay_chains - spraay_balance - spraay_price - - spraay_batch_send - - spraay_escrow_create - - spraay_rtp_discover 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 index 8dd616e..24e730e 100644 --- a/examples/spraay_crypto_payments/pyproject.toml +++ b/examples/spraay_crypto_payments/pyproject.toml @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + [project] name = "spraay-crypto-payments" version = "0.1.0" @@ -6,7 +9,7 @@ readme = "README.md" license = { text = "Apache-2.0" } requires-python = ">=3.11,<3.14" dependencies = [ - "nvidia-nat>=1.5.0", + "nvidia-nat>=1.3.0", "httpx>=0.27.0", ] @@ -16,3 +19,6 @@ 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 index 6fc6319..a9c2f92 100644 --- a/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py +++ b/examples/spraay_crypto_payments/src/spraay_crypto_payments/__init__.py @@ -1,61 +1,2 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. # SPDX-License-Identifier: Apache-2.0 - -"""Spraay Crypto Payments — NeMo Agent Toolkit plugin. - -Registers Spraay x402 gateway tools for use in NeMo Agent Toolkit workflows. -""" - -from nat.components.functions.tool import tool -from nat.registry import register - -from .spraay_tools import ( - spraay_balance, - spraay_batch_send, - spraay_chains, - spraay_escrow_create, - spraay_health, - spraay_price, - spraay_routes, - spraay_rtp_discover, -) - - -@register("spraay_health") -def _register_health(**kwargs): - return tool(spraay_health, description=kwargs.get("description", "")) - - -@register("spraay_routes") -def _register_routes(**kwargs): - return tool(spraay_routes, description=kwargs.get("description", "")) - - -@register("spraay_chains") -def _register_chains(**kwargs): - return tool(spraay_chains, description=kwargs.get("description", "")) - - -@register("spraay_balance") -def _register_balance(**kwargs): - return tool(spraay_balance, description=kwargs.get("description", "")) - - -@register("spraay_price") -def _register_price(**kwargs): - return tool(spraay_price, description=kwargs.get("description", "")) - - -@register("spraay_batch_send") -def _register_batch_send(**kwargs): - return tool(spraay_batch_send, description=kwargs.get("description", "")) - - -@register("spraay_escrow_create") -def _register_escrow_create(**kwargs): - return tool(spraay_escrow_create, description=kwargs.get("description", "")) - - -@register("spraay_rtp_discover") -def _register_rtp_discover(**kwargs): - return tool(spraay_rtp_discover, description=kwargs.get("description", "")) 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)}) diff --git a/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py b/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py deleted file mode 100644 index 4228afa..0000000 --- a/examples/spraay_crypto_payments/src/spraay_crypto_payments/spraay_tools.py +++ /dev/null @@ -1,233 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. -# SPDX-License-Identifier: Apache-2.0 - -"""Spraay x402 Gateway tools for NeMo Agent Toolkit. - -These tools enable AI agents to execute cryptocurrency payments across -13 blockchains using the Spraay x402 protocol gateway. -""" - -import json -import logging -import os - -import httpx - -logger = logging.getLogger(__name__) - -GATEWAY_URL = os.environ.get("SPRAAY_GATEWAY_URL", "https://gateway.spraay.app") - - -async def _gateway_get(path: str, params: dict | None = None) -> dict: - """Make a GET request to the Spraay gateway.""" - async with httpx.AsyncClient(timeout=30.0) as client: - response = await client.get( - f"{GATEWAY_URL}{path}", - params=params, - headers={"Content-Type": "application/json"}, - ) - response.raise_for_status() - return response.json() - - -async def _gateway_post(path: str, data: dict) -> dict: - """Make a POST request to the Spraay gateway.""" - async with httpx.AsyncClient(timeout=30.0) as client: - response = await client.post( - f"{GATEWAY_URL}{path}", - json=data, - headers={"Content-Type": "application/json"}, - ) - response.raise_for_status() - return response.json() - - -# ── Free query tools (no x402 payment required) ───────────────────────────── - - -async def spraay_health() -> str: - """Check the health status of the Spraay x402 gateway. - - Returns: - JSON string with gateway health status. - """ - try: - result = await _gateway_get("/health") - return json.dumps(result, indent=2) - except Exception as e: - return json.dumps({"error": str(e)}) - - -async def spraay_routes() -> str: - """List all available Spraay gateway routes with pricing information. - - Returns: - JSON string with all available routes and their x402 pricing. - """ - try: - result = await _gateway_get("/v1/routes") - return json.dumps(result, indent=2) - except Exception as e: - return json.dumps({"error": str(e)}) - - -async def spraay_chains() -> str: - """List all supported blockchains on the Spraay gateway. - - Returns: - JSON string with supported chains (Base, Ethereum, Arbitrum, - Polygon, BNB, Avalanche, Solana, Bitcoin, Stacks, Unichain, - Plasma, BOB, Bittensor). - """ - try: - result = await _gateway_get("/v1/chains") - return json.dumps(result, indent=2) - except Exception as e: - return json.dumps({"error": str(e)}) - - -async def spraay_balance(address: str, chain: str = "base", token: str = "USDC") -> str: - """Check the token balance of a wallet address on a specific chain. - - Args: - address: The wallet address to check (e.g., 0x1234...). - chain: The blockchain to query (default: base). - token: The token symbol (default: USDC). - - Returns: - JSON string with the wallet balance. - """ - try: - result = await _gateway_get( - "/v1/balance", - params={"address": address, "chain": chain, "token": token}, - ) - return json.dumps(result, indent=2) - except Exception as e: - return json.dumps({"error": str(e)}) - - -async def spraay_price(token: str, chain: str = "base") -> str: - """Get the current price of a token on a specific chain. - - Args: - token: The token symbol (e.g., ETH, USDC, MATIC). - chain: The blockchain to query (default: base). - - Returns: - JSON string with the current token price. - """ - try: - result = await _gateway_get( - "/v1/price", - params={"token": token, "chain": chain}, - ) - return json.dumps(result, indent=2) - except Exception as e: - return json.dumps({"error": str(e)}) - - -# ── Paid action tools (x402 USDC micropayment required) ───────────────────── - - -async def spraay_batch_send( - recipients: str, - token: str = "USDC", - chain: str = "base", -) -> str: - """Send tokens to multiple recipients in a single batch transaction. - - This is a PAID endpoint — requires x402 USDC micropayment. - - Args: - recipients: JSON string of recipients array, each with - 'address' and 'amount' fields. - Example: '[{"address": "0x...", "amount": "10.0"}]' - token: The token to send (default: USDC). - chain: The blockchain to use (default: base). - - Returns: - JSON string with the transaction result including tx hash. - """ - try: - recipients_list = json.loads(recipients) - result = await _gateway_post( - "/v1/batch-send", - data={ - "recipients": recipients_list, - "token": token, - "chain": chain, - }, - ) - return json.dumps(result, indent=2) - except json.JSONDecodeError: - return json.dumps({"error": "Invalid recipients JSON format"}) - except Exception as e: - return json.dumps({"error": str(e)}) - - -async def spraay_escrow_create( - depositor: str, - beneficiary: str, - total_amount: str, - milestones: str, - token: str = "USDC", - chain: str = "base", -) -> str: - """Create an escrow contract with milestone-based fund releases. - - This is a PAID endpoint — requires x402 USDC micropayment. - - Args: - depositor: Wallet address of the person depositing funds. - beneficiary: Wallet address of the person receiving funds. - total_amount: Total amount to escrow (e.g., "500.0"). - milestones: JSON string of milestones array, each with - 'description' and 'amount' fields. - token: The token to escrow (default: USDC). - chain: The blockchain to use (default: base). - - Returns: - JSON string with the escrow contract details. - """ - try: - milestones_list = json.loads(milestones) - result = await _gateway_post( - "/v1/escrow/create", - data={ - "depositor": depositor, - "beneficiary": beneficiary, - "totalAmount": total_amount, - "milestones": milestones_list, - "token": token, - "chain": chain, - }, - ) - return json.dumps(result, indent=2) - except json.JSONDecodeError: - return json.dumps({"error": "Invalid milestones JSON format"}) - except Exception as e: - return json.dumps({"error": str(e)}) - - -async def spraay_rtp_discover(category: str = "") -> str: - """Discover available robots and IoT devices on the RTP network. - - The Robot Task Protocol (RTP) enables AI agents to hire robots - and physical devices via x402 USDC micropayments. - - Args: - category: Optional category filter (e.g., 'robotics', - 'sensing', 'delivery', 'manufacturing', 'compute'). - - Returns: - JSON string with available devices and their capabilities. - """ - try: - params = {} - if category: - params["category"] = category - result = await _gateway_get("/v1/rtp/discover", params=params) - return json.dumps(result, indent=2) - except Exception as e: - return json.dumps({"error": str(e)})