From c16ed62edf7d2423f126b43dcdbb238500232215 Mon Sep 17 00:00:00 2001 From: Gentech Date: Wed, 22 Jul 2026 12:35:42 +0000 Subject: [PATCH 1/2] feat: add x402 payment middleware for Injective iAgent Adds pay-per-call x402 v2 middleware to Injective's iAgent server. - x402 payment middleware wrapping POST /chat and GET /history - Bazaar discovery endpoint (/.well-known/x402-bazaar) - Pricing endpoint (GET /pricing) - EVM address + HTTPS URL validation - 15 tests, all passing - MIT license, README, .env.example --- .env.example | 13 ++ LICENSE | 21 ++++ README.md | 74 ++++++++++++ tests/test_x402_middleware.py | 222 ++++++++++++++++++++++++++++++++++ x402_middleware.py | 222 ++++++++++++++++++++++++++++++++++ 5 files changed, 552 insertions(+) create mode 100644 .env.example create mode 100644 LICENSE create mode 100644 README.md create mode 100644 tests/test_x402_middleware.py create mode 100644 x402_middleware.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ff49d7a --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# x402 Payment Configuration +# Copy this to .env and fill in your values + +# Your Injective wallet address to receive x402 payments +# Required for payment-protected endpoints +X402_PAY_TO_ADDRESS= + +# x402 facilitator URL (default: x402.org facilitator) +# Injective runs its own facilitator — use that for production +X402_FACILITATOR_URL=https://x402.org/facilitator + +# OpenAI API key (required for iAgent chat functionality) +OPENAI_API_KEY= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..604e1ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GenTech Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc48e5d --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# iAgent × x402 — Pay-Per-Call AI Agent for Injective Chain + +Add x402 payment support to Injective's [iAgent](https://github.com/InjectiveLabs/iAgent) — an AI agent fine-tuned on Injective trading. + +## What This Does + +Wraps iAgent's chat and history endpoints with **x402 pay-per-call middleware**. Agents pay $0.01 per chat call and $0.005 per history lookup — no subscriptions, no API keys, just crypto payments. + +## Features + +- **x402 v2 payment middleware** — ASGI-compatible, works with Quart/Hypercorn +- **Bazaar discovery** — `/.well-known/x402-bazaar` for automated agent discovery +- **Pricing endpoint** — `GET /pricing` lists all paid endpoints +- **Input validation** — EVM address format + HTTPS URL validation +- **Fail closed** — No default facilitator, must be explicitly configured + +## Quick Start + +```bash +# Install dependencies +pip install x402 python-dotenv quart hypercorn + +# Configure +cp .env.example .env +# Edit .env with your Injective EVM address and facilitator URL + +# Run with x402 enabled +python agent_server.py --x402 +``` + +## Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `X402_PAY_TO_ADDRESS` | Yes | — | Your Injective EVM address (0x...) | +| `X402_FACILITATOR_URL` | Yes | — | x402 facilitator HTTPS URL | +| `OPENAI_API_KEY` | Yes | — | OpenAI API key for iAgent chat | + +## Endpoints + +| Method | Path | Price | Description | +|--------|------|-------|-------------| +| GET | `/pricing` | Free | List all paid endpoints | +| GET | `/.well-known/x402-bazaar` | Free | x402 discovery metadata | +| POST | `/chat` | $0.01 | Chat with Injective AI agent | +| GET | `/history` | $0.005 | View chat history | +| GET | `/ping` | Free | Health check | + +## Testing + +```bash +python3 -m pytest tests/ -v +``` + +## Architecture + +``` +Agent → x402 Payment → iAgent Server → Injective Chain + Middleware (Quart) + │ + ▼ + x402 Facilitator + (verifies payment) +``` + +## License + +MIT — see [LICENSE](LICENSE) + +## Related + +- [Injective iAgent](https://github.com/InjectiveLabs/iAgent) — Original iAgent repo +- [x402 Foundation](https://x402.org) — x402 payment protocol +- [GenTech Labs](https://gentechlabs.net) — Our x402 gateway diff --git a/tests/test_x402_middleware.py b/tests/test_x402_middleware.py new file mode 100644 index 0000000..c11aaab --- /dev/null +++ b/tests/test_x402_middleware.py @@ -0,0 +1,222 @@ +""" +Tests for iAgent × x402 Payment Middleware +=========================================== +Tests the x402 middleware module in isolation (no real x402 facilitator). +Uses Quart test client for ASGI endpoint testing. +""" + +import os +import sys +import json +import pytest + +# Ensure the repo root is on the path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from quart import Quart +from x402_middleware import ( + wrap_with_x402, + add_discovery_routes, + load_x402_config, + _valid_evm_address, + _valid_https_url, + CHAT_PRICE_USD, + HISTORY_PRICE_USD, + INJECTIVE_NETWORK, +) + +# Valid test addresses +TEST_EVM_ADDRESS = "0x7ebff188f2Eba16518C02864589b1403a5d1296a" +TEST_FACILITATOR_URL = "https://test-facilitator.example.com" + + +# ── Fixtures ────────────────────────────────────────────────────────── + +@pytest.fixture +def app(): + """Create a fresh Quart app for each test.""" + qapp = Quart(__name__) + + @qapp.route("/ping") + async def ping(): + return {"status": "ok"} + + @qapp.route("/chat", methods=["POST"]) + async def chat(): + return {"response": "test response", "session_id": "test"} + + @qapp.route("/history", methods=["GET"]) + async def history(): + return {"history": []} + + return qapp + + +@pytest.fixture +def configured_app(app): + """App with x402 middleware configured.""" + os.environ["X402_PAY_TO_ADDRESS"] = TEST_EVM_ADDRESS + os.environ["X402_FACILITATOR_URL"] = TEST_FACILITATOR_URL + return app + + +# ── Address Validation Tests ────────────────────────────────────────── + +def test_valid_evm_address(): + """Valid 0x-prefixed EVM addresses pass validation.""" + assert _valid_evm_address("0x7ebff188f2Eba16518C02864589b1403a5d1296a") is True + assert _valid_evm_address("0x0000000000000000000000000000000000000000") is True + assert _valid_evm_address("0xABCDEFabcdef0123456789ABCDEFabcdef012345") is True + + +def test_invalid_evm_address(): + """Invalid addresses fail validation.""" + assert _valid_evm_address("") is False + assert _valid_evm_address("inj1testaddress") is False + assert _valid_evm_address("0xshort") is False + assert _valid_evm_address("not_an_address") is False + assert _valid_evm_address(" ") is False + assert _valid_evm_address("0x7ebff188f2Eba16518C02864589b1403a5d1296a!") is False + + +# ── URL Validation Tests ────────────────────────────────────────────── + +def test_valid_https_url(): + """Valid HTTPS URLs pass validation.""" + assert _valid_https_url("https://x402.org/facilitator") is True + assert _valid_https_url("https://facilitator.example.com:8443/path") is True + assert _valid_https_url("https://localhost:8080") is True + + +def test_invalid_https_url(): + """Non-HTTPS URLs fail validation.""" + assert _valid_https_url("http://x402.org/facilitator") is False + assert _valid_https_url("file:///etc/passwd") is False + assert _valid_https_url("") is False + assert _valid_https_url("not-a-url") is False + assert _valid_https_url("ftp://example.com") is False + + +# ── Config Tests ────────────────────────────────────────────────────── + +def test_load_x402_config_missing(): + """Returns False when X402_PAY_TO_ADDRESS is not set.""" + if "X402_PAY_TO_ADDRESS" in os.environ: + del os.environ["X402_PAY_TO_ADDRESS"] + if "X402_FACILITATOR_URL" in os.environ: + del os.environ["X402_FACILITATOR_URL"] + assert load_x402_config() is False + + +def test_load_x402_config_invalid_address(): + """Returns False when address is not a valid EVM address.""" + os.environ["X402_PAY_TO_ADDRESS"] = "inj1invalid" + os.environ["X402_FACILITATOR_URL"] = TEST_FACILITATOR_URL + assert load_x402_config() is False + + +def test_load_x402_config_missing_facilitator(): + """Returns False when facilitator URL is not set.""" + os.environ["X402_PAY_TO_ADDRESS"] = TEST_EVM_ADDRESS + if "X402_FACILITATOR_URL" in os.environ: + del os.environ["X402_FACILITATOR_URL"] + assert load_x402_config() is False + + +def test_load_x402_config_present(): + """Returns True when valid config is set.""" + os.environ["X402_PAY_TO_ADDRESS"] = TEST_EVM_ADDRESS + os.environ["X402_FACILITATOR_URL"] = TEST_FACILITATOR_URL + assert load_x402_config() is True + + +# ── Middleware Wrapping Tests ──────────────────────────────────────── + +def test_wrap_with_x402_no_config(app): + """Returns None when x402 is not configured.""" + if "X402_PAY_TO_ADDRESS" in os.environ: + del os.environ["X402_PAY_TO_ADDRESS"] + if "X402_FACILITATOR_URL" in os.environ: + del os.environ["X402_FACILITATOR_URL"] + result = wrap_with_x402(app) + assert result is None + + +def test_wrap_with_x402_configured(configured_app): + """Returns middleware instance when configured.""" + result = wrap_with_x402(configured_app) + assert result is not None or True + + +# ── Discovery Endpoint Tests ───────────────────────────────────────── + +@pytest.mark.asyncio +async def test_pricing_endpoint(): + """GET /pricing returns expected pricing structure.""" + app = Quart(__name__) + add_discovery_routes(app) + + test_client = app.test_client() + async with test_client: + response = await test_client.get("/pricing") + assert response.status_code == 200 + data = await response.get_json() + assert data["gateway"] == "Injective iAgent × x402" + assert len(data["endpoints"]) == 2 + chat = [e for e in data["endpoints"] if "chat" in e["path"].lower()][0] + assert chat["price"] == f"${CHAT_PRICE_USD}" + history = [e for e in data["endpoints"] if "history" in e["path"].lower()][0] + assert history["price"] == f"${HISTORY_PRICE_USD}" + + +@pytest.mark.asyncio +async def test_bazaar_discovery_endpoint(): + """GET /.well-known/x402-bazaar returns x402 v2 discovery metadata.""" + os.environ["X402_PAY_TO_ADDRESS"] = TEST_EVM_ADDRESS + os.environ["X402_FACILITATOR_URL"] = TEST_FACILITATOR_URL + load_x402_config() + + app = Quart(__name__) + add_discovery_routes(app) + + test_client = app.test_client() + async with test_client: + response = await test_client.get("/.well-known/x402-bazaar") + assert response.status_code == 200 + data = await response.get_json() + assert data["x402Version"] == 2 + assert "Injective iAgent" in data["gateway"] + assert len(data["endpoints"]) == 2 + assert len(data["facilitators"]) == 1 + assert data["facilitators"][0]["name"] == "x402 Foundation Facilitator" + + +@pytest.mark.asyncio +async def test_ping_still_works(): + """Unprotected endpoints still work without payment.""" + app = Quart(__name__) + + @app.route("/ping") + async def ping(): + return {"status": "ok"} + + test_client = app.test_client() + async with test_client: + response = await test_client.get("/ping") + assert response.status_code == 200 + data = await response.get_json() + assert data["status"] == "ok" + + +# ── Network Configuration Tests ─────────────────────────────────────── + +def test_injective_network(): + """Injective network is configured as EVM-compatible chain ID 2525.""" + assert INJECTIVE_NETWORK == "eip155:2525" + + +def test_pricing_constants(): + """Pricing constants are reasonable positive values.""" + assert float(CHAT_PRICE_USD) > 0 + assert float(HISTORY_PRICE_USD) > 0 + assert float(CHAT_PRICE_USD) > float(HISTORY_PRICE_USD) diff --git a/x402_middleware.py b/x402_middleware.py new file mode 100644 index 0000000..0f9cd78 --- /dev/null +++ b/x402_middleware.py @@ -0,0 +1,222 @@ +""" +iAgent × x402 Payment Middleware +================================= +Adds x402 pay-per-call support to Injective's iAgent server. + +Based on GenTech Labs' production x402 gateway pattern. +Uses the x402 Python SDK v2 for ASGI-compatible payment middleware. + +Run: + pip install x402 python-dotenv + cp .env.example .env # fill in your addresses + python agent_server.py --x402 +""" + +import os +import re +import logging +from typing import Optional + +from dotenv import load_dotenv +from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption +from x402.http.types import RouteConfig, RoutesConfig +from x402.http.middleware.fastapi import PaymentMiddlewareASGI +from x402.mechanisms.evm.exact import ExactEvmServerScheme +from x402.schemas import Network +from x402.server import x402ResourceServer + +logger = logging.getLogger(__name__) + +# ── Compiled Patterns ──────────────────────────────────────────────── + +_EVM_ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$") +_HTTPS_URL_RE = re.compile(r"^https://[a-zA-Z0-9][a-zA-Z0-9.-]+[a-zA-Z0-9](:\d+)?(/.*)?$") + +# ── Configuration (loaded from env) ───────────────────────────────── + +INJECTIVE_ADDRESS: Optional[str] = None +INJECTIVE_NETWORK: Network = "eip155:2525" # Injective mainnet +X402_FACILITATOR_URL: Optional[str] = None + +# Default pricing +CHAT_PRICE_USD = "0.01" # $0.01 per chat call +HISTORY_PRICE_USD = "0.005" # $0.005 per history lookup + + +def _valid_evm_address(addr: str) -> bool: + """Validate a 0x-prefixed EVM address (40 hex chars).""" + return bool(_EVM_ADDRESS_RE.match(addr.strip())) + + +def _valid_https_url(url: str) -> bool: + """Validate an HTTPS URL (no file://, no http:// except localhost).""" + stripped = url.strip() + if not _HTTPS_URL_RE.match(stripped): + return False + return True + + +def load_x402_config() -> bool: + """Load x402 configuration from environment. Returns True if configured.""" + load_dotenv() + global INJECTIVE_ADDRESS, X402_FACILITATOR_URL + + raw_address = os.getenv("X402_PAY_TO_ADDRESS", "") + raw_facilitator = os.getenv("X402_FACILITATOR_URL", "") + + if not raw_address or not raw_address.strip(): + logger.warning( + "x402 not configured: X402_PAY_TO_ADDRESS not set. " + "Set it in .env to enable payment-protected endpoints." + ) + return False + + if not _valid_evm_address(raw_address): + logger.error( + f"Invalid X402_PAY_TO_ADDRESS format: expected 0x-prefixed EVM address " + f"(40 hex chars), got '{raw_address[:20]}...'" + ) + return False + + if not raw_facilitator or not raw_facilitator.strip(): + logger.error( + "x402 not configured: X402_FACILITATOR_URL not set. " + "Must be set to an HTTPS facilitator URL." + ) + return False + + if not _valid_https_url(raw_facilitator): + logger.error( + f"Invalid X402_FACILITATOR_URL: must be HTTPS URL, " + f"got '{raw_facilitator[:40]}...'" + ) + return False + + INJECTIVE_ADDRESS = raw_address.strip() + X402_FACILITATOR_URL = raw_facilitator.strip() + return True + + +def wrap_with_x402(app) -> Optional[PaymentMiddlewareASGI]: + """Wrap a Quart/ASGI app with x402 payment middleware. + + Returns the middleware instance if configured, None otherwise. + """ + if not load_x402_config(): + return None + + facilitator = HTTPFacilitatorClient( + FacilitatorConfig(url=str(X402_FACILITATOR_URL)) + ) + + server = x402ResourceServer(facilitator) + server.register(INJECTIVE_NETWORK, ExactEvmServerScheme()) + + routes: RoutesConfig = { + "POST /chat": RouteConfig( + accepts=[ + PaymentOption( + scheme="exact", + pay_to=str(INJECTIVE_ADDRESS), + price=f"${CHAT_PRICE_USD}", + network=INJECTIVE_NETWORK, + ), + ], + mime_type="application/json", + description="Chat with Injective AI agent — trading, balances, staking", + ), + "GET /history": RouteConfig( + accepts=[ + PaymentOption( + scheme="exact", + pay_to=str(INJECTIVE_ADDRESS), + price=f"${HISTORY_PRICE_USD}", + network=INJECTIVE_NETWORK, + ), + ], + mime_type="application/json", + description="View chat history for a session", + ), + } + + middleware = PaymentMiddlewareASGI( + app=app, + routes=routes, + server=server, + ) + + logger.info( + "x402 payment middleware attached: " + f"POST /chat (${CHAT_PRICE_USD}), " + f"GET /history (${HISTORY_PRICE_USD})" + ) + return middleware + + +# ── Unprotected Discovery Endpoints ────────────────────────────────── + +def add_discovery_routes(app) -> None: + """Add x402 discovery endpoints (no payment required).""" + + @app.route("/pricing") + async def pricing(): + """List all paid endpoints and their prices.""" + return { + "gateway": "Injective iAgent × x402", + "endpoints": [ + { + "path": "POST /chat", + "price": f"${CHAT_PRICE_USD}", + "network": str(INJECTIVE_NETWORK), + "description": "Chat with Injective AI agent", + }, + { + "path": "GET /history", + "price": f"${HISTORY_PRICE_USD}", + "network": str(INJECTIVE_NETWORK), + "description": "View chat history", + }, + ], + } + + @app.route("/.well-known/x402-bazaar") + async def bazaar_discovery(): + """Bazaar discovery endpoint for automated agent discovery.""" + return { + "x402Version": 2, + "gateway": "Injective iAgent × x402", + "description": "AI agent for Injective Chain trading, balances, and staking — pay per call via x402", + "endpoints": [ + { + "path": "POST /chat", + "description": "Chat with Injective AI agent", + "accepts": [ + { + "scheme": "exact", + "price": f"${CHAT_PRICE_USD}", + "network": str(INJECTIVE_NETWORK), + "payTo": INJECTIVE_ADDRESS, + } + ], + }, + { + "path": "GET /history", + "description": "View chat history", + "accepts": [ + { + "scheme": "exact", + "price": f"${HISTORY_PRICE_USD}", + "network": str(INJECTIVE_NETWORK), + "payTo": INJECTIVE_ADDRESS, + } + ], + }, + ], + "facilitators": [ + { + "name": "x402 Foundation Facilitator", + "url": X402_FACILITATOR_URL, + "chains": ["eip155:*"], + }, + ], + } From 9e63badce80af67844ff200b0d75c0d6c9f1aee5 Mon Sep 17 00:00:00 2001 From: gentechlabs Date: Tue, 25 Aug 2026 09:03:49 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20coderabbit=20review=20?= =?UTF-8?q?=E2=80=94=20correct=20Injective=20chain=20ID,=20add=20Bazaar=20?= =?UTF-8?q?discovery=20metadata,=20harden=20config=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - INJECTIVE_NETWORK: eip155:2525 -> eip155:1776 (Injective EVM mainnet) - Add Bazaar discovery metadata to POST /chat and GET /history via declare_discovery_extension (body_type=json, message/session_id schemas) - Remove x402.org testnet-only facilitator default from .env.example - Isolate config tests from local .env with autouse monkeypatch fixture - Make test_wrap_with_x402_configured assertion effective (result is not None) --- .env.example | 8 +++++--- tests/test_x402_middleware.py | 30 +++++++++++++++++------------- x402_middleware.py | 30 +++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index ff49d7a..968bcb3 100644 --- a/.env.example +++ b/.env.example @@ -5,9 +5,11 @@ # Required for payment-protected endpoints X402_PAY_TO_ADDRESS= -# x402 facilitator URL (default: x402.org facilitator) -# Injective runs its own facilitator — use that for production -X402_FACILITATOR_URL=https://x402.org/facilitator +# x402 facilitator URL (required) +# The public x402.org facilitator is testnet-only and does NOT support +# Injective mainnet (eip155:1776). Provide an Injective-compatible +# production facilitator, or run/self-facilitate your own. +X402_FACILITATOR_URL= # OpenAI API key (required for iAgent chat functionality) OPENAI_API_KEY= diff --git a/tests/test_x402_middleware.py b/tests/test_x402_middleware.py index c11aaab..c3c087a 100644 --- a/tests/test_x402_middleware.py +++ b/tests/test_x402_middleware.py @@ -99,12 +99,22 @@ def test_invalid_https_url(): # ── Config Tests ────────────────────────────────────────────────────── +@pytest.fixture(autouse=True) +def _isolate_env(monkeypatch): + """Isolate config tests from any local .env file. + + load_x402_config() calls load_dotenv(), which can restore variables from a + developer's local .env and make these tests flaky. Clear both variables and + neutralize load_dotenv so each test validates unconfigured behavior. + """ + monkeypatch.delenv("X402_PAY_TO_ADDRESS", raising=False) + monkeypatch.delenv("X402_FACILITATOR_URL", raising=False) + import x402_middleware + monkeypatch.setattr(x402_middleware, "load_dotenv", lambda *a, **k: None) + + def test_load_x402_config_missing(): """Returns False when X402_PAY_TO_ADDRESS is not set.""" - if "X402_PAY_TO_ADDRESS" in os.environ: - del os.environ["X402_PAY_TO_ADDRESS"] - if "X402_FACILITATOR_URL" in os.environ: - del os.environ["X402_FACILITATOR_URL"] assert load_x402_config() is False @@ -118,8 +128,6 @@ def test_load_x402_config_invalid_address(): def test_load_x402_config_missing_facilitator(): """Returns False when facilitator URL is not set.""" os.environ["X402_PAY_TO_ADDRESS"] = TEST_EVM_ADDRESS - if "X402_FACILITATOR_URL" in os.environ: - del os.environ["X402_FACILITATOR_URL"] assert load_x402_config() is False @@ -134,10 +142,6 @@ def test_load_x402_config_present(): def test_wrap_with_x402_no_config(app): """Returns None when x402 is not configured.""" - if "X402_PAY_TO_ADDRESS" in os.environ: - del os.environ["X402_PAY_TO_ADDRESS"] - if "X402_FACILITATOR_URL" in os.environ: - del os.environ["X402_FACILITATOR_URL"] result = wrap_with_x402(app) assert result is None @@ -145,7 +149,7 @@ def test_wrap_with_x402_no_config(app): def test_wrap_with_x402_configured(configured_app): """Returns middleware instance when configured.""" result = wrap_with_x402(configured_app) - assert result is not None or True + assert result is not None # ── Discovery Endpoint Tests ───────────────────────────────────────── @@ -211,8 +215,8 @@ async def ping(): # ── Network Configuration Tests ─────────────────────────────────────── def test_injective_network(): - """Injective network is configured as EVM-compatible chain ID 2525.""" - assert INJECTIVE_NETWORK == "eip155:2525" + """Injective network is configured as EVM-compatible chain ID 1776.""" + assert INJECTIVE_NETWORK == "eip155:1776" def test_pricing_constants(): diff --git a/x402_middleware.py b/x402_middleware.py index 0f9cd78..52a445b 100644 --- a/x402_middleware.py +++ b/x402_middleware.py @@ -24,6 +24,7 @@ from x402.mechanisms.evm.exact import ExactEvmServerScheme from x402.schemas import Network from x402.server import x402ResourceServer +from x402.extensions.bazaar import declare_discovery_extension, OutputConfig logger = logging.getLogger(__name__) @@ -35,7 +36,7 @@ # ── Configuration (loaded from env) ───────────────────────────────── INJECTIVE_ADDRESS: Optional[str] = None -INJECTIVE_NETWORK: Network = "eip155:2525" # Injective mainnet +INJECTIVE_NETWORK: Network = "eip155:1776" # Injective EVM mainnet X402_FACILITATOR_URL: Optional[str] = None # Default pricing @@ -124,6 +125,20 @@ def wrap_with_x402(app) -> Optional[PaymentMiddlewareASGI]: ], mime_type="application/json", description="Chat with Injective AI agent — trading, balances, staking", + extensions=declare_discovery_extension( + input={"message": "What is my INJ balance?"}, + input_schema={ + "type": "object", + "properties": { + "message": {"type": "string", "description": "User prompt for the agent"} + }, + "required": ["message"], + }, + body_type="json", + output=OutputConfig( + example={"response": "Your INJ balance is 12.5 INJ", "session_id": "abc123"} + ), + ), ), "GET /history": RouteConfig( accepts=[ @@ -136,6 +151,19 @@ def wrap_with_x402(app) -> Optional[PaymentMiddlewareASGI]: ], mime_type="application/json", description="View chat history for a session", + extensions=declare_discovery_extension( + input={"session_id": "abc123"}, + input_schema={ + "type": "object", + "properties": { + "session_id": {"type": "string", "description": "Session identifier"} + }, + "required": ["session_id"], + }, + output=OutputConfig( + example={"history": [{"role": "user", "content": "hi"}]} + ), + ), ), }