From f07271cc83e3e07b7a6232f2584aec67c21db0aa Mon Sep 17 00:00:00 2001 From: Mizuki Hayashi <197570892+mizuki0x@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:09:45 +0200 Subject: [PATCH 1/2] feat(xona): add Xona Agent x402 provider profile --- agent-os/Cargo.lock | 17 + agent-os/Cargo.toml | 1 + agent-os/crates/covenant-xona/Cargo.toml | 22 + .../assets/xona-orbit-snapshot.json | 1901 +++++++++++++++++ .../covenant-xona/examples/live_paid_call.rs | 217 ++ agent-os/crates/covenant-xona/src/catalog.rs | 201 ++ agent-os/crates/covenant-xona/src/config.rs | 129 ++ agent-os/crates/covenant-xona/src/lib.rs | 60 + agent-os/crates/covenant-xona/src/tools.rs | 356 +++ agent-os/crates/covenant-xona/src/x402.rs | 403 ++++ agent-os/crates/covenantd/Cargo.toml | 1 + agent-os/crates/covenantd/src/lib.rs | 224 ++ agent-os/crates/covenantd/src/main.rs | 111 + agent-os/crates/covenantd/src/xona.rs | 253 +++ docs/xona-integration.md | 168 ++ landing/app/_partners.ts | 3 +- 16 files changed, 4066 insertions(+), 1 deletion(-) create mode 100644 agent-os/crates/covenant-xona/Cargo.toml create mode 100644 agent-os/crates/covenant-xona/assets/xona-orbit-snapshot.json create mode 100644 agent-os/crates/covenant-xona/examples/live_paid_call.rs create mode 100644 agent-os/crates/covenant-xona/src/catalog.rs create mode 100644 agent-os/crates/covenant-xona/src/config.rs create mode 100644 agent-os/crates/covenant-xona/src/lib.rs create mode 100644 agent-os/crates/covenant-xona/src/tools.rs create mode 100644 agent-os/crates/covenant-xona/src/x402.rs create mode 100644 agent-os/crates/covenantd/src/xona.rs create mode 100644 docs/xona-integration.md diff --git a/agent-os/Cargo.lock b/agent-os/Cargo.lock index 887703c2a..ca5229a31 100644 --- a/agent-os/Cargo.lock +++ b/agent-os/Cargo.lock @@ -1505,6 +1505,22 @@ dependencies = [ "wiremock", ] +[[package]] +name = "covenant-xona" +version = "0.0.0" +dependencies = [ + "async-trait", + "covenant-mcp", + "covenant-x402", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", + "wiremock", +] + [[package]] name = "covenantd" version = "0.1.0" @@ -1531,6 +1547,7 @@ dependencies = [ "covenant-settlement", "covenant-types", "covenant-x402", + "covenant-xona", "dotenvy", "futures", "reqwest 0.12.28", diff --git a/agent-os/Cargo.toml b/agent-os/Cargo.toml index 450c1a8b5..8a09939a4 100644 --- a/agent-os/Cargo.toml +++ b/agent-os/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/covenant-budget", "crates/covenant-x402", "crates/covenant-hyre", + "crates/covenant-xona", "crates/covenant-sap-bridge", "crates/covenant-stake-keeper", "crates/covenantd", diff --git a/agent-os/crates/covenant-xona/Cargo.toml b/agent-os/crates/covenant-xona/Cargo.toml new file mode 100644 index 000000000..54da9cc14 --- /dev/null +++ b/agent-os/crates/covenant-xona/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "covenant-xona" +version = "0.0.0" +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "Xona Agent x402 provider profile for Covenant — orbit-registry catalog + MCP tools over the generic x402 gateway." + +[dependencies] +covenant-x402 = { path = "../covenant-x402" } +covenant-mcp = { path = "../covenant-mcp" } +async-trait = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +reqwest = { workspace = true } + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +wiremock = "0.6" diff --git a/agent-os/crates/covenant-xona/assets/xona-orbit-snapshot.json b/agent-os/crates/covenant-xona/assets/xona-orbit-snapshot.json new file mode 100644 index 000000000..0a2189706 --- /dev/null +++ b/agent-os/crates/covenant-xona/assets/xona-orbit-snapshot.json @@ -0,0 +1,1901 @@ +[ + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/ai/x-news", + "slug": "ai/x-news", + "method": "POST", + "description": "Extract latest news from X (Twitter) username, generate draft tweet and banner with Xona.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/ai/x-persona", + "slug": "ai/x-persona", + "method": "POST", + "description": "Extract structured persona from X (Twitter) username. Analyzes posts, profile, and communication style to generate persona, style profile, content patterns, and avatar prompt.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/audio/elevenlabs-music", + "slug": "audio/elevenlabs-music", + "method": "POST", + "description": "AI music generation using ElevenLabs Music (Replicate). Dynamic pricing: $1 per 120 seconds, max 3 minutes.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "250000", + "amountUsdc": 0.25, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/audio/speech-to-text", + "slug": "audio/speech-to-text", + "method": "POST", + "description": "Speech-to-text: transcribe audio from a URL using OpenAI GPT-4o Transcribe (Replicate).", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "20000", + "amountUsdc": 0.02, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/audio/x-text-to-speech", + "slug": "audio/x-text-to-speech", + "method": "POST", + "description": "X.AI text-to-speech: convert text to speech (MP3).", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/ai/x-news", + "slug": "base-main/ai/x-news", + "method": "POST", + "description": "Extract latest news from X (Twitter) username, generate draft tweet and banner with Xona. (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/ai/x-persona", + "slug": "base-main/ai/x-persona", + "method": "POST", + "description": "Extract structured persona from X (Twitter) username. Analyzes posts, profile, and communication style to generate persona, style profile, content patterns, and avatar prompt. (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/audio/elevenlabs-music", + "slug": "base-main/audio/elevenlabs-music", + "method": "POST", + "description": "AI music generation using ElevenLabs Music (Replicate). Dynamic pricing: $1 per 120 seconds, max 3 minutes (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "250000", + "amountUsdc": 0.25, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/audio/speech-to-text", + "slug": "base-main/audio/speech-to-text", + "method": "POST", + "description": "Speech-to-text: transcribe audio from a URL using OpenAI GPT-4o Transcribe (Replicate) (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "20000", + "amountUsdc": 0.02, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/audio/x-text-to-speech", + "slug": "base-main/audio/x-text-to-speech", + "method": "POST", + "description": "X.AI text-to-speech: convert text to speech (MP3) (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image-model/flux-2-flex", + "slug": "base-main/image-model/flux-2-flex", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image-model/flux-2-max", + "slug": "base-main/image-model/flux-2-max", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image-model/flux-2-pro", + "slug": "base-main/image-model/flux-2-pro", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image-model/qwen-image", + "slug": "base-main/image-model/qwen-image", + "method": "POST", + "description": "Generate image using qwen/qwen-image model (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image-model/seedream-4.5", + "slug": "base-main/image-model/seedream-4.5", + "method": "POST", + "description": "Generate image using ByteDance Seedream-4.5 model (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "80000", + "amountUsdc": 0.08, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/creative-director", + "slug": "base-main/image/creative-director", + "method": "POST", + "description": "AI-powered creative research and prompt refinement using X and Google (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "30000", + "amountUsdc": 0.03, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/designer", + "slug": "base-main/image/designer", + "method": "POST", + "description": "AI image generation with intelligent style blending (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "60000", + "amountUsdc": 0.06, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/flux-2-flex", + "slug": "base-main/image/flux-2-flex", + "method": "POST", + "description": "Max-quality image generation and editing with support for ten reference images (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "60000", + "amountUsdc": 0.06, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/flux-2-max", + "slug": "base-main/image/flux-2-max", + "method": "POST", + "description": "The highest fidelity image model from Black Forest Labs (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "80000", + "amountUsdc": 0.08, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/flux-2-pro", + "slug": "base-main/image/flux-2-pro", + "method": "POST", + "description": "High-quality image generation and editing with support for eight reference images (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/grok-imagine", + "slug": "base-main/image/grok-imagine", + "method": "POST", + "description": "AI image generation using Grok Imagine model (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "40000", + "amountUsdc": 0.04, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/nano-banana", + "slug": "base-main/image/nano-banana", + "method": "POST", + "description": "AI image generation using nano-banana model (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/image/nano-banana-pro", + "slug": "base-main/image/nano-banana-pro", + "method": "POST", + "description": "AI image generation using nano-banana-pro model (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "150000", + "amountUsdc": 0.15, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/news", + "slug": "base-main/token/news", + "method": "POST", + "description": "Fetch token information from Jupiter via Corbits, get curated news from Syra, and generate banner and social content with Xona. (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/pumpfun-movers", + "slug": "base-main/token/pumpfun-movers", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/pumpfun-trending", + "slug": "base-main/token/pumpfun-trending", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/solana-discovery", + "slug": "base-main/token/solana-discovery", + "method": "POST", + "description": "Solana token discovery via Onchain OS (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/solana-market", + "slug": "base-main/token/solana-market", + "method": "POST", + "description": "Solana token market data via Onchain OS (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/starter-kit", + "slug": "base-main/token/starter-kit", + "method": "POST", + "description": "Generate token logo and banner automatically from a prompt. Extracts token information, generates logo, then creates a matching banner. (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "200000", + "amountUsdc": 0.2, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/xlayer-discovery", + "slug": "base-main/token/xlayer-discovery", + "method": "POST", + "description": "X Layer token discovery via Onchain OS (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/token/xlayer-market", + "slug": "base-main/token/xlayer-market", + "method": "POST", + "description": "X Layer token market data via Onchain OS (Base Mainnet)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-description", + "slug": "base-main/tokens-api/asset-description", + "method": "POST", + "description": "Mint description via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-detail", + "slug": "base-main/tokens-api/asset-detail", + "method": "POST", + "description": "Full asset detail via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-markets", + "slug": "base-main/tokens-api/asset-markets", + "method": "POST", + "description": "DEX markets via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-ohlcv", + "slug": "base-main/tokens-api/asset-ohlcv", + "method": "POST", + "description": "OHLCV candle data via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-price-chart", + "slug": "base-main/tokens-api/asset-price-chart", + "method": "POST", + "description": "Price chart via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-profile", + "slug": "base-main/tokens-api/asset-profile", + "method": "POST", + "description": "Asset profile via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-risk-details", + "slug": "base-main/tokens-api/asset-risk-details", + "method": "POST", + "description": "Detailed risk analysis via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-risk-summary", + "slug": "base-main/tokens-api/asset-risk-summary", + "method": "POST", + "description": "Asset risk summary via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-tickers", + "slug": "base-main/tokens-api/asset-tickers", + "method": "POST", + "description": "Exchange tickers via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-variant-market", + "slug": "base-main/tokens-api/asset-variant-market", + "method": "POST", + "description": "Single variant market via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/asset-variants", + "slug": "base-main/tokens-api/asset-variants", + "method": "POST", + "description": "Asset variants via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/curated", + "slug": "base-main/tokens-api/curated", + "method": "POST", + "description": "Curated asset collections via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/market-snapshots", + "slug": "base-main/tokens-api/market-snapshots", + "method": "POST", + "description": "Bulk market snapshots via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/resolve", + "slug": "base-main/tokens-api/resolve", + "method": "POST", + "description": "Resolve mint/ref to canonical asset via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/risk-summary", + "slug": "base-main/tokens-api/risk-summary", + "method": "POST", + "description": "Quick risk assessment via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/search", + "slug": "base-main/tokens-api/search", + "method": "POST", + "description": "Search assets by query via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/tokens-api/variant-markets", + "slug": "base-main/tokens-api/variant-markets", + "method": "POST", + "description": "Batch variant market snapshots via Tokens API (Base/EVM)", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base-main/video/short-generation", + "slug": "base-main/video/short-generation", + "method": "POST", + "description": "AI short video generation using Grok Imagine Video model (Base Mainnet).", + "pricing": [ + { + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/ai/x-news", + "slug": "base/ai/x-news", + "method": "POST", + "description": "Extract latest news from X (Twitter) username, generate draft tweet and banner with Xona. (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/ai/x-persona", + "slug": "base/ai/x-persona", + "method": "POST", + "description": "Extract structured persona from X (Twitter) username. Analyzes posts, profile, and communication style to generate persona, style profile, content patterns, and avatar prompt. (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/audio/elevenlabs-music", + "slug": "base/audio/elevenlabs-music", + "method": "POST", + "description": "AI music generation using ElevenLabs Music (Replicate). Dynamic pricing: $1 per 120 seconds, max 3 minutes (Base/EVM).", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "250000", + "amountUsdc": 0.25, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/audio/speech-to-text", + "slug": "base/audio/speech-to-text", + "method": "POST", + "description": "Speech-to-text: transcribe audio from a URL using OpenAI GPT-4o Transcribe (Replicate) (Base/EVM).", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "20000", + "amountUsdc": 0.02, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/audio/x-text-to-speech", + "slug": "base/audio/x-text-to-speech", + "method": "POST", + "description": "X.AI text-to-speech: convert text to speech (MP3) (Base/EVM).", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image-model/flux-2-flex", + "slug": "base/image-model/flux-2-flex", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image-model/flux-2-max", + "slug": "base/image-model/flux-2-max", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image-model/flux-2-pro", + "slug": "base/image-model/flux-2-pro", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image-model/qwen-image", + "slug": "base/image-model/qwen-image", + "method": "POST", + "description": "Generate image using qwen/qwen-image model (Base/EVM). Takes your prompt and generates a high-quality image with advanced AI capabilities.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image-model/seedream-4.5", + "slug": "base/image-model/seedream-4.5", + "method": "POST", + "description": "Generate image using ByteDance Seedream-4.5 model (Base/EVM). Takes your prompt and generates a high-quality image with advanced AI capabilities.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "80000", + "amountUsdc": 0.08, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/creative-director", + "slug": "base/image/creative-director", + "method": "POST", + "description": "AI-powered creative research and prompt refinement using X and Google (Base/EVM). Analyzes trends, and transforms your idea into an optimized generation plan.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "30000", + "amountUsdc": 0.03, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/designer", + "slug": "base/image/designer", + "method": "POST", + "description": "AI image generation with intelligent style blending (Base/EVM). Takes your prompt and style keywords, refines them together, and generates a high-quality image.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "60000", + "amountUsdc": 0.06, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/flux-2-flex", + "slug": "base/image/flux-2-flex", + "method": "POST", + "description": "Max-quality image generation and editing with support for ten reference images (Base/SKALE).", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "60000", + "amountUsdc": 0.06, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/flux-2-max", + "slug": "base/image/flux-2-max", + "method": "POST", + "description": "The highest fidelity image model from Black Forest Labs (Base/SKALE).", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "80000", + "amountUsdc": 0.08, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/flux-2-pro", + "slug": "base/image/flux-2-pro", + "method": "POST", + "description": "High-quality image generation and editing with support for eight reference images (Base/SKALE).", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/grok-imagine", + "slug": "base/image/grok-imagine", + "method": "POST", + "description": "AI image generation using Grok Imagine model (Base/EVM). Takes your prompt and generates a high-quality image using xAI's Grok Imagine API.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "40000", + "amountUsdc": 0.04, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/nano-banana", + "slug": "base/image/nano-banana", + "method": "POST", + "description": "AI image generation using nano-banana model (Base/EVM). Takes your prompt and generates a high-quality image directly without style blending.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/nano-banana-pro", + "slug": "base/image/nano-banana-pro", + "method": "POST", + "description": "AI image generation using nano-banana-pro model (Base/EVM). Takes your prompt and generates a high-quality image directly without style blending.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "150000", + "amountUsdc": 0.15, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/news", + "slug": "base/token/news", + "method": "POST", + "description": "Fetch token information from Jupiter via Corbits, get curated news from Syra, and generate banner and social content with Xona. (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/pumpfun-movers", + "slug": "base/token/pumpfun-movers", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/pumpfun-trending", + "slug": "base/token/pumpfun-trending", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/solana-discovery", + "slug": "base/token/solana-discovery", + "method": "POST", + "description": "Solana token discovery via Onchain OS (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/solana-market", + "slug": "base/token/solana-market", + "method": "POST", + "description": "Solana token market data via Onchain OS (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/starter-kit", + "slug": "base/token/starter-kit", + "method": "POST", + "description": "Generate token logo and banner automatically from a prompt. Extracts token information, generates logo, then creates a matching banner. (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "200000", + "amountUsdc": 0.2, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/xlayer-discovery", + "slug": "base/token/xlayer-discovery", + "method": "POST", + "description": "X Layer token discovery via Onchain OS (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/token/xlayer-market", + "slug": "base/token/xlayer-market", + "method": "POST", + "description": "X Layer token market data via Onchain OS (Base/EVM)", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/video/short-generation", + "slug": "base/video/short-generation", + "method": "POST", + "description": "AI short video generation using Grok Imagine Video model (Base/EVM). Takes your prompt and generates a 10-second high-quality video using xAI's Grok Video API.", + "pricing": [ + { + "network": "eip155:1187947933", + "asset": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "0x515e7Bce44Baa5F6e42D16d4B5f27768E7f2F8cC", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image-model/flux-2-flex", + "slug": "image-model/flux-2-flex", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image-model/flux-2-max", + "slug": "image-model/flux-2-max", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image-model/flux-2-pro", + "slug": "image-model/flux-2-pro", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image-model/qwen-image", + "slug": "image-model/qwen-image", + "method": "POST", + "description": "Generate image using qwen/qwen-image model", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image-model/seedream-4.5", + "slug": "image-model/seedream-4.5", + "method": "POST", + "description": "Generate image using ByteDance Seedream-4.5 model", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "80000", + "amountUsdc": 0.08, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/creative-director", + "slug": "image/creative-director", + "method": "POST", + "description": "AI-powered creative research and prompt refinement using X and Google. Analyzes trends, and transforms your idea into an optimized generation plan.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "30000", + "amountUsdc": 0.03, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/designer", + "slug": "image/designer", + "method": "POST", + "description": "AI image generation with intelligent style blending. Takes your prompt and style keywords, refines them together, and generates a high-quality image.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "60000", + "amountUsdc": 0.06, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/flux-2-flex", + "slug": "image/flux-2-flex", + "method": "POST", + "description": "Max-quality image generation and editing with support for ten reference images", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "60000", + "amountUsdc": 0.06, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/flux-2-max", + "slug": "image/flux-2-max", + "method": "POST", + "description": "The highest fidelity image model from Black Forest Labs", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "80000", + "amountUsdc": 0.08, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/flux-2-pro", + "slug": "image/flux-2-pro", + "method": "POST", + "description": "High-quality image generation and editing with support for eight reference images", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/nano-banana", + "slug": "image/nano-banana", + "method": "POST", + "description": "AI image generation using nano-banana model. Takes your prompt and generates a high-quality image directly without style blending.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "50000", + "amountUsdc": 0.05, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/nano-banana-pro", + "slug": "image/nano-banana-pro", + "method": "POST", + "description": "AI image generation using nano-banana-pro model. Takes your prompt and generates a high-quality image directly without style blending.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "150000", + "amountUsdc": 0.15, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/news", + "slug": "token/news", + "method": "POST", + "description": "Fetch token information from Jupiter via Corbits, get curated news from Syra, and generate banner and social content with Xona.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "500000", + "amountUsdc": 0.5, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/pumpfun-movers", + "slug": "token/pumpfun-movers", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/pumpfun-trending", + "slug": "token/pumpfun-trending", + "method": "POST", + "description": "", + "pricing": [] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/solana-discovery", + "slug": "token/solana-discovery", + "method": "POST", + "description": "Solana token discovery via Onchain OS", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/solana-market", + "slug": "token/solana-market", + "method": "POST", + "description": "Solana token market data via Onchain OS", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/starter-kit", + "slug": "token/starter-kit", + "method": "POST", + "description": "Generate token logo and banner automatically from a prompt. Extracts token information, generates logo, then creates a matching banner.", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "200000", + "amountUsdc": 0.2, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/xlayer-discovery", + "slug": "token/xlayer-discovery", + "method": "POST", + "description": "X Layer token discovery via Onchain OS", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/xlayer-market", + "slug": "token/xlayer-market", + "method": "POST", + "description": "X Layer token market data via Onchain OS", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-description", + "slug": "tokens-api/asset-description", + "method": "POST", + "description": "Mint description via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-detail", + "slug": "tokens-api/asset-detail", + "method": "POST", + "description": "Full asset detail via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-markets", + "slug": "tokens-api/asset-markets", + "method": "POST", + "description": "DEX markets via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-ohlcv", + "slug": "tokens-api/asset-ohlcv", + "method": "POST", + "description": "OHLCV candle data via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-price-chart", + "slug": "tokens-api/asset-price-chart", + "method": "POST", + "description": "Price chart via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-profile", + "slug": "tokens-api/asset-profile", + "method": "POST", + "description": "Asset profile via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-risk-details", + "slug": "tokens-api/asset-risk-details", + "method": "POST", + "description": "Detailed risk analysis via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-risk-summary", + "slug": "tokens-api/asset-risk-summary", + "method": "POST", + "description": "Asset risk summary via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-tickers", + "slug": "tokens-api/asset-tickers", + "method": "POST", + "description": "Exchange tickers via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-variant-market", + "slug": "tokens-api/asset-variant-market", + "method": "POST", + "description": "Single variant market via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/asset-variants", + "slug": "tokens-api/asset-variants", + "method": "POST", + "description": "Asset variants via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/curated", + "slug": "tokens-api/curated", + "method": "POST", + "description": "Curated asset collections via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/market-snapshots", + "slug": "tokens-api/market-snapshots", + "method": "POST", + "description": "Bulk market snapshots via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/resolve", + "slug": "tokens-api/resolve", + "method": "POST", + "description": "Resolve mint/ref to canonical asset via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/risk-summary", + "slug": "tokens-api/risk-summary", + "method": "POST", + "description": "Quick risk assessment via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/search", + "slug": "tokens-api/search", + "method": "POST", + "description": "Search assets by query via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + }, + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/tokens-api/variant-markets", + "slug": "tokens-api/variant-markets", + "method": "POST", + "description": "Batch variant market snapshots via Tokens API", + "pricing": [ + { + "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": "10000", + "amountUsdc": 0.01, + "payTo": "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA", + "scheme": "exact" + } + ] + } +] \ No newline at end of file diff --git a/agent-os/crates/covenant-xona/examples/live_paid_call.rs b/agent-os/crates/covenant-xona/examples/live_paid_call.rs new file mode 100644 index 000000000..89929c424 --- /dev/null +++ b/agent-os/crates/covenant-xona/examples/live_paid_call.rs @@ -0,0 +1,217 @@ +//! End-to-end live smoke against Xona Agent's production API, settling +//! self-paid on Solana (no sponsor feePayer). +//! +//! Dry-run by default: builds the catalog, resolves the target endpoint, +//! fetches its 402 challenge, picks the matching option, and prints what +//! would be signed and posted. Pass `--confirm` (or set +//! `COVENANT_XONA_CONFIRM=1`) to actually re-POST the signed request and +//! move USDC. +//! +//! COVENANT_X402_FUNDING_KEYPAIR=~/.config/solana/some.json \ +//! COVENANT_X402_RPC_URL=https://api.mainnet-beta.solana.com \ +//! COVENANT_X402_SIGNER_BIN=/path/to/covenant-x402-signer \ +//! cargo run -p covenant-xona --example live_paid_call -- --confirm +//! +//! Targets `image/creative-director` ($0.03) — the endpoint the original +//! covenant-x402 `xona-demo` proved end-to-end. + +use std::process::Stdio; + +use async_trait::async_trait; +use covenant_x402::{ + orbit::{DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE}, + OrbitClient, PaymentRequirements, Result as X402Result, Signer, X402Error, +}; +use covenant_xona::config::{PAY_TO, SOLANA_NETWORK, USDC_MINT}; +use covenant_xona::{execute_paid, PaidRequest, XonaCatalog, XonaConfig}; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; + +const TARGET_SLUG: &str = "image/creative-director"; +const PROMPT: &str = "a single neon koi over a black background, minimal"; + +/// Signs by shelling out to the built `covenant-x402-signer` sidecar — +/// the same binary the daemon spawns. With no `feePayer` in the +/// requirement the sidecar self-pays via `SolanaSigner`. +struct SubprocessSigner { + program: String, + env: Vec<(String, String)>, +} + +#[async_trait] +impl Signer for SubprocessSigner { + async fn build_payment(&self, req: &PaymentRequirements) -> X402Result { + let payload = serde_json::to_vec(req) + .map_err(|e| X402Error::Sign(format!("encode requirement: {e}")))?; + let mut child = Command::new(&self.program) + .envs(self.env.iter().map(|(k, v)| (k.as_str(), v.as_str()))) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| X402Error::Sign(format!("spawn {}: {e}", self.program)))?; + child + .stdin + .take() + .ok_or_else(|| X402Error::Sign("stdin unavailable".into()))? + .write_all(&payload) + .await + .map_err(|e| X402Error::Sign(format!("write stdin: {e}")))?; + let out = child + .wait_with_output() + .await + .map_err(|e| X402Error::Sign(format!("await child: {e}")))?; + if !out.status.success() { + return Err(X402Error::Sign(format!( + "signer exited {}: {}", + out.status, + String::from_utf8_lossy(&out.stderr).trim() + ))); + } + let header = String::from_utf8(out.stdout) + .map_err(|e| X402Error::Sign(format!("non-utf8 stdout: {e}")))? + .trim() + .to_string(); + if header.is_empty() { + return Err(X402Error::Sign("signer returned empty header".into())); + } + Ok(header) + } +} + +#[tokio::main] +async fn main() { + let confirm = std::env::args().any(|a| a == "--confirm") + || std::env::var("COVENANT_XONA_CONFIRM").ok().as_deref() == Some("1"); + + let cfg = XonaConfig { + enabled: true, + ..Default::default() + }; + + // Resolve the endpoint through the live registry (vendored fallback), + // exactly as the daemon does at boot. + println!("--- step 0: build the Xona catalog ---"); + let orbit = OrbitClient::with( + reqwest::Client::new(), + DEFAULT_BASE_URL.to_string(), + DEFAULT_PAGE_SIZE, + ); + let catalog = match XonaCatalog::refresh(&orbit, &cfg).await { + Ok(c) if !c.is_empty() => { + println!( + "catalog: {} Solana endpoints (live registry)", + c.endpoints().len() + ); + c + } + _ => { + let c = XonaCatalog::from_vendored(&cfg).expect("vendored snapshot"); + println!( + "catalog: {} Solana endpoints (vendored fallback)", + c.endpoints().len() + ); + c + } + }; + let ep = catalog + .endpoints() + .iter() + .find(|e| e.slug == TARGET_SLUG) + .unwrap_or_else(|| panic!("catalog has no Solana endpoint for {TARGET_SLUG}")) + .clone(); + + let per_call_cap = ep.price_micro_usdc.max(1); + println!("\ntarget: POST {}", ep.url); + println!("net: {SOLANA_NETWORK}"); + println!("asset: {USDC_MINT}"); + println!("pay_to: {} (config-pinned {PAY_TO})", ep.pay_to); + println!( + "cap: {per_call_cap} atomic USDC ({:.4} USDC)", + per_call_cap as f64 / 1e6 + ); + + let http = reqwest::Client::new(); + let body = serde_json::json!({ "prompt": PROMPT }); + + println!("\n--- step 1: fetch the unpaid 402 challenge (free) ---"); + let probe = http.post(&ep.url).json(&body).send().await.expect("POST"); + println!("status: {}", probe.status()); + let challenge = probe.text().await.unwrap(); + println!("challenge body: {}", &challenge[..challenge.len().min(400)]); + let options = covenant_xona::parse_challenge(&challenge).expect("parse"); + println!("parsed {} option(s)", options.len()); + let chosen = covenant_xona::x402::select(&options, &cfg.network, &cfg.asset, per_call_cap) + .expect("at least one option within cap"); + println!("picked: amount={} payTo={}", chosen.amount, chosen.pay_to); + + if chosen.pay_to != ep.pay_to { + panic!( + "REFUSING: 402 advertised payTo={} but registry-advertised payee={}", + chosen.pay_to, ep.pay_to + ); + } + + if !confirm { + println!("\nDRY RUN — pass --confirm to actually pay. Exiting cleanly."); + return; + } + + println!("\n--- step 2: sign + POST (real USDC) ---"); + let kp = std::env::var("COVENANT_X402_FUNDING_KEYPAIR") + .expect("COVENANT_X402_FUNDING_KEYPAIR must be set"); + let rpc = std::env::var("COVENANT_X402_RPC_URL") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into()); + let signer_bin = std::env::var("COVENANT_X402_SIGNER_BIN") + .expect("COVENANT_X402_SIGNER_BIN must point to the built sidecar binary"); + + let signer = SubprocessSigner { + program: signer_bin, + env: vec![ + ("COVENANT_X402_FUNDING_KEYPAIR".into(), kp), + ("COVENANT_X402_RPC_URL".into(), rpc), + ], + }; + + let plan = PaidRequest { + provider: "xona".into(), + slug: ep.slug.clone(), + url: ep.url.clone(), + method: ep.method.clone(), + body: Some(body), + network: cfg.network.clone(), + asset: cfg.asset.clone(), + per_call_cap, + credits: ep.credits(), + price_micro_usdc: ep.price_micro_usdc, + pay_to: ep.pay_to.clone(), + }; + + let out = match execute_paid(&http, &signer, &plan).await { + Ok(o) => o, + Err(e) => { + eprintln!("\nFAIL: {e}"); + std::process::exit(1); + } + }; + println!("status: {}", out.status); + println!("paid_amount: {:?} atomic USDC", out.paid_amount); + println!("response body: {}", &out.body[..out.body.len().min(1000)]); + + if let Some(amount) = out.paid_amount { + if (200..300).contains(&out.status) { + println!("\nOK — paid {amount} atomic USDC against {}.", ep.pay_to); + } else { + println!( + "\nFAIL — Xona returned {} after we paid. Investigate.", + out.status + ); + std::process::exit(2); + } + } else { + println!( + "\nNo payment recorded — Xona returned {} on the first POST (free).", + out.status + ); + } +} diff --git a/agent-os/crates/covenant-xona/src/catalog.rs b/agent-os/crates/covenant-xona/src/catalog.rs new file mode 100644 index 000000000..283f3cc5d --- /dev/null +++ b/agent-os/crates/covenant-xona/src/catalog.rs @@ -0,0 +1,201 @@ +//! Build the endpoint index from the orbit-x402 registry. +//! +//! [`XonaCatalog`] is the bridge between the registry and the tool +//! layer. It filters the registry down to Xona's entries that settle on +//! the configured `(network, asset)` rail — Xona also lists Base +//! endpoints the Solana funding key cannot pay — and keeps a flat +//! [`XonaEndpoint`] list the tool layer reads. One catalog is cheap to +//! rebuild on each refresh cycle. + +use covenant_x402::{OrbitClient, RegistryEntry}; + +use crate::config::XonaConfig; +use crate::{Result, XonaError}; + +/// One resolved Xona endpoint on the configured settlement rail. +#[derive(Debug, Clone, PartialEq)] +pub struct XonaEndpoint { + /// Registry slug, e.g. `image/creative-director`. + pub slug: String, + /// Fully resolved endpoint URL. + pub url: String, + /// HTTP verb the endpoint expects (Xona's are POST). + pub method: String, + /// Capability summary from the registry. + pub description: String, + /// Published price in atomic USDC for the configured rail — the + /// discovery-time figure; the live 402 is authoritative. + pub price_micro_usdc: u128, + /// Registry-advertised payee on the configured rail. Pinned against + /// the live 402 challenge before any signature is produced. + pub pay_to: String, +} + +impl XonaEndpoint { + /// MCP tool name: `xona.` with path separators flattened to + /// dots and any braces stripped. + pub fn tool_name(&self) -> String { + let body = self + .slug + .trim_start_matches('/') + .replace(['{', '}'], "") + .replace('/', "."); + format!("xona.{body}") + } + + /// USD-pegged budget credits to debit — one credit per atomic cent + /// of published price, matching the daemon's other paid-call rails. + pub fn credits(&self) -> u64 { + (self.price_micro_usdc / 10_000) as u64 + } +} + +pub struct XonaCatalog { + endpoints: Vec, +} + +impl XonaCatalog { + /// Filter raw registry entries down to Xona's endpoints on the + /// configured rail. An entry is kept when its `serverTitle` matches + /// the Xona prefix, its slug is allowed, and it carries a pricing + /// option on the configured `(network, asset)`. + pub fn from_entries(entries: Vec, config: &XonaConfig) -> Self { + let endpoints = entries + .into_iter() + .filter(|e| config.matches_server(&e.server_title) && config.allows(&e.slug)) + .filter_map(|e| { + let pricing = e + .pricing + .iter() + .find(|p| p.network == config.network && p.asset == config.asset)?; + let price_micro_usdc = pricing.amount.parse::().ok()?; + Some(XonaEndpoint { + slug: e.slug, + url: e.endpoint, + method: e.method, + description: e.description, + price_micro_usdc, + pay_to: pricing.pay_to.clone(), + }) + }) + .collect(); + Self { endpoints } + } + + /// Parse a vendored or fetched registry snapshot (a JSON array of + /// [`RegistryEntry`]) and filter it under the config. + pub fn from_snapshot(snapshot_json: &str, config: &XonaConfig) -> Result { + let entries: Vec = serde_json::from_str(snapshot_json) + .map_err(|e| XonaError::Snapshot(format!("decode registry snapshot: {e}")))?; + Ok(Self::from_entries(entries, config)) + } + + /// Build from the crate's vendored snapshot — the offline default. + pub fn from_vendored(config: &XonaConfig) -> Result { + Self::from_snapshot(crate::VENDORED_SNAPSHOT, config) + } + + /// Fetch the live orbit-x402 registry and rebuild. The daemon polls + /// this on a refresh interval so a Xona catalog change needs no + /// rebuild. + pub async fn refresh(orbit: &OrbitClient, config: &XonaConfig) -> Result { + let entries = orbit + .fetch_all() + .await + .map_err(|e| XonaError::Registry(e.to_string()))?; + Ok(Self::from_entries(entries, config)) + } + + pub fn endpoints(&self) -> &[XonaEndpoint] { + &self.endpoints + } + + pub fn is_empty(&self) -> bool { + self.endpoints.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vendored_catalog_keeps_only_solana_endpoints() { + let cat = XonaCatalog::from_vendored(&XonaConfig::default()).unwrap(); + // The vendored snapshot carries Solana + Base entries; only the + // Solana-settled ones survive the default (network, asset) filter. + assert!(!cat.is_empty()); + assert!(cat + .endpoints() + .iter() + .all(|e| !e.slug.starts_with("base/") && !e.slug.starts_with("base-main/"))); + let cd = cat + .endpoints() + .iter() + .find(|e| e.slug == "image/creative-director") + .expect("creative-director on solana"); + assert_eq!(cd.pay_to, crate::config::PAY_TO); + assert_eq!(cd.url, "https://api.xona-agent.com/image/creative-director"); + assert_eq!(cd.tool_name(), "xona.image.creative-director"); + assert!(cd.price_micro_usdc > 0); + } + + #[test] + fn allowlist_filters_catalog() { + let config = XonaConfig { + allow: Some(vec![ + "image/creative-director".into(), + "audio/speech-to-text".into(), + ]), + ..XonaConfig::default() + }; + let cat = XonaCatalog::from_vendored(&config).unwrap(); + assert_eq!(cat.endpoints().len(), 2); + assert!(cat + .endpoints() + .iter() + .all(|e| e.slug == "image/creative-director" || e.slug == "audio/speech-to-text")); + } + + #[test] + fn from_entries_drops_off_rail_and_unpriced() { + let entries: Vec = serde_json::from_value(serde_json::json!([ + // Xona, Solana-priced → kept. + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/image/designer", + "slug": "image/designer", "method": "POST", "description": "d", + "pricing": [{ "network": crate::config::SOLANA_NETWORK, "asset": crate::config::USDC_MINT, "amount": "60000", "amountUsdc": 0.06, "payTo": crate::config::PAY_TO, "scheme": "exact" }] + }, + // Xona, but only Base-priced → dropped (off-rail). + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/base/image/designer", + "slug": "base/image/designer", "method": "POST", "description": "d", + "pricing": [{ "network": "eip155:8453", "asset": "0xUSDCbase", "amount": "60000", "amountUsdc": 0.06, "payTo": "0xpayee", "scheme": "exact" }] + }, + // Xona, no pricing → dropped. + { + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": "https://api.xona-agent.com/token/pumpfun-movers", + "slug": "token/pumpfun-movers", "method": "POST", "description": "d", + "pricing": [] + }, + // A different provider, Solana-priced → dropped (wrong server). + { + "serverUrl": "https://other.example", + "serverTitle": "Orbis — API Marketplace", + "endpoint": "https://other.example/x", "slug": "x", "method": "POST", "description": "d", + "pricing": [{ "network": crate::config::SOLANA_NETWORK, "asset": crate::config::USDC_MINT, "amount": "10000", "amountUsdc": 0.01, "payTo": "Pother", "scheme": "exact" }] + } + ])) + .unwrap(); + let cat = XonaCatalog::from_entries(entries, &XonaConfig::default()); + assert_eq!(cat.endpoints().len(), 1); + assert_eq!(cat.endpoints()[0].slug, "image/designer"); + assert_eq!(cat.endpoints()[0].credits(), 6); + } +} diff --git a/agent-os/crates/covenant-xona/src/config.rs b/agent-os/crates/covenant-xona/src/config.rs new file mode 100644 index 000000000..8ff230f51 --- /dev/null +++ b/agent-os/crates/covenant-xona/src/config.rs @@ -0,0 +1,129 @@ +//! Xona provider configuration. +//! +//! Everything operator-tunable about the Xona profile lives here: the +//! settlement rail, the spend allowlist, and the registry source. The +//! defaults encode Xona's published Solana/USDC rail so a daemon that +//! only flips `enabled = true` gets a working profile. + +use serde::{Deserialize, Serialize}; + +/// CAIP-2 id for Xona's Solana settlement (mainnet-beta genesis hash). +pub const SOLANA_NETWORK: &str = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; +/// USDC mint payments are denominated in (Solana mainnet). +pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +/// Xona's Solana payee, pinned against the live 402 challenge so a +/// manipulated challenge can't steer the funding key to another address. +pub const PAY_TO: &str = "9VaDVp1Wb78G4Wm6VuTiMrpESjrUymXefQTHcJGRSTEA"; +/// Stable prefix of Xona's `serverTitle` in the orbit registry +/// (`"Xona Agent | Infrastructure for Agentic Commerce"`). Matched by +/// prefix so a tagline change doesn't drop the catalog. +pub const SERVER_TITLE_PREFIX: &str = "Xona Agent"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct XonaConfig { + /// Master switch. `false` means no Xona tools are registered and + /// no Xona call ever leaves the host. + #[serde(default)] + pub enabled: bool, + /// Registry `serverTitle` prefix that identifies Xona's entries. + #[serde(default = "default_server_title_prefix")] + pub server_title_prefix: String, + /// CAIP-2 settlement network. Only endpoints priced on this network + /// (and `asset`) enter the catalog — Xona also lists Base endpoints + /// the Solana funding key cannot pay. + #[serde(default = "default_network")] + pub network: String, + /// Payment asset (USDC mint). + #[serde(default = "default_asset")] + pub asset: String, + /// Per-call ceiling in atomic USDC. A 402 amount above this is + /// rejected before the signer runs. `0` disables the local cap and + /// defers entirely to the daemon capability's per-call cap. + #[serde(default)] + pub per_call_cap: u128, + /// Endpoint allowlist by slug (`image/creative-director`). `None` + /// allows every catalog endpoint; `Some([])` allows none. + #[serde(default)] + pub allow: Option>, +} + +fn default_server_title_prefix() -> String { + SERVER_TITLE_PREFIX.to_string() +} +fn default_network() -> String { + SOLANA_NETWORK.to_string() +} +fn default_asset() -> String { + USDC_MINT.to_string() +} + +impl Default for XonaConfig { + fn default() -> Self { + Self { + enabled: false, + server_title_prefix: default_server_title_prefix(), + network: default_network(), + asset: default_asset(), + per_call_cap: 0, + allow: None, + } + } +} + +impl XonaConfig { + /// Whether a registry `serverTitle` belongs to Xona. + pub fn matches_server(&self, server_title: &str) -> bool { + server_title.starts_with(&self.server_title_prefix) + } + + /// Whether `slug` is permitted by the allowlist. + pub fn allows(&self, slug: &str) -> bool { + match &self.allow { + None => true, + Some(list) => list.iter().any(|s| s == slug), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_is_disabled_with_solana_rail() { + let c = XonaConfig::default(); + assert!(!c.enabled); + assert_eq!(c.network, SOLANA_NETWORK); + assert_eq!(c.asset, USDC_MINT); + assert_eq!(c.server_title_prefix, SERVER_TITLE_PREFIX); + } + + #[test] + fn matches_server_is_prefix_not_exact() { + let c = XonaConfig::default(); + assert!(c.matches_server("Xona Agent | Infrastructure for Agentic Commerce")); + assert!(c.matches_server("Xona Agent")); + assert!(!c.matches_server("Orbis — API Marketplace")); + assert!(!c.matches_server("Hyre")); + } + + #[test] + fn allowlist_none_allows_all_some_is_exact() { + let mut c = XonaConfig::default(); + assert!(c.allows("image/creative-director")); + c.allow = Some(vec!["audio/speech-to-text".into()]); + assert!(c.allows("audio/speech-to-text")); + assert!(!c.allows("image/creative-director")); + c.allow = Some(vec![]); + assert!(!c.allows("audio/speech-to-text")); + } + + #[test] + fn config_round_trips_through_serde_with_defaults() { + let json = serde_json::json!({ "enabled": true }); + let c: XonaConfig = serde_json::from_value(json).unwrap(); + assert!(c.enabled); + assert_eq!(c.network, SOLANA_NETWORK); + assert_eq!(c.server_title_prefix, SERVER_TITLE_PREFIX); + } +} diff --git a/agent-os/crates/covenant-xona/src/lib.rs b/agent-os/crates/covenant-xona/src/lib.rs new file mode 100644 index 000000000..bbf676b98 --- /dev/null +++ b/agent-os/crates/covenant-xona/src/lib.rs @@ -0,0 +1,60 @@ +//! Xona Agent as an x402 provider profile. +//! +//! Xona Agent publishes a catalog of paid generation and market-data +//! endpoints (image, audio, token intelligence) as x402 HTTP: each +//! endpoint answers a 402 challenge and settles in USDC on Solana. This +//! crate is not a second runtime — it is a *provider profile* over the +//! generic gateway in `covenant-x402`: +//! +//! - [`catalog`] filters the orbit-x402 registry ([`covenant_x402::OrbitClient`]) +//! down to Xona's Solana-settled endpoints and keeps the endpoint +//! index the tool layer reads. No daemon rebuild when Xona adds +//! endpoints — the catalog is data, refreshed from the live registry. +//! - [`tools`] generates one [`covenant_mcp::Tool`] per endpoint, wired +//! to a [`PaidExecutor`] the daemon supplies. The daemon is the only +//! signer; this crate never touches a funding key. +//! - [`x402`] runs the 402-then-pay loop for one resolved call. Xona +//! serves the plain (self-paid) Solana x402 scheme — no sponsor +//! `feePayer` — so the daemon's signer sidecar self-pays with +//! `SolanaSigner`. +//! +//! Settlement, budget, and audit are not re-implemented here: a Xona +//! call runs through the daemon's existing x402 accounting path, so its +//! receipt rolls into the same Merkle batch and optional Synapse mirror +//! as every other paid call. + +#![deny(unsafe_code)] + +pub mod catalog; +pub mod config; +pub mod tools; +pub mod x402; + +pub use catalog::{XonaCatalog, XonaEndpoint}; +pub use config::XonaConfig; +pub use tools::{xona_specs, xona_tool, xona_tools, PaidExecutor, PaidRequest, PaidResponse}; +pub use x402::{execute_paid, parse_challenge, PaidHttp}; + +/// Vendored snapshot of Xona's orbit-x402 registry entries, captured for +/// offline startup. The daemon refreshes from the live registry on boot; +/// this copy is the fallback so a fresh install has the catalog without a +/// network round-trip. +pub const VENDORED_SNAPSHOT: &str = include_str!("../assets/xona-orbit-snapshot.json"); + +#[derive(Debug, thiserror::Error)] +pub enum XonaError { + #[error("snapshot: {0}")] + Snapshot(String), + #[error("402 challenge: {0}")] + Challenge(String), + #[error("no payment option matched policy: {0}")] + NotAllowed(String), + #[error("paid call: {0}")] + Execute(String), + #[error("registry: {0}")] + Registry(String), + #[error("http: {0}")] + Http(#[from] reqwest::Error), +} + +pub type Result = std::result::Result; diff --git a/agent-os/crates/covenant-xona/src/tools.rs b/agent-os/crates/covenant-xona/src/tools.rs new file mode 100644 index 000000000..51fbf658d --- /dev/null +++ b/agent-os/crates/covenant-xona/src/tools.rs @@ -0,0 +1,356 @@ +//! Generate MCP tools from the catalog. +//! +//! Every catalog endpoint becomes one [`XonaEndpointTool`] named +//! `xona.`. Xona's endpoints take a free-form JSON body (most read +//! a `prompt`), so the tool surfaces a permissive object schema and +//! forwards the call arguments as the request body. +//! +//! A tool never makes a network call itself. It marshals arguments into +//! a [`PaidRequest`] and hands it to a [`PaidExecutor`] the daemon +//! supplies. The executor owns the x402 client, the funding-key signer +//! (a sidecar), and the budget/settlement/audit accounting. Keeping that +//! behind a trait is what lets this crate stay free of funding keys and +//! of the daemon's subsystem types. + +use std::sync::Arc; + +use async_trait::async_trait; +use covenant_mcp::{Content, Tool, ToolCallResult, ToolError}; +use serde_json::{Map, Value}; + +use crate::catalog::{XonaCatalog, XonaEndpoint}; +use crate::config::XonaConfig; + +/// Provider tag carried on every Xona paid call, for audit and catalog +/// disambiguation. +pub const PROVIDER: &str = "xona"; + +/// A resolved paid call the daemon executes and accounts for. +#[derive(Debug, Clone, PartialEq)] +pub struct PaidRequest { + pub provider: String, + /// Endpoint slug, for audit (`image/creative-director`). + pub slug: String, + /// Fully resolved endpoint URL. + pub url: String, + pub method: String, + pub body: Option, + /// CAIP-2 settlement network. + pub network: String, + /// Payment asset (USDC mint). + pub asset: String, + /// Per-call ceiling in atomic USDC. + pub per_call_cap: u128, + /// USD-pegged budget credits to debit. + pub credits: u64, + /// Published price in atomic USDC — the discovery-time figure. + pub price_micro_usdc: u128, + /// Registry-advertised payee, pinned against the live 402 challenge. + pub pay_to: String, +} + +/// Outcome of a paid call. +#[derive(Debug, Clone, PartialEq)] +pub struct PaidResponse { + pub status: u16, + /// Upstream response body, parsed as JSON when possible. + pub body: Value, + /// Settlement receipt id joining budget, settlement, and audit logs. + /// `None` when the upstream returned a non-success status. + pub receipt_id: Option, +} + +/// The daemon's paid-call seam. Implemented over the x402 client + +/// signer sidecar + settlement accounting; mocked in this crate's tests. +#[async_trait] +pub trait PaidExecutor: Send + Sync { + async fn execute(&self, req: PaidRequest) -> std::result::Result; +} + +struct XonaEndpointTool { + name: String, + description: String, + endpoint: XonaEndpoint, + network: String, + asset: String, + per_call_cap: u128, + executor: Arc, +} + +impl XonaEndpointTool { + /// A permissive object schema. Xona's registry does not publish + /// per-endpoint argument shapes, so we surface the common `prompt` + /// field and forward any other supplied keys as the request body. + fn schema(&self) -> Value { + serde_json::json!({ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Primary natural-language input for the endpoint. Most Xona endpoints read this; some accept additional endpoint-specific fields.", + } + }, + "additionalProperties": true, + }) + } +} + +#[async_trait] +impl Tool for XonaEndpointTool { + fn name(&self) -> &str { + &self.name + } + fn description(&self) -> &str { + &self.description + } + fn input_schema(&self) -> Value { + self.schema() + } + + async fn call(&self, arguments: Value) -> Result { + let empty = Map::new(); + let args = match &arguments { + Value::Object(m) => m, + Value::Null => &empty, + _ => { + return Err(ToolError::InvalidArguments( + "arguments must be a JSON object".into(), + )) + } + }; + // Xona endpoints are POST and read a JSON body; forward the call + // arguments verbatim. An empty object is still sent so the + // endpoint receives a well-formed body. + let body = Some(Value::Object(args.clone())); + + let req = PaidRequest { + provider: PROVIDER.to_string(), + slug: self.endpoint.slug.clone(), + url: self.endpoint.url.clone(), + method: self.endpoint.method.clone(), + body, + network: self.network.clone(), + asset: self.asset.clone(), + per_call_cap: self.per_call_cap, + credits: self.endpoint.credits(), + price_micro_usdc: self.endpoint.price_micro_usdc, + pay_to: self.endpoint.pay_to.clone(), + }; + + match self.executor.execute(req).await { + Ok(resp) if (200..300).contains(&resp.status) => { + let mut content = vec![Content::json(resp.body)]; + if let Some(rid) = resp.receipt_id { + content.push(Content::text(format!("x402 settled; receipt {rid}"))); + } + Ok(ToolCallResult::ok(content)) + } + Ok(resp) => Ok(ToolCallResult::error(format!( + "xona {} returned status {}", + self.endpoint.slug, resp.status + ))), + Err(e) => Ok(ToolCallResult::error(format!("xona paid call failed: {e}"))), + } + } +} + +/// Build one tool from one endpoint, wired to `executor`. The per-call +/// cap comes from the config when set, otherwise the endpoint's +/// published price (so the gateway accepts an at-price 402). +fn build_tool( + config: &XonaConfig, + ep: &XonaEndpoint, + executor: Arc, +) -> Arc { + let cap = if config.per_call_cap > 0 { + config.per_call_cap + } else { + ep.price_micro_usdc + }; + let price = ep.price_micro_usdc as f64 / 1_000_000.0; + let summary = if ep.description.is_empty() { + ep.slug.clone() + } else { + ep.description.clone() + }; + Arc::new(XonaEndpointTool { + name: ep.tool_name(), + description: format!("{summary} (paid: ${price} USDC via x402)"), + endpoint: ep.clone(), + network: config.network.clone(), + asset: config.asset.clone(), + per_call_cap: cap, + executor, + }) +} + +/// Generate one tool per catalog endpoint, wired to `executor`. +pub fn xona_tools( + catalog: &XonaCatalog, + config: &XonaConfig, + executor: Arc, +) -> Vec> { + catalog + .endpoints() + .iter() + .map(|ep| build_tool(config, ep, executor.clone())) + .collect() +} + +/// Build just the named tool, or `None` if no endpoint maps to it. Lets +/// the daemon bind a payer-scoped executor per call without materialising +/// the whole tool set. +pub fn xona_tool( + catalog: &XonaCatalog, + config: &XonaConfig, + name: &str, + executor: Arc, +) -> Option> { + catalog + .endpoints() + .iter() + .find(|ep| ep.tool_name() == name) + .map(|ep| build_tool(config, ep, executor)) +} + +/// Tool specs for discovery, without binding an executor — what a host +/// advertises in `tools/list` before any call binds a payer. +pub fn xona_specs(catalog: &XonaCatalog, config: &XonaConfig) -> Vec { + xona_tools(catalog, config, Arc::new(SpecOnly)) + .iter() + .map(|t| t.spec()) + .collect() +} + +struct SpecOnly; + +#[async_trait] +impl PaidExecutor for SpecOnly { + async fn execute(&self, _req: PaidRequest) -> std::result::Result { + Err("spec-only executor cannot make calls".into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + #[derive(Default)] + struct MockExecutor { + last: Mutex>, + status: u16, + } + + #[async_trait] + impl PaidExecutor for MockExecutor { + async fn execute(&self, req: PaidRequest) -> Result { + *self.last.lock().unwrap() = Some(req); + Ok(PaidResponse { + status: if self.status == 0 { 200 } else { self.status }, + body: serde_json::json!({ "ok": true }), + receipt_id: Some("rcpt-1".into()), + }) + } + } + + fn tools_for(config: &XonaConfig, exec: Arc) -> Vec> { + let cat = XonaCatalog::from_vendored(config).unwrap(); + xona_tools(&cat, config, exec) + } + + fn find<'a>(tools: &'a [Arc], name: &str) -> &'a Arc { + tools + .iter() + .find(|t| t.name() == name) + .expect("tool present") + } + + #[test] + fn generates_a_tool_per_solana_endpoint() { + let tools = tools_for(&XonaConfig::default(), Arc::new(MockExecutor::default())); + // 37 Solana-settled endpoints in the vendored snapshot. + assert_eq!(tools.len(), 37); + assert!(tools + .iter() + .any(|t| t.name() == "xona.image.creative-director")); + assert!(tools + .iter() + .any(|t| t.name() == "xona.audio.speech-to-text")); + // The Base-only endpoints never become tools. + assert!(!tools.iter().any(|t| t.name().starts_with("xona.base."))); + } + + #[test] + fn schema_is_permissive_object_with_prompt() { + let tools = tools_for(&XonaConfig::default(), Arc::new(MockExecutor::default())); + let schema = find(&tools, "xona.image.creative-director").input_schema(); + assert_eq!(schema["type"], "object"); + assert_eq!(schema["additionalProperties"], Value::Bool(true)); + assert_eq!(schema["properties"]["prompt"]["type"], "string"); + } + + #[tokio::test] + async fn forwards_arguments_as_body_and_carries_pricing() { + let exec = Arc::new(MockExecutor::default()); + let tools = tools_for(&XonaConfig::default(), exec.clone()); + let res = find(&tools, "xona.image.creative-director") + .call(serde_json::json!({ "prompt": "a neon koi" })) + .await + .unwrap(); + assert!(!res.is_error); + let req = exec.last.lock().unwrap().clone().unwrap(); + assert_eq!(req.provider, "xona"); + assert_eq!(req.slug, "image/creative-director"); + assert_eq!(req.method, "POST"); + assert_eq!( + req.url, + "https://api.xona-agent.com/image/creative-director" + ); + assert_eq!(req.network, crate::config::SOLANA_NETWORK); + assert_eq!(req.asset, crate::config::USDC_MINT); + assert_eq!(req.pay_to, crate::config::PAY_TO); + assert_eq!(req.body.unwrap()["prompt"], "a neon koi"); + // 30000 atomic USDC → per-call cap defers to price, 3 credits. + assert_eq!(req.per_call_cap, 30_000); + assert_eq!(req.credits, 3); + } + + #[tokio::test] + async fn non_object_arguments_are_invalid() { + let tools = tools_for(&XonaConfig::default(), Arc::new(MockExecutor::default())); + let err = find(&tools, "xona.image.creative-director") + .call(serde_json::json!("just a string")) + .await + .unwrap_err(); + assert!(matches!(err, ToolError::InvalidArguments(_))); + } + + #[tokio::test] + async fn upstream_error_status_surfaces_as_tool_error() { + let exec = Arc::new(MockExecutor { + status: 502, + ..Default::default() + }); + let tools = tools_for(&XonaConfig::default(), exec); + let res = find(&tools, "xona.image.creative-director") + .call(serde_json::json!({ "prompt": "x" })) + .await + .unwrap(); + assert!(res.is_error); + } + + #[tokio::test] + async fn spec_only_executor_refuses_to_call() { + let tools = tools_for(&XonaConfig::default(), Arc::new(SpecOnly)); + let res = find(&tools, "xona.image.creative-director") + .call(serde_json::json!({ "prompt": "x" })) + .await + .unwrap(); + assert!(res.is_error); + let Content::Text { text } = &res.content[0] else { + panic!("expected text content, got {:?}", res.content); + }; + assert!(text.contains("spec-only executor cannot make calls")); + } +} diff --git a/agent-os/crates/covenant-xona/src/x402.rs b/agent-os/crates/covenant-xona/src/x402.rs new file mode 100644 index 000000000..c772436ab --- /dev/null +++ b/agent-os/crates/covenant-xona/src/x402.rs @@ -0,0 +1,403 @@ +//! Xona's x402 challenge handling and the 402-then-pay loop. +//! +//! Xona serves the mainline x402 shape: a `402` body that is either a +//! bare array of payment options or `{"accepts": [ … ]}`, each option an +//! [`covenant_x402::PaymentRequirements`] with `amount`, `payTo`, and a +//! CAIP-2 `network`. Unlike the Hyre profile there is no sponsor +//! `feePayer`: Xona's Solana endpoints are self-paid, so the option's +//! `extra` is absent and the daemon's signer sidecar settles with +//! `SolanaSigner` (the funder pays its own fees). +//! +//! What's reused from `covenant-x402`: the [`Signer`] trait (the +//! funding-key sidecar) and the [`PaymentRequirements`] type it consumes. +//! Budget, settlement, and audit accounting wrap this loop in the daemon. + +use covenant_x402::{PaymentRequirements, Signer}; +use serde_json::Value; +use tracing::debug; + +use crate::tools::PaidRequest; +use crate::{Result, XonaError}; + +/// Outcome of the loop. `paid_amount` is the atomic amount actually +/// settled (the live, authoritative figure recorded on the receipt); +/// `None` means the endpoint answered 2xx without a 402 — a free call +/// that needs no settlement. +#[derive(Debug, Clone, PartialEq)] +pub struct PaidHttp { + pub status: u16, + pub body: String, + pub paid_amount: Option, +} + +/// Parse a Xona 402 body into its payment options. Tolerates both the +/// `{"accepts": [...]}` object and a bare `[...]` array. +pub fn parse_challenge(body: &str) -> Result> { + let value: Value = serde_json::from_str(body) + .map_err(|e| XonaError::Challenge(format!("decode 402 body: {e}")))?; + let options = if value.is_array() { + value + } else { + value + .get("accepts") + .cloned() + .ok_or_else(|| XonaError::Challenge("402 body has no accepts array".into()))? + }; + serde_json::from_value(options) + .map_err(|e| XonaError::Challenge(format!("decode accepts: {e}"))) +} + +/// First option that settles on the caller's `(network, asset)` for an +/// `exact` payment within `per_call_cap`. Network match is lenient across +/// the short (`solana`) and CAIP-2 (`solana:…`) spellings. +pub fn select<'a>( + options: &'a [PaymentRequirements], + network: &str, + asset: &str, + per_call_cap: u128, +) -> Option<&'a PaymentRequirements> { + options.iter().find(|o| { + o.scheme == "exact" + && o.asset == asset + && network_matches(&o.network, network) + && o.amount + .parse::() + .ok() + .is_some_and(|n| n <= per_call_cap) + }) +} + +fn network_matches(option: &str, want: &str) -> bool { + option == want + || want.starts_with(&format!("{option}:")) + || option.starts_with(&format!("{want}:")) +} + +/// Normalise a selected option into the signer's input. The payee is +/// pinned to the registry-advertised `expected_pay_to` so a manipulated +/// 402 challenge can't steer the funding key to another address, and the +/// network is forced to the operator's CAIP-2 id (the on-chain rail). +/// `extra` is carried through unchanged — absent for Xona's self-paid +/// Solana scheme, which routes the sidecar to `SolanaSigner`. +fn to_requirements( + option: &PaymentRequirements, + caip2_network: &str, + expected_pay_to: &str, +) -> Result { + if !expected_pay_to.is_empty() && option.pay_to != expected_pay_to { + return Err(XonaError::NotAllowed(format!( + "challenge payTo {} does not match the registry-advertised Xona payee {}", + option.pay_to, expected_pay_to + ))); + } + Ok(PaymentRequirements { + network: caip2_network.to_string(), + asset: option.asset.clone(), + amount: option.amount.clone(), + amount_usdc: option.amount_usdc, + pay_to: option.pay_to.clone(), + scheme: option.scheme.clone(), + extra: option.extra.clone(), + }) +} + +/// Run the 402-then-pay loop for one resolved Xona call. +/// +/// A first hit with no payment header either returns 2xx (free — no +/// settlement) or a 402 challenge. On 402 the matching option is signed +/// by `signer` and the request is retried once with the `x-payment` +/// header. Selection or payee-pin failures surface as +/// [`XonaError::NotAllowed`] — the call is rejected before the signer +/// runs, so no payment leaves the host for an out-of-policy option. +pub async fn execute_paid( + http: &reqwest::Client, + signer: &dyn Signer, + plan: &PaidRequest, +) -> Result { + let method = reqwest::Method::from_bytes(plan.method.as_bytes()) + .map_err(|_| XonaError::Execute(format!("invalid HTTP method: {:?}", plan.method)))?; + + let first = send(http, &method, &plan.url, plan.body.as_ref(), None).await?; + let status = first.status(); + if status.is_success() { + let body = first.text().await?; + return Ok(PaidHttp { + status: status.as_u16(), + body, + paid_amount: None, + }); + } + if status.as_u16() != 402 { + let body = first.text().await.unwrap_or_default(); + return Err(XonaError::Execute(format!( + "{} returned {} (not 402): {}", + plan.url, + status.as_u16(), + truncate(&body) + ))); + } + + let challenge = first.text().await?; + let options = parse_challenge(&challenge)?; + let option = + select(&options, &plan.network, &plan.asset, plan.per_call_cap).ok_or_else(|| { + XonaError::NotAllowed(format!( + "no x402 option on {} / {} within cap {} atomic", + plan.network, plan.asset, plan.per_call_cap + )) + })?; + + let requirements = to_requirements(option, &plan.network, &plan.pay_to)?; + debug!(pay_to = %requirements.pay_to, amount = %requirements.amount, url = %plan.url, "xona x402 option selected; signing"); + + let header = signer + .build_payment(&requirements) + .await + .map_err(|e| XonaError::Execute(e.to_string()))?; + + let paid = send(http, &method, &plan.url, plan.body.as_ref(), Some(&header)).await?; + let status = paid.status(); + let body = paid.text().await?; + let paid_amount = if status.is_success() { + Some(requirements.amount) + } else { + None + }; + Ok(PaidHttp { + status: status.as_u16(), + body, + paid_amount, + }) +} + +async fn send( + http: &reqwest::Client, + method: &reqwest::Method, + url: &str, + body: Option<&Value>, + payment_header: Option<&str>, +) -> Result { + let mut req = http.request(method.clone(), url); + if let Some(b) = body { + req = req.json(b); + } + if let Some(h) = payment_header { + req = req.header("x-payment", h); + } + Ok(req.send().await?) +} + +fn truncate(s: &str) -> String { + let cut: String = s.chars().take(200).collect(); + if cut.len() < s.len() { + format!("{cut}…") + } else { + cut + } +} + +#[cfg(test)] +mod tests { + use super::*; + use covenant_x402::MockSigner; + use wiremock::matchers::{header_exists, method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + const NETWORK: &str = crate::config::SOLANA_NETWORK; + const ASSET: &str = crate::config::USDC_MINT; + const PAY_TO: &str = crate::config::PAY_TO; + + /// A live-shaped Xona 402 challenge (bare array, no feePayer). + fn challenge_body() -> String { + serde_json::json!([{ + "network": NETWORK, + "asset": ASSET, + "amount": "30000", + "amountUsdc": 0.03, + "payTo": PAY_TO, + "scheme": "exact" + }]) + .to_string() + } + + fn plan(url: &str, cap: u128) -> PaidRequest { + PaidRequest { + provider: "xona".into(), + slug: "image/creative-director".into(), + url: url.into(), + method: "POST".into(), + body: Some(serde_json::json!({ "prompt": "x" })), + network: NETWORK.into(), + asset: ASSET.into(), + per_call_cap: cap, + credits: 3, + price_micro_usdc: 30_000, + pay_to: PAY_TO.into(), + } + } + + #[test] + fn parses_bare_array_and_object_shaped_challenges() { + let arr = parse_challenge(&challenge_body()).expect("array"); + assert_eq!(arr.len(), 1); + assert_eq!(arr[0].amount, "30000"); + let obj = + parse_challenge(&format!("{{\"accepts\": {}}}", challenge_body())).expect("object"); + assert_eq!(obj[0].pay_to, PAY_TO); + } + + #[test] + fn parse_challenge_rejects_malformed() { + assert!(matches!( + parse_challenge("{not json"), + Err(XonaError::Challenge(_)) + )); + assert!(matches!( + parse_challenge(r#"{"error":"pay up"}"#), + Err(XonaError::Challenge(m)) if m.contains("no accepts array") + )); + } + + #[test] + fn select_respects_cap_asset_and_network() { + let opts = parse_challenge(&challenge_body()).unwrap(); + assert!(select(&opts, NETWORK, ASSET, 30_000).is_some()); + assert!(select(&opts, NETWORK, ASSET, 29_999).is_none(), "over cap"); + assert!( + select(&opts, NETWORK, "OTHER", 30_000).is_none(), + "wrong asset" + ); + } + + #[test] + fn to_requirements_pins_payee() { + let opt = parse_challenge(&challenge_body()).unwrap()[0].clone(); + to_requirements(&opt, NETWORK, PAY_TO).expect("pinned payee normalises"); + let err = to_requirements(&opt, NETWORK, "SomeOtherPayee").expect_err("payee mismatch"); + assert!(matches!(err, XonaError::NotAllowed(m) if m.contains("does not match"))); + } + + #[tokio::test] + async fn full_loop_pays_self_paid_shape() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/image/creative-director")) + .respond_with(ResponseTemplate::new(402).set_body_string(challenge_body())) + .up_to_n_times(1) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/image/creative-director")) + .and(header_exists("x-payment")) + .respond_with( + ResponseTemplate::new(200) + .set_body_json(serde_json::json!({ "image_url": "https://x/y.png" })), + ) + .mount(&server) + .await; + + let out = execute_paid( + &reqwest::Client::new(), + &MockSigner, + &plan(&format!("{}/image/creative-director", server.uri()), 30_000), + ) + .await + .expect("paid"); + assert_eq!(out.status, 200); + assert_eq!(out.paid_amount.as_deref(), Some("30000")); + let body: Value = serde_json::from_str(&out.body).unwrap(); + assert_eq!(body["image_url"], "https://x/y.png"); + } + + #[tokio::test] + async fn over_cap_rejected_before_payment() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/c")) + .respond_with(ResponseTemplate::new(402).set_body_string(challenge_body())) + .mount(&server) + .await; + let err = execute_paid( + &reqwest::Client::new(), + &MockSigner, + &plan(&format!("{}/c", server.uri()), 29_999), + ) + .await + .expect_err("over cap"); + assert!(matches!(err, XonaError::NotAllowed(_)), "got {err:?}"); + } + + #[tokio::test] + async fn payee_mismatch_rejected_before_signing() { + let server = MockServer::start().await; + // Challenge pays a different address than the plan's pinned payee. + let hostile = serde_json::json!([{ + "network": NETWORK, "asset": ASSET, "amount": "30000", "amountUsdc": 0.03, + "payTo": "So11111111111111111111111111111111111111112", "scheme": "exact" + }]) + .to_string(); + Mock::given(method("POST")) + .and(path("/c")) + .respond_with(ResponseTemplate::new(402).set_body_string(hostile)) + .mount(&server) + .await; + let err = execute_paid( + &reqwest::Client::new(), + &MockSigner, + &plan(&format!("{}/c", server.uri()), 30_000), + ) + .await + .expect_err("payee mismatch"); + assert!( + matches!(err, XonaError::NotAllowed(ref m) if m.contains("payTo")), + "got {err:?}" + ); + } + + #[tokio::test] + async fn free_2xx_needs_no_payment() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/c")) + .respond_with( + ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })), + ) + .mount(&server) + .await; + let out = execute_paid( + &reqwest::Client::new(), + &MockSigner, + &plan(&format!("{}/c", server.uri()), 30_000), + ) + .await + .expect("free"); + assert_eq!(out.status, 200); + assert!(out.paid_amount.is_none()); + } + + #[tokio::test] + async fn rejected_payment_records_no_amount() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/c")) + .respond_with(ResponseTemplate::new(402).set_body_string(challenge_body())) + .up_to_n_times(1) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/c")) + .and(header_exists("x-payment")) + .respond_with(ResponseTemplate::new(402).set_body_string("payment rejected")) + .mount(&server) + .await; + let out = execute_paid( + &reqwest::Client::new(), + &MockSigner, + &plan(&format!("{}/c", server.uri()), 30_000), + ) + .await + .expect("loop returns rejection"); + assert_eq!(out.status, 402); + assert!(out.paid_amount.is_none()); + } +} diff --git a/agent-os/crates/covenantd/Cargo.toml b/agent-os/crates/covenantd/Cargo.toml index 79382504a..25e6d7229 100644 --- a/agent-os/crates/covenantd/Cargo.toml +++ b/agent-os/crates/covenantd/Cargo.toml @@ -33,6 +33,7 @@ covenant-settlement = { path = "../covenant-settlement" } # feature in a separate change. covenant-x402 = { path = "../covenant-x402" } covenant-hyre = { path = "../covenant-hyre" } +covenant-xona = { path = "../covenant-xona" } anyhow = { workspace = true } async-trait = { workspace = true } axum = { workspace = true } diff --git a/agent-os/crates/covenantd/src/lib.rs b/agent-os/crates/covenantd/src/lib.rs index b1e5bda76..9311132e1 100644 --- a/agent-os/crates/covenantd/src/lib.rs +++ b/agent-os/crates/covenantd/src/lib.rs @@ -14,6 +14,7 @@ pub mod sse; pub mod stream_dispatch; pub mod stream_tracker; pub mod x402; +pub mod xona; use anyhow::{Context, Result}; use covenant_a2a::Mailbox; @@ -1060,6 +1061,10 @@ pub struct Server { /// None when the operator has not enabled Hyre; in that state no /// `hyre.*` tool is advertised or callable. hyre: Option>, + /// Opt-in Xona Agent provider profile: the materialised catalog + + /// config. None when the operator has not enabled Xona; in that state + /// no `xona.*` tool is advertised or callable. + xona: Option>, /// Opt-in Synapse Agent Protocol bridge. `None` when no operator /// has wired it in (the default); a built [`SapBridge`] when /// `Server::with_sap_bridge` was called at boot. Handlers that @@ -1111,6 +1116,7 @@ impl Server { home: None, x402_dispatch: None, hyre: None, + xona: None, sap_bridge: None, intent_outcomes: Arc::new(std::sync::Mutex::new(OutcomeStore::default())), } @@ -1196,6 +1202,16 @@ impl Server { self } + /// Enable the Xona Agent provider profile. Advertises one `xona.*` + /// MCP tool per catalog endpoint and routes their calls through the + /// outbound x402 path. Requires [`Self::with_x402_dispatch`] for the + /// funding-key sidecar; without it a `xona.*` call returns a + /// "not configured" error. + pub fn with_xona(mut self, state: xona::XonaState) -> Self { + self.xona = Some(Arc::new(state)); + self + } + pub fn with_budget_checkpoints(mut self, store: Arc) -> Self { self.budget_checkpoints = Some(store); self @@ -3699,6 +3715,9 @@ impl Server { if let Some(state) = &self.hyre { tools.extend(covenant_hyre::hyre_specs(&state.catalog, &state.config)); } + if let Some(state) = &self.xona { + tools.extend(covenant_xona::xona_specs(&state.catalog, &state.config)); + } Response::ToolList { tools } } @@ -3764,6 +3783,9 @@ impl Server { if name.starts_with("hyre.") { return self.hyre_tool_call(name, arguments, peer).await; } + if name.starts_with("xona.") { + return self.xona_tool_call(name, arguments, peer).await; + } match self.tools.call(&name, arguments).await { Ok(r) => Response::ToolResult { @@ -3825,6 +3847,55 @@ impl Server { } } + /// Execute a Xona tool on the caller's behalf. The `tool.call.` + /// capability and scope are already enforced by [`Self::call_tool`]; + /// this binds the caller as payer and runs the resolved call through + /// the outbound x402 path, so the budget debit, settlement receipt, + /// and audit event land against the agent that invoked the tool. + async fn xona_tool_call( + &self, + name: String, + arguments: serde_json::Value, + peer: &AgentId, + ) -> Response { + let Some(state) = self.xona.clone() else { + return Response::Error { + message: "xona provider is not enabled on this daemon.".into(), + }; + }; + let Some(x402) = self.x402_dispatch.clone() else { + return Response::Error { + message: "xona requires the x402 funding-key sidecar. \ + Wire it via Server::with_x402_dispatch and restart." + .into(), + }; + }; + + let executor = Arc::new(xona::DaemonXonaExecutor::new( + self.settlement.clone(), + self.audit.clone(), + self.budget.clone(), + x402, + self.identity.agent_id(), + peer.clone(), + )); + let Some(tool) = covenant_xona::xona_tool(&state.catalog, &state.config, &name, executor) + else { + return Response::Error { + message: format!("unknown xona tool: {name}"), + }; + }; + match tool.call(arguments).await { + Ok(r) => Response::ToolResult { + content: r.content, + is_error: r.is_error, + }, + Err(e) => Response::Error { + message: format!("tool: {e}"), + }, + } + } + async fn tool_call_scope_allows( &self, action: &str, @@ -46002,6 +46073,159 @@ required = {caps:?} ); } + /// Full Xona path: capability gate → executor → 402-then-pay loop + /// (against Xona's self-paid challenge shape, no feePayer) → budget + /// debit + settlement receipt + audit event. The signer is a shell + /// script standing in for the funding-key sidecar, so no real USDC + /// moves. The catalog is built from a snapshot pointed at the mock + /// upstream. + #[cfg(unix)] + #[tokio::test] + async fn xona_tool_call_pays_and_records_end_to_end() { + use std::os::unix::fs::PermissionsExt; + use wiremock::matchers::{header_exists, method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + let upstream = MockServer::start().await; + // Xona's self-paid 402: bare array, no extra.feePayer. + let challenge = serde_json::json!([{ + "scheme": "exact", + "network": covenant_xona::config::SOLANA_NETWORK, + "asset": covenant_xona::config::USDC_MINT, + "amount": "30000", + "amountUsdc": 0.03, + "payTo": covenant_xona::config::PAY_TO, + }]) + .to_string(); + Mock::given(method("POST")) + .and(path("/image/creative-director")) + .respond_with(ResponseTemplate::new(402).set_body_string(challenge)) + .up_to_n_times(1) + .mount(&upstream) + .await; + Mock::given(method("POST")) + .and(path("/image/creative-director")) + .and(header_exists("x-payment")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "image_url": "https://x/y.png" + }))) + .mount(&upstream) + .await; + + let dir = tempfile::tempdir().unwrap(); + let signer = dir.path().join("signer.sh"); + std::fs::write( + &signer, + "#!/bin/sh\ncat >/dev/null\nprintf 'x402-mock-header'\n", + ) + .unwrap(); + std::fs::set_permissions(&signer, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let settlement = Arc::new(InMemorySettlement::new()); + let audit = Arc::new(covenant_audit::InMemoryAuditLog::new()); + let budget = Arc::new(covenant_budget::InMemoryLedger::new()); + let identity = Arc::new(LocalIdentity::generate("user@local")); + + let cfg = covenant_xona::XonaConfig { + enabled: true, + ..Default::default() + }; + // Snapshot points the endpoint at the mock upstream. + let snapshot = serde_json::json!([{ + "serverUrl": "https://api.xona-agent.com", + "serverTitle": "Xona Agent | Infrastructure for Agentic Commerce", + "endpoint": format!("{}/image/creative-director", upstream.uri()), + "slug": "image/creative-director", + "method": "POST", + "description": "creative director", + "pricing": [{ + "network": covenant_xona::config::SOLANA_NETWORK, + "asset": covenant_xona::config::USDC_MINT, + "amount": "30000", "amountUsdc": 0.03, + "payTo": covenant_xona::config::PAY_TO, "scheme": "exact" + }] + }]) + .to_string(); + let catalog = covenant_xona::XonaCatalog::from_snapshot(&snapshot, &cfg).unwrap(); + + let s = Server::new( + Arc::new(Router::from_cards(vec![])), + Arc::new(MockRunner::new("")), + Arc::new(InMemoryStore::new()), + settlement.clone(), + audit.clone(), + Arc::new(covenant_permissions::InMemoryCapabilityStore::new()), + Arc::new(covenant_llm::MockEmbedder::new(64)), + identity.clone(), + Arc::new(IgnoreSet::default()), + Arc::new(ToolRegistry::default()), + Arc::new(covenant_a2a::InMemoryMailbox::new()), + Arc::new(covenant_peer_auth::InMemoryPeerRegistry::new()), + budget.clone(), + ) + .with_x402_dispatch(x402::X402Config { + enabled: true, + signer_binary: signer, + signer_env: vec![], + }) + .with_xona(xona::XonaState::new(catalog, cfg)); + + let peer = identity.agent_id(); + budget.set_capacity(&peer, 1000).await.unwrap(); + s.op_respond(Request::GrantCapability { + action: "tool.call.xona.image.creative-director".into(), + scope: None, + expires_at: None, + }) + .await; + + let resp = s + .op_respond(Request::CallTool { + name: "xona.image.creative-director".into(), + arguments: serde_json::json!({ "prompt": "a neon koi" }), + }) + .await; + + match resp { + Response::ToolResult { content, is_error } => { + assert!(!is_error, "expected success, got {content:?}"); + let data = content + .iter() + .find_map(|c| match c { + covenant_mcp::Content::Json { value } => Some(value.clone()), + _ => None, + }) + .expect("json content"); + assert_eq!(data["image_url"], "https://x/y.png"); + } + other => panic!("expected ToolResult, got {other:?}"), + } + + let receipts = settlement.recent(10).await.unwrap(); + assert_eq!(receipts.len(), 1, "one settlement receipt"); + assert_eq!(receipts[0].resource, covenant_types::ResourceKind::Tool); + assert_eq!(receipts[0].credits_consumed, 3, "$0.03 → 3 credits"); + + let events = audit.recent(20).await.unwrap(); + let settled = events + .iter() + .find_map(|e| match &e.kind { + AuditKind::ExternalPaymentSettled { + provider, amount, .. + } => Some((provider.clone(), amount.clone())), + _ => None, + }) + .expect("ExternalPaymentSettled audit event"); + assert_eq!(settled.0, "xona"); + assert_eq!(settled.1, "30000", "records the live atomic amount"); + + assert_eq!( + budget.tokens_remaining(&peer).await.unwrap(), + 997, + "3 credits debited from the caller" + ); + } + #[tokio::test] async fn call_tool_accepts_matching_scope_arguments() { let s = server_with(vec![], ""); diff --git a/agent-os/crates/covenantd/src/main.rs b/agent-os/crates/covenantd/src/main.rs index cd3064ebc..734ada31e 100644 --- a/agent-os/crates/covenantd/src/main.rs +++ b/agent-os/crates/covenantd/src/main.rs @@ -355,6 +355,66 @@ async fn main() -> Result<()> { None => server, }; + let server = match xona_config_from_env() { + Some(cfg) => { + // Prefer the live orbit-x402 registry so a restart picks up + // Xona's current endpoints; fall back to the vendored snapshot + // offline or if the crawl is slow. + let http = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + let orbit = covenant_x402::OrbitClient::with( + http, + covenant_x402::orbit::DEFAULT_BASE_URL.to_string(), + covenant_x402::orbit::DEFAULT_PAGE_SIZE, + ); + let refreshed = tokio::time::timeout( + std::time::Duration::from_secs(20), + covenant_xona::XonaCatalog::refresh(&orbit, &cfg), + ) + .await; + let catalog = match refreshed { + Ok(Ok(c)) if !c.is_empty() => { + info!( + source = "registry", + endpoints = c.endpoints().len(), + "xona catalog loaded" + ); + Some(c) + } + Ok(Ok(_)) => { + tracing::warn!( + "xona registry returned no endpoints on rail; using vendored snapshot" + ); + covenant_xona::XonaCatalog::from_vendored(&cfg).ok() + } + Ok(Err(e)) => { + tracing::warn!(error = %e, "xona registry refresh failed; using vendored snapshot"); + covenant_xona::XonaCatalog::from_vendored(&cfg).ok() + } + Err(_) => { + tracing::warn!("xona registry refresh timed out; using vendored snapshot"); + covenant_xona::XonaCatalog::from_vendored(&cfg).ok() + } + }; + match catalog { + Some(catalog) if !catalog.is_empty() => { + info!( + endpoints = catalog.endpoints().len(), + "xona provider enabled" + ); + server.with_xona(covenantd::xona::XonaState::new(catalog, cfg)) + } + _ => { + tracing::warn!("xona catalog unavailable; provider disabled"); + server + } + } + } + None => server, + }; + server .register_agent_budgets() .await @@ -737,6 +797,57 @@ fn hyre_config_from_env() -> Option { Some(cfg) } +/// Build the Xona provider config from env, or None when the operator +/// hasn't opted in. The catalog itself loads from the live orbit-x402 +/// registry (vendored snapshot fallback); these vars only tune the rail +/// and the spend policy. +/// +/// - `COVENANT_XONA_ENABLED` truthy (`1`, `true`, `yes`) +/// - `COVENANT_XONA_NETWORK` / `COVENANT_XONA_ASSET` — override the +/// settlement rail (optional; defaults to Solana mainnet + USDC) +/// - `COVENANT_XONA_PER_CALL_CAP` — atomic-USDC per-call ceiling (optional) +/// - `COVENANT_XONA_ALLOW` — comma-separated endpoint slug allowlist (optional) +/// - `COVENANT_XONA_SERVER_TITLE_PREFIX` — registry serverTitle prefix +/// that identifies Xona's entries (optional) +fn xona_config_from_env() -> Option { + let enabled = std::env::var("COVENANT_XONA_ENABLED") + .map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes")) + .unwrap_or(false); + if !enabled { + return None; + } + let mut cfg = covenant_xona::XonaConfig { + enabled: true, + ..Default::default() + }; + if let Ok(network) = std::env::var("COVENANT_XONA_NETWORK") { + cfg.network = network; + } + if let Ok(asset) = std::env::var("COVENANT_XONA_ASSET") { + cfg.asset = asset; + } + if let Ok(prefix) = std::env::var("COVENANT_XONA_SERVER_TITLE_PREFIX") { + cfg.server_title_prefix = prefix; + } + if let Ok(cap) = std::env::var("COVENANT_XONA_PER_CALL_CAP") { + match cap.trim().parse() { + Ok(n) => cfg.per_call_cap = n, + Err(_) => { + tracing::warn!(value = %cap, "ignoring non-numeric COVENANT_XONA_PER_CALL_CAP") + } + } + } + if let Ok(list) = std::env::var("COVENANT_XONA_ALLOW") { + cfg.allow = Some( + list.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(), + ); + } + Some(cfg) +} + /// Mask secret query params (api keys, tokens) in a URL before logging it, /// so a keyed RPC endpoint in `sap.env` never lands in stdout/journald. fn redact_url(url: &str) -> String { diff --git a/agent-os/crates/covenantd/src/xona.rs b/agent-os/crates/covenantd/src/xona.rs new file mode 100644 index 000000000..dd6607bde --- /dev/null +++ b/agent-os/crates/covenantd/src/xona.rs @@ -0,0 +1,253 @@ +//! Daemon glue for the Xona Agent x402 provider profile. +//! +//! `covenant-xona` owns the catalog and the MCP tool surface but knows +//! nothing about the daemon's funding key, budget, or settlement logs — +//! it reaches them through a [`covenant_xona::PaidExecutor`]. This module +//! is that implementation: [`DaemonXonaExecutor`] binds one caller (the +//! payer) to the daemon's accounting subsystems and routes a resolved +//! Xona call through the same [`crate::x402::record_paid_call`] path +//! every other outbound x402 payment uses. A Xona receipt is a plain +//! `ResourceKind::Tool` receipt — it rolls into the same Merkle batch and +//! optional Synapse mirror with no Xona-specific surface. +//! +//! The funding key never enters the daemon: signing is delegated to the +//! `covenant-x402-signer` sidecar via [`crate::x402::SubprocessSigner`]. +//! Xona's Solana endpoints are self-paid (no sponsor `feePayer`), so the +//! sidecar settles them with `SolanaSigner`. + +use std::sync::Arc; + +use async_trait::async_trait; +use covenant_audit::AuditLog; +use covenant_budget::{BudgetError, BudgetLedger}; +use covenant_settlement::Settlement; +use covenant_types::AgentId; +use covenant_x402::Capability; +use covenant_xona::{PaidExecutor, PaidRequest, PaidResponse, XonaCatalog, XonaConfig}; +use serde_json::Value; +use tracing::warn; + +use crate::x402::{record_paid_call, PaidCall, SettlementContext, X402Config}; + +/// Materialised Xona catalog plus its config, built once at daemon +/// startup and shared behind an `Arc`. Rebuilt out of band when the +/// daemon refreshes the registry. +pub struct XonaState { + pub catalog: XonaCatalog, + pub config: XonaConfig, +} + +impl XonaState { + pub fn new(catalog: XonaCatalog, config: XonaConfig) -> Self { + Self { catalog, config } + } +} + +/// A [`PaidExecutor`] bound to one payer and the daemon's accounting +/// subsystems. Constructed per tool call so the budget debit and +/// settlement receipt land against the agent that invoked the tool. +pub struct DaemonXonaExecutor { + settlement: Arc, + audit: Arc, + budget: Arc, + x402: Arc, + issuer: AgentId, + payer: AgentId, +} + +impl DaemonXonaExecutor { + pub fn new( + settlement: Arc, + audit: Arc, + budget: Arc, + x402: Arc, + issuer: AgentId, + payer: AgentId, + ) -> Self { + Self { + settlement, + audit, + budget, + x402, + issuer, + payer, + } + } +} + +#[async_trait] +impl PaidExecutor for DaemonXonaExecutor { + async fn execute(&self, req: PaidRequest) -> Result { + if !self.x402.enabled { + return Err("x402 outbound surface is disabled".into()); + } + let method = reqwest::Method::from_bytes(req.method.as_bytes()) + .map_err(|_| format!("invalid HTTP method: {:?}", req.method))?; + + // Read-only pre-check so the daemon never spends USDC for an + // agent that can't afford the credits. The authoritative debit + // happens inside record_paid_call after a successful response. + match self.budget.would_exceed(&self.payer, req.credits).await { + Ok(false) => {} + Ok(true) => return Err("payer budget would be exceeded by this call".into()), + Err(BudgetError::NoCapacity(_)) => { + return Err("payer has no budget capacity; refusing to spend".into()) + } + Err(e) => return Err(format!("budget: {e}")), + } + + let mut signer = crate::x402::SubprocessSigner::new(&self.x402.signer_binary); + for (k, v) in &self.x402.signer_env { + signer = signer.env(k.clone(), v.clone()); + } + + let http = reqwest::Client::new(); + let out = covenant_xona::execute_paid(&http, &signer, &req) + .await + .map_err(|e| e.to_string())?; + + let receipt_id = match &out.paid_amount { + // A 402 was answered and paid: record the live, authoritative + // amount against the caller. + Some(amount) if (200..300).contains(&out.status) => { + let capability = Capability { + provider: req.provider.clone(), + network: req.network.clone(), + asset: req.asset.clone(), + per_call_cap: req.per_call_cap, + }; + let call = PaidCall { + provider: &req.provider, + endpoint: &req.url, + method, + capability, + body: req.body.as_ref(), + amount: amount.clone(), + network: req.network.clone(), + asset: req.asset.clone(), + credits: req.credits, + }; + let ctx = SettlementContext { + settlement: self.settlement.as_ref(), + audit: self.audit.as_ref(), + budget: self.budget.as_ref(), + issuer: &self.issuer, + }; + match record_paid_call(&ctx, &self.payer, &call).await { + Ok(id) => Some(id), + Err(e) => { + // Payment already settled on-chain; failing to record + // it is an accounting gap, so surface it loudly. + warn!(error = %e, endpoint = %req.url, "xona paid call succeeded but accounting failed"); + return Err(e.to_string()); + } + } + } + // Free 2xx, or a paid attempt that didn't return success: no debit. + _ => None, + }; + + let body = serde_json::from_str(&out.body).unwrap_or(Value::String(out.body)); + Ok(PaidResponse { + status: out.status, + body, + receipt_id: receipt_id.map(|id| id.to_string()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use covenant_audit::InMemoryAuditLog; + use covenant_budget::InMemoryLedger; + use covenant_settlement::InMemorySettlement; + + fn agent(tag: u8) -> AgentId { + AgentId::new("agent@local", [tag; 32]) + } + + fn req() -> PaidRequest { + PaidRequest { + provider: "xona".into(), + slug: "image/creative-director".into(), + url: "https://api.xona-agent.com/image/creative-director".into(), + method: "POST".into(), + body: Some(serde_json::json!({ "prompt": "x" })), + network: covenant_xona::config::SOLANA_NETWORK.into(), + asset: covenant_xona::config::USDC_MINT.into(), + per_call_cap: 30_000, + credits: 3, + price_micro_usdc: 30_000, + pay_to: covenant_xona::config::PAY_TO.into(), + } + } + + fn enabled_x402() -> X402Config { + X402Config { + enabled: true, + signer_binary: "/nonexistent-signer".into(), + signer_env: vec![], + } + } + + /// With the funding sidecar disabled the executor must refuse before + /// any network or signer activity and write no accounting. + #[tokio::test] + async fn executor_refuses_when_x402_disabled() { + let settlement = Arc::new(InMemorySettlement::new()); + let audit = Arc::new(InMemoryAuditLog::new()); + let budget = Arc::new(InMemoryLedger::new()); + let payer = agent(1); + budget.set_capacity(&payer, 1000).await.unwrap(); + + let exec = DaemonXonaExecutor::new( + settlement.clone(), + audit.clone(), + budget.clone(), + Arc::new(X402Config::default()), // enabled = false + agent(9), + payer, + ); + + let err = exec.execute(req()).await.expect_err("disabled"); + assert!(err.contains("disabled"), "got: {err}"); + assert!(settlement.recent(10).await.unwrap().is_empty()); + assert!(audit.recent(10).await.unwrap().is_empty()); + } + + /// A payer with no budget bucket is refused before any network or + /// signer activity — the read-only pre-check fires first. + #[tokio::test] + async fn executor_refuses_payer_without_budget() { + let exec = DaemonXonaExecutor::new( + Arc::new(InMemorySettlement::new()), + Arc::new(InMemoryAuditLog::new()), + Arc::new(InMemoryLedger::new()), + Arc::new(enabled_x402()), + agent(9), + agent(1), // never given capacity + ); + let err = exec.execute(req()).await.expect_err("no capacity"); + assert!(err.contains("capacity"), "got: {err}"); + } + + #[tokio::test] + async fn executor_rejects_bad_method_after_budget_check() { + let budget = Arc::new(InMemoryLedger::new()); + let payer = agent(1); + budget.set_capacity(&payer, 1000).await.unwrap(); + let exec = DaemonXonaExecutor::new( + Arc::new(InMemorySettlement::new()), + Arc::new(InMemoryAuditLog::new()), + budget, + Arc::new(enabled_x402()), + agent(9), + payer, + ); + let mut bad = req(); + bad.method = "BAD METHOD".into(); // space is not a valid method token + let err = exec.execute(bad).await.expect_err("bad method"); + assert!(err.contains("invalid HTTP method"), "got: {err}"); + } +} diff --git a/docs/xona-integration.md b/docs/xona-integration.md new file mode 100644 index 000000000..9b2a48ea0 --- /dev/null +++ b/docs/xona-integration.md @@ -0,0 +1,168 @@ +# Xona Agent x402 Provider + +[Xona Agent](https://xona-agent.com) is exposed to Covenant agents as a +*provider profile* over the generic outbound x402 gateway (`covenant-x402`), +not as a parallel runtime. An agent calls Xona through capability-gated MCP +tools; the daemon signs the x402 payment, debits the agent's budget, and writes +a settlement receipt. The agent never holds the funding key. + +The profile lives in the `covenant-xona` crate plus a thin daemon bridge in +`covenantd::xona`. It adds no Xona-specific code to the core crates: settlement, +budgeting, audit, and attestation are reused from the existing x402 accounting +path that backs every paid call. + +Unlike the Hyre profile, Xona's Solana endpoints are **self-paid**: the 402 +challenge carries no sponsor `feePayer`, so the funder pays its own gas and the +signer sidecar settles with `SolanaSigner` (not PayAI's sponsored flow). + +## Quick start (operator) + +```sh +# 1. Build the funding-key sidecar (its own workspace; isolates solana-sdk). +cd agent-os/crates/covenant-x402-signer && cargo build --bin covenant-x402-signer + +# 2. Point a funded Solana keypair at it. The funder needs (a) a USDC token +# account that exists on chain, (b) enough USDC for the per-call price, and +# (c) a little SOL for gas — Xona is self-paid, there is no sponsor. +export COVENANT_X402_FUNDING_KEYPAIR=~/.config/solana/funder.json +export COVENANT_X402_RPC_URL=https://api.mainnet-beta.solana.com +export COVENANT_X402_SIGNER_BIN=$PWD/target/debug/covenant-x402-signer + +# 3. Resolve the catalog + 402 challenge for free (dry run, no payment). +cd ../../.. && cargo run -p covenant-xona --example live_paid_call + +# 4. Run a real paid call ($0.03 USDC against image/creative-director). +cargo run -p covenant-xona --example live_paid_call -- --confirm +``` + +To enable the profile on a real `covenantd` instance, set the env in the +[Configuration](#configuration) table and start the daemon — it will build the +catalog and register `xona.*` MCP tools. + +## Components + +| Concern | Where | Notes | +| --- | --- | --- | +| Endpoint catalog + pricing | `covenant-xona::catalog` | Filtered from the orbit-x402 registry. No rebuild when Xona adds endpoints. | +| MCP tools | `covenant-xona::tools` | One `xona.*` tool per endpoint, with a permissive `{prompt, …}` body schema. | +| 402 challenge + pay loop | `covenant-xona::x402` | Standard x402 challenge parsing, option selection with a payee pin, and the 402-then-pay loop. Reuses the `covenant_x402::Signer` sidecar. | +| Paid execution | `covenant-xona::PaidExecutor` → `covenantd::xona::DaemonXonaExecutor` | The daemon binds the caller as payer, runs the loop, and records accounting via `covenantd::x402::record_paid_call`. | +| Settlement / budget / audit | `covenantd::x402::record_paid_call` (unchanged) | A Xona call is a `ResourceKind::Tool` receipt recording the live atomic amount. Rolls into the same Merkle batch and optional Synapse mirror. | + +## Catalog + +Xona publishes its endpoints to the [orbit-x402 registry](https://api.orbitx402.com) +(`GET /api/services-list`), which Covenant already polls through +`covenant_x402::OrbitClient`. `covenant-xona` filters that registry down to +entries whose `serverTitle` starts with `Xona Agent` **and** that carry a +pricing option on the configured `(network, asset)` rail. + +Xona lists the same logical endpoints on three chains — Solana (bare slugs, +e.g. `image/creative-director`), Base mainnet (`base-main/*`), and Base testnet +(`base/*`). Covenant's funding key is a Solana keypair, so the default config +keeps only the **Solana-settled** endpoints (network `solana:5eykt4…`, asset +USDC `EPjFWdd5…`); the Base mirrors are dropped because the daemon cannot pay +them. Endpoints with no pricing on the rail are dropped too. Budget credits are +the cent value of the published price ($0.03 → 3). + +At startup the daemon crawls the live registry (20s timeout) and falls back to +the vendored snapshot at `assets/xona-orbit-snapshot.json` if the crawl fails or +is slow — so a restart picks up Xona's current Solana endpoints without a +rebuild, and an offline boot still has the catalog. The registry price is a +discovery hint; the authoritative amount comes from the live 402 challenge and +is what the receipt records. + +## x402 challenge format + +Xona serves the mainline x402 challenge shape: + +- the 402 body is a bare array of payment options (also tolerates + `{"accepts": [ … ]}`); +- each option is a `covenant_x402::PaymentRequirements`: `amount` (atomic), + `asset`, `payTo`, `scheme` (`"exact"`), and a CAIP-2 `network`; +- there is **no** `extra.feePayer` — the payment is self-paid. + +`covenant-xona::x402` parses this directly, matches the option on the operator's +`(network, asset)` within the per-call cap, **pins the option's `payTo` to the +registry-advertised Xona payee** (binding the live challenge to discovery so a +manipulated 402 cannot redirect funds), normalises it onto the operator's CAIP-2 +rail, and hands it to the `covenant_x402::Signer` sidecar. With no `feePayer` +present, the sidecar dispatches to `SolanaSigner`: the funder builds and signs a +legacy transfer, pays its own gas, and re-POSTs the request with the +base64 `x-payment` header. + +## Tools + +`xona.` names flatten the path: `image/creative-director` → +`xona.image.creative-director`, `tokens-api/asset-risk-summary` → +`xona.tokens-api.asset-risk-summary`. Xona's registry does not publish +per-endpoint argument shapes, so each tool surfaces a permissive object schema +with a documented `prompt` field and `additionalProperties: true`; the call +arguments are forwarded verbatim as the POST body. A tool marshals arguments +into a `PaidRequest` and hands it to the `PaidExecutor`; it never makes a +network call itself. + +## Capabilities and budget + +A `xona.*` call goes through the daemon's normal `call_tool` path and is gated +by the `tool.call.` capability and its scope — the per-endpoint allowlist +is the set of `tool.call.xona.*` capabilities an agent holds, and the TTL is the +capability's expiry. The per-call cap is the config cap (or the endpoint's +published price), enforced before the signer runs. Per-day and total budget are +the agent's budget-ledger capacity, debited on success. Out-of-policy calls are +rejected before they leave the host. + +The daemon binds the **caller** as payer per call, so the budget debit, +settlement receipt, and audit event (`ExternalPaymentSettled`) all land against +the agent that invoked the tool, sharing one receipt id. + +## Configuration + +The profile is opt-in and requires the x402 funding-key sidecar +(`Server::with_x402_dispatch`). + +### Daemon env + +| Var | Effect | +| --- | --- | +| `COVENANT_XONA_ENABLED` | Truthy (`1`/`true`/`yes`) enables the profile. | +| `COVENANT_XONA_NETWORK` / `COVENANT_XONA_ASSET` | Override the settlement rail (default Solana mainnet + USDC). **Warning:** the selector compares the option's `asset` against this value, so overriding it away from USDC effectively disables the asset pin. | +| `COVENANT_XONA_PER_CALL_CAP` | Atomic-USDC per-call ceiling. **Warning:** `0` defers to the registry-published price, which means a poisoned registry refresh could inflate the cap. Set explicitly (e.g. `35000` = $0.035 for the $0.03 `image/creative-director` endpoint). | +| `COVENANT_XONA_ALLOW` | Comma-separated endpoint-slug allowlist (`image/creative-director,audio/speech-to-text`). | +| `COVENANT_XONA_SERVER_TITLE_PREFIX` | Registry `serverTitle` prefix identifying Xona's entries (default `Xona Agent`). | +| `COVENANT_X402_SIGNER_BINARY` | Absolute path to the built `covenant-x402-signer` binary. Required when the Xona profile is enabled. | + +### Sidecar env (read inside the spawned signer process) + +| Var | Effect | +| --- | --- | +| `COVENANT_X402_FUNDING_KEYPAIR` | Path to the funder's Solana keypair JSON. Required. The funder must hold USDC, have an existing USDC ATA, and hold a little SOL for gas (Xona is self-paid). | +| `COVENANT_X402_RPC_URL` | Solana RPC for blockhash + ATA checks. Defaults to `https://api.mainnet-beta.solana.com`. | + +### Security pins (built into the code, not configurable) + +- **payTo:** `to_requirements` rejects any 402 whose `payTo` differs from the + registry-advertised Xona payee for that endpoint (`9VaDVp1…` on Solana). + Binds the live challenge to the discovery source and prevents fund + redirection if Xona's response is MITM'd. +- **Rail filter:** only endpoints priced on the configured `(network, asset)` + enter the catalog, so the Solana funding key is never asked to pay a Base + endpoint it cannot settle. + +### Live example + +| Example | What it does | Cost | +| --- | --- | --- | +| `cargo run -p covenant-xona --example live_paid_call` | Builds the catalog (live registry, vendored fallback), POSTs `image/creative-director` to fetch the 402, runs the selector, and prints the payee-pin match. | free | +| `cargo run -p covenant-xona --example live_paid_call -- --confirm` | Real $0.03 USDC paid call via `execute_paid` (the inner 402-then-pay loop, self-paid `SolanaSigner`). | $0.03 USDC | + +With the profile disabled, no `xona.*` tool is advertised or callable and no +Xona call ever leaves the host. + +## Settlement, attestation, and the SAP bridge + +Nothing Xona-specific exists on the settlement or attestation path. A Xona +receipt is an ordinary `ResourceKind::Tool` receipt; operators who have enabled +the Synapse attestation mirror get its root mirrored on the same path as every +other receipt, and operators with the mirror off see no on-chain footprint +beyond the x402 payment itself. diff --git a/landing/app/_partners.ts b/landing/app/_partners.ts index c3729facf..c1142a4c8 100644 --- a/landing/app/_partners.ts +++ b/landing/app/_partners.ts @@ -130,8 +130,9 @@ export const PARTNERS: Integration[] = [ slug: "xona", name: "Xona Agent", blurb: - "Xona Agent's creative generation endpoints reach agents as capability-scoped, x402-billed tools.", + "Xona Agent's image, audio, and token-intelligence endpoints reach agents as capability-scoped, x402-billed MCP tools.", status: "building", + href: "https://xona-agent.com", }, { slug: "orbserv", From a1909e5f8715c94c084a8194cecbb8f1ae5135fd Mon Sep 17 00:00:00 2001 From: Mizuki Hayashi <197570892+mizuki0x@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:11:44 +0200 Subject: [PATCH 2/2] feat(xona): add provider discovery, smart-layer scaffold, resale publish --- agent-os/crates/covenant-ipc/src/lib.rs | 106 ++++++ agent-os/crates/covenant-xona/src/config.rs | 10 + agent-os/crates/covenant-xona/src/lib.rs | 2 + agent-os/crates/covenant-xona/src/publish.rs | 87 +++++ agent-os/crates/covenantd/src/discovery.rs | 239 ++++++++++++ agent-os/crates/covenantd/src/lib.rs | 168 ++++++++ agent-os/crates/covenantd/src/main.rs | 72 ++++ agent-os/crates/covenantd/src/smart_layer.rs | 379 +++++++++++++++++++ docs/xona-integration.md | 19 + 9 files changed, 1082 insertions(+) create mode 100644 agent-os/crates/covenant-xona/src/publish.rs create mode 100644 agent-os/crates/covenantd/src/discovery.rs create mode 100644 agent-os/crates/covenantd/src/smart_layer.rs diff --git a/agent-os/crates/covenant-ipc/src/lib.rs b/agent-os/crates/covenant-ipc/src/lib.rs index e87fc451b..5d7d04836 100644 --- a/agent-os/crates/covenant-ipc/src/lib.rs +++ b/agent-os/crates/covenant-ipc/src/lib.rs @@ -50,6 +50,28 @@ pub struct ChainStatus { pub missing: Vec, } +/// One x402 provider endpoint surfaced by `Request::DiscoverProviders`. +/// A flat projection of `covenant_x402::RegistryEntry` plus its first +/// pricing option — enough for an agent to decide whether to pay the +/// endpoint without re-fetching the registry. `price_atomic_usdc` is the +/// atomic amount as a decimal string (mirrors `PayX402::per_call_cap`) to +/// avoid JSON's 53-bit integer limit; `network` + `asset` name the rail. +/// All three pricing fields are absent when the endpoint is free (the +/// registry entry carried no pricing). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct DiscoveredProvider { + pub server_title: String, + pub endpoint: String, + pub slug: String, + pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub price_atomic_usdc: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub network: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub asset: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ReceiptBatchSummary { pub batch_id: String, @@ -762,6 +784,17 @@ pub enum Request { #[serde(default)] expires_at_unix: Option, }, + /// Enumerate available x402 providers from the orbit-x402 registry. + /// Capability-gated by `x402.discover` and opt-in via env; when the + /// daemon was started without discovery wired in, the handler returns + /// [`Response::Error`]. The cached catalog is served (refreshed when + /// stale), filtered to `server_title` when set and to any operator + /// allowlist, and projected to [`DiscoveredProvider`]. Read-only — no + /// payment, no signer. + DiscoverProviders { + #[serde(default)] + server_title: Option, + }, } fn default_recent_limit() -> usize { @@ -1016,6 +1049,13 @@ pub enum Response { agent_pda: String, signature: String, }, + /// Successful response to [`Request::DiscoverProviders`]. Carries the + /// projected, filtered provider list from the cached orbit-x402 + /// catalog. An empty list is a valid answer (no providers matched, or + /// the registry returned none). + ProvidersDiscovered { + providers: Vec, + }, Error { message: String, }, @@ -11571,4 +11611,70 @@ mod tests { other => panic!("expected Ipc(Io(UnexpectedEof)), got {other:?}"), } } + + #[test] + fn request_discover_providers_serde_round_trips_with_and_without_filter() { + // Request::DiscoverProviders is the agent-facing verb that + // enumerates orbit-x402 providers. The discriminator slug is + // 'discover_providers'; server_title is #[serde(default)] so a + // stale CLI that omits it deserialises as None (no filter). + let unfiltered = Request::DiscoverProviders { server_title: None }; + let wire = serde_json::to_value(&unfiltered).unwrap(); + assert_eq!(wire["kind"], serde_json::json!("discover_providers")); + let back: Request = + serde_json::from_value(serde_json::json!({ "kind": "discover_providers" })).unwrap(); + assert_eq!( + back, unfiltered, + "a DiscoverProviders frame without server_title must deserialise as the unfiltered request" + ); + + let filtered = Request::DiscoverProviders { + server_title: Some("Xona".into()), + }; + let wire = serde_json::to_value(&filtered).unwrap(); + assert_eq!(wire["server_title"], serde_json::json!("Xona")); + let back: Request = serde_json::from_value(wire).unwrap(); + assert_eq!(back, filtered); + } + + #[test] + fn response_providers_discovered_serde_round_trips_priced_and_free() { + // The priced fields are Option + skip_serializing_if so a free + // endpoint (no pricing) drops price/network/asset from the wire + // entirely; a consumer distinguishes free from paid by absence. + let priced = DiscoveredProvider { + server_title: "Xona".into(), + endpoint: "https://api.xona-agent.com/image/creative-director".into(), + slug: "image/creative-director".into(), + method: "POST".into(), + price_atomic_usdc: Some("30000".into()), + network: Some("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp".into()), + asset: Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".into()), + }; + let free = DiscoveredProvider { + server_title: "Xona".into(), + endpoint: "https://api.xona-agent.com/free/ping".into(), + slug: "free/ping".into(), + method: "GET".into(), + price_atomic_usdc: None, + network: None, + asset: None, + }; + let resp = Response::ProvidersDiscovered { + providers: vec![priced.clone(), free.clone()], + }; + + let wire = serde_json::to_value(&resp).unwrap(); + assert_eq!(wire["kind"], serde_json::json!("providers_discovered")); + let free_wire = &wire["providers"][1]; + assert!( + free_wire.get("price_atomic_usdc").is_none() + && free_wire.get("network").is_none() + && free_wire.get("asset").is_none(), + "a free provider must drop the pricing fields from the wire" + ); + + let back: Response = serde_json::from_value(wire).unwrap(); + assert_eq!(back, resp); + } } diff --git a/agent-os/crates/covenant-xona/src/config.rs b/agent-os/crates/covenant-xona/src/config.rs index 8ff230f51..65af29cc9 100644 --- a/agent-os/crates/covenant-xona/src/config.rs +++ b/agent-os/crates/covenant-xona/src/config.rs @@ -45,6 +45,10 @@ pub struct XonaConfig { /// allows every catalog endpoint; `Some([])` allows none. #[serde(default)] pub allow: Option>, + /// Resale markup in basis points applied to the published price when + /// republishing tools through the SAP bridge. `0` = at cost. + #[serde(default)] + pub markup_bps: u16, } fn default_server_title_prefix() -> String { @@ -66,6 +70,7 @@ impl Default for XonaConfig { asset: default_asset(), per_call_cap: 0, allow: None, + markup_bps: 0, } } } @@ -83,6 +88,11 @@ impl XonaConfig { Some(list) => list.iter().any(|s| s == slug), } } + + /// Apply the resale markup to an atomic-USDC price. + pub fn marked_up(&self, price_micro_usdc: u128) -> u128 { + price_micro_usdc + price_micro_usdc * self.markup_bps as u128 / 10_000 + } } #[cfg(test)] diff --git a/agent-os/crates/covenant-xona/src/lib.rs b/agent-os/crates/covenant-xona/src/lib.rs index bbf676b98..b20f802aa 100644 --- a/agent-os/crates/covenant-xona/src/lib.rs +++ b/agent-os/crates/covenant-xona/src/lib.rs @@ -27,11 +27,13 @@ pub mod catalog; pub mod config; +pub mod publish; pub mod tools; pub mod x402; pub use catalog::{XonaCatalog, XonaEndpoint}; pub use config::XonaConfig; +pub use publish::{publishable, PublishableTool}; pub use tools::{xona_specs, xona_tool, xona_tools, PaidExecutor, PaidRequest, PaidResponse}; pub use x402::{execute_paid, parse_challenge, PaidHttp}; diff --git a/agent-os/crates/covenant-xona/src/publish.rs b/agent-os/crates/covenant-xona/src/publish.rs new file mode 100644 index 000000000..ff03335c9 --- /dev/null +++ b/agent-os/crates/covenant-xona/src/publish.rs @@ -0,0 +1,87 @@ +//! Optional resale publishing through the SAP bridge. +//! +//! If an operator runs the SAP bridge and flags a Xona tool as +//! published, remote agents can discover that this daemon resells paid +//! Xona access at a marked-up tier — the markup flowing back through +//! Covenant settlement. This module only derives the publishable +//! descriptors; the bridge owns the actual on-chain registration and is +//! gated by its own capability. With the bridge off, [`publishable`] +//! returns nothing and the rest of the profile is unaffected. + +use crate::catalog::XonaCatalog; +use crate::config::XonaConfig; + +/// A Xona tool offered for resale, priced at the operator's markup. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublishableTool { + pub tool_name: String, + pub slug: String, + pub description: String, + /// Xona's published price, atomic USDC. + pub base_price_micro_usdc: u128, + /// Resale price after the operator markup, atomic USDC. + pub resale_price_micro_usdc: u128, +} + +/// Derive the descriptors for the tools an operator has flagged for +/// resale. Returns empty when the bridge is off, so callers can wire +/// this unconditionally. `is_published` is the per-tool flag the +/// operator controls (by tool name). +pub fn publishable( + catalog: &XonaCatalog, + config: &XonaConfig, + bridge_enabled: bool, + is_published: impl Fn(&str) -> bool, +) -> Vec { + if !bridge_enabled { + return Vec::new(); + } + catalog + .endpoints() + .iter() + .filter(|ep| is_published(&ep.tool_name())) + .map(|ep| PublishableTool { + tool_name: ep.tool_name(), + slug: ep.slug.clone(), + description: if ep.description.is_empty() { + ep.slug.clone() + } else { + ep.description.clone() + }, + base_price_micro_usdc: ep.price_micro_usdc, + resale_price_micro_usdc: config.marked_up(ep.price_micro_usdc), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn catalog() -> XonaCatalog { + XonaCatalog::from_vendored(&XonaConfig::default()).unwrap() + } + + #[test] + fn bridge_off_publishes_nothing() { + let out = publishable(&catalog(), &XonaConfig::default(), false, |_| true); + assert!(out.is_empty()); + } + + #[test] + fn per_tool_flag_filters_and_markup_applies() { + let config = XonaConfig { + markup_bps: 5_000, // +50% + ..XonaConfig::default() + }; + let out = publishable(&catalog(), &config, true, |name| { + name == "xona.image.creative-director" + }); + assert_eq!(out.len(), 1); + let t = &out[0]; + assert_eq!(t.slug, "image/creative-director"); + // image/creative-director is $0.03 on Solana. + assert_eq!(t.base_price_micro_usdc, 30_000); + assert_eq!(t.resale_price_micro_usdc, 45_000); + } +} diff --git a/agent-os/crates/covenantd/src/discovery.rs b/agent-os/crates/covenantd/src/discovery.rs new file mode 100644 index 000000000..65136d6a0 --- /dev/null +++ b/agent-os/crates/covenantd/src/discovery.rs @@ -0,0 +1,239 @@ +//! Daemon glue for x402 provider discovery. +//! +//! Wraps the existing [`covenant_x402::OrbitClient`] in a lazily-refreshed +//! cache. The first `Request::DiscoverProviders` after startup populates +//! the catalog (no fetch happens at boot, so a daemon with discovery +//! enabled still starts offline); subsequent calls reuse the cached +//! catalog until it ages past the refresh interval. +//! +//! Discovery is read-only: it never signs, pays, or touches the budget. +//! The handler in [`crate::Server`] gates it behind the `x402.discover` +//! capability and the optional operator allowlist held here. + +use std::time::{Duration, Instant}; + +use covenant_ipc::DiscoveredProvider; +use covenant_x402::{Catalog, OrbitClient, RegistryEntry}; +use tokio::sync::Mutex; +use tracing::{debug, warn}; + +/// Cached orbit-x402 catalog plus the policy for serving it. +/// +/// Held on [`crate::Server`] behind `Option>`. The +/// catalog starts empty and is filled on the first discovery call; a +/// refresh failure leaves the previous catalog in place so a transient +/// registry outage degrades to stale-but-usable rather than empty. +pub struct DiscoveryState { + client: OrbitClient, + refresh_interval: Duration, + /// Optional operator pin: only providers whose `server_title` is in + /// this set are ever returned. `None` means no pin (every provider + /// the registry lists is eligible). + allow: Option>, + cache: Mutex, +} + +#[derive(Default)] +struct Cache { + catalog: Option, + fetched_at: Option, +} + +impl DiscoveryState { + pub fn new( + client: OrbitClient, + refresh_interval: Duration, + allow: Option>, + ) -> Self { + let allow = allow.filter(|a| !a.is_empty()); + Self { + client, + refresh_interval, + allow, + cache: Mutex::new(Cache::default()), + } + } + + /// The cache TTL: how long a fetched catalog is served before the + /// next discovery call triggers a refresh. + pub fn refresh_interval(&self) -> Duration { + self.refresh_interval + } + + /// The operator `server_title` allowlist, or `None` when no pin is + /// configured (every provider eligible). + pub fn allow(&self) -> Option<&[String]> { + self.allow.as_deref() + } + + /// Whether a `server_title` passes the operator allowlist. No pin + /// configured means everything passes. + fn allowed(&self, server_title: &str) -> bool { + match &self.allow { + Some(list) => list.iter().any(|t| t == server_title), + None => true, + } + } + + /// Return the providers matching `filter` (an exact `server_title`), + /// refreshing the cached catalog first when it is empty or stale. + /// + /// On a refresh fetch failure with a populated cache, the stale + /// catalog is served and the error logged; with an empty cache the + /// error propagates so the caller can surface it. + pub async fn providers( + &self, + filter: Option<&str>, + ) -> Result, covenant_x402::X402Error> { + let mut cache = self.cache.lock().await; + let stale = match cache.fetched_at { + Some(at) => at.elapsed() >= self.refresh_interval, + None => true, + }; + if stale { + match self.client.fetch_all().await { + Ok(entries) => { + debug!(count = entries.len(), "orbit-x402 catalog refreshed"); + cache.catalog = Some(Catalog::new(entries)); + cache.fetched_at = Some(Instant::now()); + } + Err(e) if cache.catalog.is_some() => { + warn!(error = %e, "orbit-x402 refresh failed; serving stale catalog"); + } + Err(e) => return Err(e), + } + } + + let catalog = cache + .catalog + .as_ref() + .expect("catalog populated after a successful refresh or preserved stale entry"); + let providers = catalog + .iter() + .filter(|e| filter.is_none_or(|t| e.server_title == t)) + .filter(|e| self.allowed(&e.server_title)) + .map(project) + .collect(); + Ok(providers) + } +} + +/// Project a registry entry to the IPC wire struct, lifting the first +/// pricing option (the registry lists one per `(network, asset)` rail; +/// the first is the canonical quote for display). A free endpoint (no +/// pricing) leaves the price/network/asset fields absent. +fn project(entry: &RegistryEntry) -> DiscoveredProvider { + let pricing = entry.pricing.first(); + DiscoveredProvider { + server_title: entry.server_title.clone(), + endpoint: entry.endpoint.clone(), + slug: entry.slug.clone(), + method: entry.method.clone(), + price_atomic_usdc: pricing.map(|p| p.amount.clone()), + network: pricing.map(|p| p.network.clone()), + asset: pricing.map(|p| p.asset.clone()), + } +} + +/// Test seam: build a `DiscoveryState` whose cache is pre-populated so +/// `providers` serves it without a live fetch. The `fetched_at` is set +/// to now so the interval gate treats it as fresh. +#[cfg(test)] +pub fn seeded( + entries: Vec, + allow: Option>, +) -> std::sync::Arc { + let state = DiscoveryState::new(OrbitClient::new(), Duration::from_secs(3600), allow); + { + let mut cache = state + .cache + .try_lock() + .expect("fresh state has no contended lock"); + cache.catalog = Some(Catalog::new(entries)); + cache.fetched_at = Some(Instant::now()); + } + std::sync::Arc::new(state) +} + +#[cfg(test)] +mod tests { + use super::*; + use covenant_x402::{PaymentRequirements, RegistryEntry}; + + fn entry(server: &str, slug: &str, priced: bool) -> RegistryEntry { + RegistryEntry { + server_url: format!("https://api.{}.test", server.to_lowercase()), + server_title: server.into(), + endpoint: format!("https://api.{}.test/{slug}", server.to_lowercase()), + slug: slug.into(), + method: "POST".into(), + description: "test endpoint".into(), + pricing: if priced { + vec![PaymentRequirements { + network: "solana:mainnet".into(), + asset: "usdc-sol".into(), + amount: "80000".into(), + amount_usdc: 0.08, + pay_to: "pay-to-addr".into(), + scheme: "exact".into(), + extra: None, + }] + } else { + vec![] + }, + } + } + + #[tokio::test] + async fn seeded_catalog_projects_priced_and_free_entries() { + let state = seeded( + vec![ + entry("Xona", "image/creative-director", true), + entry("Xona", "free/ping", false), + ], + None, + ); + let providers = state.providers(None).await.expect("seeded serve"); + assert_eq!(providers.len(), 2); + let priced = &providers[0]; + assert_eq!(priced.price_atomic_usdc.as_deref(), Some("80000")); + assert_eq!(priced.network.as_deref(), Some("solana:mainnet")); + let free = &providers[1]; + assert!(free.price_atomic_usdc.is_none()); + assert!(free.network.is_none()); + assert!(free.asset.is_none()); + } + + #[tokio::test] + async fn server_title_filter_narrows_to_one_provider() { + let state = seeded( + vec![ + entry("Xona", "a", true), + entry("Hyre", "b", true), + entry("Xona", "c", true), + ], + None, + ); + let xona = state.providers(Some("Xona")).await.unwrap(); + assert_eq!(xona.len(), 2); + assert!(xona.iter().all(|p| p.server_title == "Xona")); + let none = state.providers(Some("Nonexistent")).await.unwrap(); + assert!(none.is_empty()); + } + + #[tokio::test] + async fn operator_allowlist_drops_unlisted_servers() { + let state = seeded( + vec![entry("Xona", "a", true), entry("Hyre", "b", true)], + Some(vec!["Xona".into()]), + ); + // No filter, but the allowlist pins to Xona only. + let all = state.providers(None).await.unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].server_title, "Xona"); + // A filter for an off-allowlist server yields nothing even though + // the registry carries it. + let hyre = state.providers(Some("Hyre")).await.unwrap(); + assert!(hyre.is_empty()); + } +} diff --git a/agent-os/crates/covenantd/src/lib.rs b/agent-os/crates/covenantd/src/lib.rs index 9311132e1..9385d8160 100644 --- a/agent-os/crates/covenantd/src/lib.rs +++ b/agent-os/crates/covenantd/src/lib.rs @@ -8,8 +8,10 @@ #![deny(unsafe_code)] +pub mod discovery; pub mod http; pub mod hyre; +pub mod smart_layer; pub mod sse; pub mod stream_dispatch; pub mod stream_tracker; @@ -1065,6 +1067,10 @@ pub struct Server { /// config. None when the operator has not enabled Xona; in that state /// no `xona.*` tool is advertised or callable. xona: Option>, + /// Opt-in x402 provider discovery over the orbit-x402 registry. None + /// when the operator has not enabled discovery; in that state every + /// `Request::DiscoverProviders` returns a "not configured" error. + discovery: Option>, /// Opt-in Synapse Agent Protocol bridge. `None` when no operator /// has wired it in (the default); a built [`SapBridge`] when /// `Server::with_sap_bridge` was called at boot. Handlers that @@ -1117,6 +1123,7 @@ impl Server { x402_dispatch: None, hyre: None, xona: None, + discovery: None, sap_bridge: None, intent_outcomes: Arc::new(std::sync::Mutex::new(OutcomeStore::default())), } @@ -1212,6 +1219,15 @@ impl Server { self } + /// Enable x402 provider discovery over the orbit-x402 registry. + /// Without this, every `Request::DiscoverProviders` returns a "not + /// configured" error. The catalog loads lazily on the first + /// discovery call, so wiring this at boot adds no startup fetch. + pub fn with_discovery(mut self, state: Arc) -> Self { + self.discovery = Some(state); + self + } + pub fn with_budget_checkpoints(mut self, store: Arc) -> Self { self.budget_checkpoints = Some(store); self @@ -2230,6 +2246,45 @@ impl Server { self.revoke_peer(token_prefix, force, match_limit, peer) .await } + Request::DiscoverProviders { server_title } => { + self.discover_providers(server_title, peer).await + } + } + } + + /// Enumerate x402 providers from the cached orbit-x402 registry. + /// + /// Gated by the `x402.discover` capability; the gate also records the + /// `CapabilityCheck` audit row (pass or fail), so a rejection is + /// audited like every other gated verb. Returns `Error` when + /// discovery was not wired in at boot. Otherwise serves the cached + /// catalog (refreshing when stale), filtered by `server_title` and + /// the operator allowlist, projected to `DiscoveredProvider`. + async fn discover_providers(&self, server_title: Option, peer: &AgentId) -> Response { + let check = self + .check_capabilities("x402:discover".into(), vec!["x402.discover".into()], peer) + .await; + if !check.passed { + return Response::Error { + message: "provider discovery requires capability \"x402.discover\". \ + Grant it with `covenant capabilities grant x402.discover`." + .into(), + }; + } + + let Some(discovery) = self.discovery.clone() else { + return Response::Error { + message: "provider discovery is not configured on this daemon. \ + Enable it via COVENANT_X402_DISCOVERY_ENABLED and restart." + .into(), + }; + }; + + match discovery.providers(server_title.as_deref()).await { + Ok(providers) => Response::ProvidersDiscovered { providers }, + Err(e) => Response::Error { + message: format!("provider discovery failed: {e}"), + }, } } @@ -46226,6 +46281,119 @@ required = {caps:?} ); } + fn discovery_entry(server: &str, slug: &str) -> covenant_x402::RegistryEntry { + covenant_x402::RegistryEntry { + server_url: format!("https://api.{}.test", server.to_lowercase()), + server_title: server.into(), + endpoint: format!("https://api.{}.test/{slug}", server.to_lowercase()), + slug: slug.into(), + method: "POST".into(), + description: "test endpoint".into(), + pricing: vec![covenant_x402::PaymentRequirements { + network: "solana:mainnet".into(), + asset: "usdc-sol".into(), + amount: "80000".into(), + amount_usdc: 0.08, + pay_to: "pay-to-addr".into(), + scheme: "exact".into(), + extra: None, + }], + } + } + + #[tokio::test] + async fn discover_providers_rejects_when_capability_missing() { + // No x402.discover grant: the daemon must refuse before touching + // the registry, and the gate records the CapabilityCheck audit + // row like every other gated verb. + let audit = Arc::new(covenant_audit::InMemoryAuditLog::new()); + let s = server_with_audit(audit.clone()).with_discovery(discovery::seeded( + vec![discovery_entry("Xona", "image/creative-director")], + None, + )); + let resp = s + .op_respond(Request::DiscoverProviders { server_title: None }) + .await; + match resp { + Response::Error { message } => assert!( + message.contains("x402.discover"), + "error must name the missing capability so the operator can grant it: {message}" + ), + other => panic!("expected Error, got: {other:?}"), + } + let events = audit.recent(10).await.unwrap(); + assert!( + events + .iter() + .any(|e| matches!(&e.kind, AuditKind::CapabilityCheck { passed, .. } if !passed)), + "a rejected discovery must leave a failed CapabilityCheck audit row" + ); + } + + #[tokio::test] + async fn discover_providers_rejects_when_not_configured() { + // Capability granted, but discovery was never wired in — the + // daemon must say "not configured" rather than return an empty + // list (which would look like "no providers exist"). + let s = server_with_audit(Arc::new(covenant_audit::InMemoryAuditLog::new())); + grant_action(&s, "x402.discover").await; + let resp = s + .op_respond(Request::DiscoverProviders { server_title: None }) + .await; + match resp { + Response::Error { message } => assert!( + message.contains("not configured"), + "error must say 'not configured' so the operator knows to enable discovery: {message}" + ), + other => panic!("expected Error, got: {other:?}"), + } + } + + #[tokio::test] + async fn discover_providers_returns_seeded_catalog_with_capability() { + // Capability granted + a seeded (no live fetch) catalog: the + // daemon projects and returns every provider, and the + // server_title filter narrows to one. + let s = server_with_audit(Arc::new(covenant_audit::InMemoryAuditLog::new())) + .with_discovery(discovery::seeded( + vec![ + discovery_entry("Xona", "image/creative-director"), + discovery_entry("Hyre", "defi/tvl"), + ], + None, + )); + grant_action(&s, "x402.discover").await; + + let resp = s + .op_respond(Request::DiscoverProviders { server_title: None }) + .await; + match resp { + Response::ProvidersDiscovered { providers } => { + assert_eq!(providers.len(), 2); + let xona = providers + .iter() + .find(|p| p.server_title == "Xona") + .expect("xona present"); + assert_eq!(xona.slug, "image/creative-director"); + assert_eq!(xona.price_atomic_usdc.as_deref(), Some("80000")); + } + other => panic!("expected ProvidersDiscovered, got: {other:?}"), + } + + let resp = s + .op_respond(Request::DiscoverProviders { + server_title: Some("Hyre".into()), + }) + .await; + match resp { + Response::ProvidersDiscovered { providers } => { + assert_eq!(providers.len(), 1); + assert_eq!(providers[0].server_title, "Hyre"); + } + other => panic!("expected ProvidersDiscovered, got: {other:?}"), + } + } + #[tokio::test] async fn call_tool_accepts_matching_scope_arguments() { let s = server_with(vec![], ""); diff --git a/agent-os/crates/covenantd/src/main.rs b/agent-os/crates/covenantd/src/main.rs index 734ada31e..e888738b6 100644 --- a/agent-os/crates/covenantd/src/main.rs +++ b/agent-os/crates/covenantd/src/main.rs @@ -415,6 +415,16 @@ async fn main() -> Result<()> { None => server, }; + let server = match discovery_config_from_env() { + Some(state) => { + // No fetch here — the catalog loads on the first + // DiscoverProviders call, keeping startup offline-safe. + info!("x402 provider discovery enabled"); + server.with_discovery(state) + } + None => server, + }; + server .register_agent_budgets() .await @@ -809,6 +819,7 @@ fn hyre_config_from_env() -> Option { /// - `COVENANT_XONA_ALLOW` — comma-separated endpoint slug allowlist (optional) /// - `COVENANT_XONA_SERVER_TITLE_PREFIX` — registry serverTitle prefix /// that identifies Xona's entries (optional) +/// - `COVENANT_XONA_MARKUP_BPS` — resale markup in basis points (optional) fn xona_config_from_env() -> Option { let enabled = std::env::var("COVENANT_XONA_ENABLED") .map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes")) @@ -845,9 +856,70 @@ fn xona_config_from_env() -> Option { .collect(), ); } + if let Ok(bps) = std::env::var("COVENANT_XONA_MARKUP_BPS") { + match bps.trim().parse() { + Ok(n) => cfg.markup_bps = n, + Err(_) => tracing::warn!(value = %bps, "ignoring non-numeric COVENANT_XONA_MARKUP_BPS"), + } + } Some(cfg) } +/// Build the x402 provider-discovery state from env, or None when the +/// operator hasn't opted in. Construction is offline (it only builds an +/// `OrbitClient`); the registry is fetched lazily on the first +/// `DiscoverProviders` call, so this keeps daemon startup network-free. +/// +/// - `COVENANT_X402_DISCOVERY_ENABLED` truthy (`1`, `true`, `yes`) +/// - `COVENANT_X402_DISCOVERY_ORBIT_URL` — registry base URL +/// (default `covenant_x402::orbit::DEFAULT_BASE_URL`) +/// - `COVENANT_X402_DISCOVERY_REFRESH_SECS` — cache TTL in seconds +/// (default 300; a non-numeric value is ignored) +/// - `COVENANT_X402_DISCOVERY_ALLOW` — comma-separated `server_title` +/// allowlist; only listed providers are ever returned (optional) +fn discovery_config_from_env() -> Option> { + let enabled = std::env::var("COVENANT_X402_DISCOVERY_ENABLED") + .map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes")) + .unwrap_or(false); + if !enabled { + return None; + } + let base_url = std::env::var("COVENANT_X402_DISCOVERY_ORBIT_URL") + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| covenant_x402::orbit::DEFAULT_BASE_URL.to_string()); + let refresh_secs = match std::env::var("COVENANT_X402_DISCOVERY_REFRESH_SECS") { + Ok(v) => match v.trim().parse::() { + Ok(n) if n > 0 => n, + _ => { + tracing::warn!(value = %v, "ignoring invalid COVENANT_X402_DISCOVERY_REFRESH_SECS; using default"); + 300 + } + }, + Err(_) => 300, + }; + let allow = std::env::var("COVENANT_X402_DISCOVERY_ALLOW") + .ok() + .map(|list| { + list.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect::>() + }); + let http = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + let client = + covenant_x402::OrbitClient::with(http, base_url, covenant_x402::orbit::DEFAULT_PAGE_SIZE); + Some(Arc::new(covenantd::discovery::DiscoveryState::new( + client, + std::time::Duration::from_secs(refresh_secs), + allow, + ))) +} + /// Mask secret query params (api keys, tokens) in a URL before logging it, /// so a keyed RPC endpoint in `sap.env` never lands in stdout/journald. fn redact_url(url: &str) -> String { diff --git a/agent-os/crates/covenantd/src/smart_layer.rs b/agent-os/crates/covenantd/src/smart_layer.rs new file mode 100644 index 000000000..95eeb519d --- /dev/null +++ b/agent-os/crates/covenantd/src/smart_layer.rs @@ -0,0 +1,379 @@ +//! Optional inbound payload validator for the x402 resale surface. +//! +//! When an operator resells daemon-fronted tools, a paying caller's +//! request can be routed through this layer for an AI payload-integrity +//! / quality check *before* the gateway settles payment. It is shaped +//! against Xona's `x402-smart-layer` Express middleware, configured by +//! `{ realm, programId, connection, price, qualityGuarantee }`. +//! +//! ## Scope +//! +//! Inbound only. The outbound payment path ([`crate::x402`]) does not +//! touch this layer — nothing the daemon pays for is gated here, only +//! requests the daemon would serve and settle for a remote payer. +//! +//! ## Defaults and failure mode +//! +//! The layer is off unless an operator opts in via +//! `COVENANT_X402_SMART_LAYER_ENABLED`. Off resolves to [`NoOpValidator`] +//! (or no validator at all), so a daemon with no opt-in behaves exactly +//! as it would without this module. +//! +//! When enabled the layer is **fail-closed**: any condition that +//! prevents a verdict — a missing Gemini key, a transport error, a +//! malformed model response, or an explicit model rejection — returns +//! [`Err`] and must block settlement. An enabled-but-unconfigured +//! validator rejecting every call is the correct, honest behavior; it +//! never silently approves. + +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::warn; + +/// A paying caller's inbound request, presented to the validator before +/// the gateway settles. Carries enough of the request for the model to +/// judge payload integrity against the operator's quality guarantee. +#[derive(Debug, Clone)] +pub struct InboundCall<'a> { + /// Settlement realm the resold resource belongs to (Xona's `realm`). + pub realm: &'a str, + /// Resource / tool slug the payer is calling. + pub resource: &'a str, + /// Decoded request payload, when it parsed as JSON. + pub payload: Option<&'a Value>, + /// Raw request body, always present even when `payload` is `None`. + pub payload_bytes: &'a [u8], + /// Advertised price for this call, atomic USDC. + pub price_micro_usdc: u128, +} + +/// Why the smart layer refused to clear an inbound call. Every variant +/// blocks settlement; the daemon should map this onto a payment refusal, +/// not a soft skip. +#[derive(Debug, thiserror::Error)] +pub enum SmartLayerReject { + /// The model judged the payload below the operator's guarantee. + #[error("payload rejected by smart layer: {0}")] + Rejected(String), + /// The layer is enabled but missing the Gemini key, so no verdict is + /// possible. Fail-closed: treated as a rejection. + #[error("smart layer enabled but unconfigured: {0}")] + Unconfigured(&'static str), + /// The verdict call failed to reach the model or complete. + #[error("smart layer transport error: {0}")] + Transport(String), + /// The model responded but the verdict could not be parsed. + #[error("smart layer response malformed: {0}")] + Malformed(String), +} + +/// Validates inbound resale requests before settlement. The integration +/// seam: the future inbound-resale serve path holds an +/// `Arc` and calls [`validate`](Self::validate) +/// before serving or settling. On `Err` it must refuse the call. +#[async_trait] +pub trait SmartLayerValidator: Send + Sync { + async fn validate(&self, req: &InboundCall<'_>) -> Result<(), SmartLayerReject>; +} + +/// The off / absent configuration. Approves every call, imposing no +/// behavior change on a daemon that has not opted in. +#[derive(Debug, Default, Clone)] +pub struct NoOpValidator; + +#[async_trait] +impl SmartLayerValidator for NoOpValidator { + async fn validate(&self, _req: &InboundCall<'_>) -> Result<(), SmartLayerReject> { + Ok(()) + } +} + +/// Gemini-backed inbound validator. +/// +/// Holds the middleware config (`{ realm, programId, connection, price, +/// qualityGuarantee }`) plus the Gemini credentials. [`validate`] +/// POSTs the payload and the quality guarantee to Gemini and maps the +/// verdict onto `Ok`/`Err`. +pub struct GeminiSmartLayer { + pub realm: String, + pub program_id: String, + pub connection: String, + pub price_micro_usdc: u128, + pub quality_guarantee: String, + gemini_api_key: String, + gemini_endpoint: String, + client: reqwest::Client, +} + +/// PROVISIONAL CONTRACT. Xona has not shipped the final +/// `x402-smart-layer` middleware signature, so the exact Gemini +/// request/response shape is not yet fixed. This module commits to the +/// documented config (`realm`, `programId`, `connection`, `price`, +/// `qualityGuarantee`) and a minimal verdict contract: the request +/// carries the guarantee and payload; the response is expected to expose +/// a boolean `approved` (with an optional `reason`). When Xona publishes +/// the final contract, only [`SmartLayerVerdict`] and the request body +/// in [`GeminiSmartLayer::validate`] need to change — the trait, the +/// config, and the integration seam are stable. +#[derive(Debug, Deserialize)] +struct SmartLayerVerdict { + approved: bool, + #[serde(default)] + reason: Option, +} + +#[derive(Serialize)] +struct VerdictRequest<'a> { + realm: &'a str, + program_id: &'a str, + connection: &'a str, + resource: &'a str, + price_micro_usdc: u128, + quality_guarantee: &'a str, + payload: Value, +} + +impl GeminiSmartLayer { + pub fn new( + realm: impl Into, + program_id: impl Into, + connection: impl Into, + price_micro_usdc: u128, + quality_guarantee: impl Into, + gemini_api_key: impl Into, + gemini_endpoint: impl Into, + ) -> Self { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("reqwest client"); + Self { + realm: realm.into(), + program_id: program_id.into(), + connection: connection.into(), + price_micro_usdc, + quality_guarantee: quality_guarantee.into(), + gemini_api_key: gemini_api_key.into(), + gemini_endpoint: gemini_endpoint.into(), + client, + } + } + + fn request_body<'a>(&'a self, req: &InboundCall<'a>) -> VerdictRequest<'a> { + let payload = req.payload.cloned().unwrap_or_else(|| { + Value::String(String::from_utf8_lossy(req.payload_bytes).into_owned()) + }); + VerdictRequest { + realm: &self.realm, + program_id: &self.program_id, + connection: &self.connection, + resource: req.resource, + price_micro_usdc: req.price_micro_usdc, + quality_guarantee: &self.quality_guarantee, + payload, + } + } +} + +#[async_trait] +impl SmartLayerValidator for GeminiSmartLayer { + async fn validate(&self, req: &InboundCall<'_>) -> Result<(), SmartLayerReject> { + if self.gemini_api_key.is_empty() { + return Err(SmartLayerReject::Unconfigured("gemini api key missing")); + } + if self.gemini_endpoint.is_empty() { + return Err(SmartLayerReject::Unconfigured("gemini endpoint missing")); + } + + let resp = self + .client + .post(&self.gemini_endpoint) + .header("x-goog-api-key", &self.gemini_api_key) + .json(&self.request_body(req)) + .send() + .await + .map_err(|e| SmartLayerReject::Transport(e.to_string()))?; + + let status = resp.status(); + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + return Err(SmartLayerReject::Transport(format!( + "gemini returned {}: {}", + status.as_u16(), + body.trim() + ))); + } + + let verdict: SmartLayerVerdict = resp + .json() + .await + .map_err(|e| SmartLayerReject::Malformed(e.to_string()))?; + + if verdict.approved { + Ok(()) + } else { + Err(SmartLayerReject::Rejected( + verdict.reason.unwrap_or_else(|| "no reason given".into()), + )) + } + } +} + +fn parse_bool(value: Option<&str>) -> bool { + match value.map(str::trim).map(str::to_ascii_lowercase) { + Some(s) => matches!(s.as_str(), "1" | "true" | "yes"), + None => false, + } +} + +/// Resolve a validator from the process environment. +/// +/// Returns `None` when `COVENANT_X402_SMART_LAYER_ENABLED` is unset or +/// falsy — the caller then skips the check (or substitutes +/// [`NoOpValidator`]), preserving today's behavior exactly. When +/// enabled, returns a [`GeminiSmartLayer`]; a missing Gemini key is +/// *not* an error here — the validator still constructs and rejects +/// fail-closed at call time, so an operator who flips the switch without +/// finishing configuration gets refusals rather than silent approvals. +pub fn from_env() -> Option> { + from_env_map(std::env::vars()) +} + +fn from_env_map(env: I) -> Option> +where + I: IntoIterator, + K: AsRef, + V: AsRef, +{ + use std::collections::HashMap; + let map: HashMap = env + .into_iter() + .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string())) + .collect(); + let get = |key: &str| map.get(key).map(String::as_str); + + if !parse_bool(get("COVENANT_X402_SMART_LAYER_ENABLED")) { + return None; + } + + let realm = get("COVENANT_X402_SMART_LAYER_REALM").unwrap_or_default(); + let program_id = get("COVENANT_X402_SMART_LAYER_PROGRAM_ID").unwrap_or_default(); + let connection = get("COVENANT_X402_SMART_LAYER_CONNECTION").unwrap_or_default(); + let price = get("COVENANT_X402_SMART_LAYER_PRICE") + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + let quality_guarantee = get("COVENANT_X402_SMART_LAYER_QUALITY_GUARANTEE").unwrap_or_default(); + let api_key = get("COVENANT_X402_SMART_LAYER_GEMINI_API_KEY").unwrap_or_default(); + let endpoint = get("COVENANT_X402_SMART_LAYER_GEMINI_ENDPOINT").unwrap_or_default(); + + if api_key.is_empty() || endpoint.is_empty() { + warn!("x402 smart layer enabled without a complete Gemini config; it will reject inbound calls fail-closed"); + } + + Some(Arc::new(GeminiSmartLayer::new( + realm, + program_id, + connection, + price, + quality_guarantee, + api_key, + endpoint, + ))) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn call<'a>(payload: &'a Value) -> InboundCall<'a> { + InboundCall { + realm: "covenant", + resource: "image/creative-director", + payload: Some(payload), + payload_bytes: b"{}", + price_micro_usdc: 120_000, + } + } + + struct RejectStub; + + #[async_trait] + impl SmartLayerValidator for RejectStub { + async fn validate(&self, _req: &InboundCall<'_>) -> Result<(), SmartLayerReject> { + Err(SmartLayerReject::Rejected("stub".into())) + } + } + + #[tokio::test] + async fn noop_approves() { + let payload = json!({"prompt": "hi"}); + assert!(NoOpValidator.validate(&call(&payload)).await.is_ok()); + } + + #[tokio::test] + async fn stub_rejects() { + let payload = json!({"prompt": "hi"}); + let err = RejectStub + .validate(&call(&payload)) + .await + .expect_err("reject"); + assert!(matches!(err, SmartLayerReject::Rejected(_))); + } + + #[test] + fn from_env_none_when_disabled() { + assert!(from_env_map::<_, &str, &str>([]).is_none()); + assert!(from_env_map([("COVENANT_X402_SMART_LAYER_ENABLED", "false")]).is_none()); + assert!(from_env_map([("COVENANT_X402_SMART_LAYER_ENABLED", "0")]).is_none()); + } + + #[test] + fn from_env_some_when_enabled() { + let v = from_env_map([ + ("COVENANT_X402_SMART_LAYER_ENABLED", "true"), + ("COVENANT_X402_SMART_LAYER_REALM", "covenant"), + ("COVENANT_X402_SMART_LAYER_GEMINI_API_KEY", "k"), + ( + "COVENANT_X402_SMART_LAYER_GEMINI_ENDPOINT", + "https://example.test/v1", + ), + ]); + assert!(v.is_some()); + } + + /// Enabled but no key: must reject without ever touching the network. + #[tokio::test] + async fn enabled_missing_key_rejects_offline() { + let layer = GeminiSmartLayer::new( + "covenant", + "prog", + "https://api.mainnet-beta.solana.com", + 120_000, + "must be coherent", + "", + "https://unreachable.invalid/should-never-be-called", + ); + let payload = json!({"prompt": "hi"}); + let err = layer + .validate(&call(&payload)) + .await + .expect_err("fail-closed"); + assert!(matches!(err, SmartLayerReject::Unconfigured(_))); + } + + #[tokio::test] + async fn enabled_missing_endpoint_rejects_offline() { + let layer = GeminiSmartLayer::new("covenant", "prog", "conn", 1, "guarantee", "key", ""); + let payload = json!({"prompt": "hi"}); + let err = layer + .validate(&call(&payload)) + .await + .expect_err("fail-closed"); + assert!(matches!(err, SmartLayerReject::Unconfigured(_))); + } +} diff --git a/docs/xona-integration.md b/docs/xona-integration.md index 9b2a48ea0..6c4102978 100644 --- a/docs/xona-integration.md +++ b/docs/xona-integration.md @@ -159,6 +159,25 @@ The profile is opt-in and requires the x402 funding-key sidecar With the profile disabled, no `xona.*` tool is advertised or callable and no Xona call ever leaves the host. +## Resale publishing (SAP bridge) + +`covenant-xona::publish` derives per-tool resale descriptors so an operator who +runs the SAP bridge can republish a marked-up Xona tier (markup set via +`COVENANT_XONA_MARKUP_BPS`, the flow back through Covenant settlement). The +bridge owns the on-chain registration and its own capability; with the bridge +off, `publishable` returns nothing and the profile is unaffected. + +## Discovery (provider-agnostic) + +Xona is also reachable through generic x402 provider discovery: with +`COVENANT_X402_DISCOVERY_ENABLED`, a `Request::DiscoverProviders { server_title }` +(capability-gated by `x402.discover`) serves the cached orbit-x402 catalog — +filtered to Xona via `server_title: "Xona Agent …"` — as `DiscoveredProvider` +rows an agent can read before deciding to pay. Discovery is read-only: it never +signs, pays, or touches the budget. An inbound resale quality gate +(`covenantd::smart_layer`, default-off, fail-closed) is scaffolded for operators +who resell Xona-fronted tools. + ## Settlement, attestation, and the SAP bridge Nothing Xona-specific exists on the settlement or attestation path. A Xona