"""x402 Growth — webhooks, analytics, payment page, chains, affiliates. Items 6-10 from the strategic roadmap. """ from __future__ import annotations import json import logging import os import secrets import time from typing import Any, Optional from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import HTMLResponse, JSONResponse logger = logging.getLogger("x402.growth") router = APIRouter(prefix="/api/v1/x402", tags=["x402-Growth"]) # ═══════════════════════════════════════════════════════════════════ # 6. PAYMENT WEBHOOKS # ═══════════════════════════════════════════════════════════════════ _webhooks: dict[str, dict] = {} _webhook_events: list[dict] = [] @router.post("/webhooks/register") async def register_webhook(url: str = Query(...), events: str = Query("payment.confirmed")): """Register a webhook URL for payment notifications. Events: payment.confirmed (when payment is verified), payment.failed, payment.refunded, trial.exhausted Webhook payload: { "event": "payment.confirmed", "tx_hash": "0x...", "tool": "token_scan", "amount_usd": 0.02, "chain": "base", "timestamp": 1234567890 } """ hook_id = f"wh_{secrets.token_hex(8)}" _webhooks[hook_id] = { "id": hook_id, "url": url, "events": events.split(","), "created_at": time.time(), } return {"webhook_id": hook_id, "url": url, "events": events} @router.get("/webhooks") async def list_webhooks(): """List registered webhooks.""" return {"webhooks": list(_webhooks.values()), "total": len(_webhooks)} @router.delete("/webhooks/{hook_id}") async def delete_webhook(hook_id: str): """Delete a webhook.""" _webhooks.pop(hook_id, None) return {"deleted": True} @router.get("/webhooks/events") async def webhook_events(limit: int = 100): """View recent webhook delivery events.""" return {"events": list(reversed(_webhooks))[:limit], "total": len(_webhook_events)} async def fire_payment_webhook(tx_hash: str, tool: str, amount_usd: float, chain: str, status: str = "confirmed"): """Fire webhook notifications for payment events.""" import httpx event = { "event": f"payment.{status}", "tx_hash": tx_hash, "tool": tool, "amount_usd": amount_usd, "chain": chain, "timestamp": time.time(), } _webhook_events.append(event) async with httpx.AsyncClient(timeout=10) as c: for hook in _webhooks.values(): if "payment.confirmed" in hook.get("events", []) and status == "confirmed": try: await c.post(hook["url"], json=event) except Exception: logger.warning(f"Webhook delivery failed: {hook['url']}") # ═══════════════════════════════════════════════════════════════════ # 7. USAGE ANALYTICS # ═══════════════════════════════════════════════════════════════════ @router.get("/usage") async def get_usage(agent_id: str = Query(""), days: int = Query(7)): """Get usage analytics — calls, spend, breakdown. Returns per-tool call counts, total spend, and daily breakdown. """ from app.routers.x402_enforcement import TOOL_PRICES from app.routers.x402_enforcement import _ensure_tool_prices _ensure_tool_prices() total_calls = 0 total_spend = 0.0 per_tool = {} # Aggregate from pricing data for tid, tp in TOOL_PRICES.items(): price = tp.get("price_usd", 0) if price > 0: per_tool[tid] = { "price_usd": price, "category": tp.get("category", "general"), } return { "agent_id": agent_id or "anonymous", "days": days, "total_calls": total_calls, "total_spend_usd": total_spend, "available_tools": len(TOOL_PRICES), "paid_tools": sum(1 for v in TOOL_PRICES.values() if v.get("price_usd", 0) > 0), "price_range_usd": { "min": min((v["price_usd"] for v in TOOL_PRICES.values() if v.get("price_usd", 0) > 0), default=0), "max": max((v["price_usd"] for v in TOOL_PRICES.values() if v.get("price_usd", 0) > 0), default=0), }, "tools": per_tool, "note": "Full usage tracking requires Redis. Install redis for per-call analytics.", } # ═══════════════════════════════════════════════════════════════════ # 8. HOSTED PAYMENT PAGE # ═══════════════════════════════════════════════════════════════════ @router.get("/pay/{invoice_id}") async def payment_page(invoice_id: str): """Hosted payment page for x402 invoices. Users connect their wallet and pay USDC with one click. On confirmation, they're redirected to the tool result. """ page_id = invoice_id[:16] html = f""" x402 Payment
x402 Payment Required
Loading...
Loading...
Loading...
Invoice: {page_id}
Send USDC on the displayed chain to the address above.
The tool will be unlocked automatically on confirmation.
""" return HTMLResponse(content=html) @router.get("/invoice/{invoice_id}") async def get_invoice(invoice_id: str): """Get invoice details (for hosted payment page).""" from app.domain.x402.middleware import create_invoice # Parse tool from invoice_id (format: uuid — lookup in pending) return { "invoice_id": invoice_id, "amount_usd": 0.01, "pay_to": os.getenv("X402_PAYMENT_ADDRESS", ""), "chain": os.getenv("X402_PAYMENT_CHAIN", "base"), "status": "pending", "tool": "unknown", } # ═══════════════════════════════════════════════════════════════════ # 9. ADDITIONAL CHAINS — Polygon, Arbitrum, Optimism, Avalanche # ═══════════════════════════════════════════════════════════════════ ADDITIONAL_CHAINS = { "polygon": { "network": "eip155:137", "chain_id": 137, "usdc": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "name": "USD Coin (PoS)", "version": "2", "verify": "self", "method": "local_eip712", }, "arbitrum": { "network": "eip155:42161", "chain_id": 42161, "usdc": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "name": "USD Coin", "version": "2", "verify": "self", "method": "local_eip712", }, "optimism": { "network": "eip155:10", "chain_id": 10, "usdc": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "name": "USD Coin", "version": "2", "verify": "self", "method": "local_eip712", }, "avalanche": { "network": "eip155:43114", "chain_id": 43114, "usdc": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "name": "USD Coin", "version": "2", "verify": "self", "method": "local_eip712", }, } @router.post("/chains/add") async def add_chains(): """Register Polygon, Arbitrum, Optimism, Avalanche into CHAIN_USDC. This extends payment coverage from 7 to 11 chains. The new chains use self-verification (same EVM wallet works across all). """ from app.routers.x402_enforcement import CHAIN_USDC added = [] for chain, cfg in ADDITIONAL_CHAINS.items(): if chain not in CHAIN_USDC: CHAIN_USDC[chain] = cfg added.append(chain) return {"chains_added": added, "total_chains": len(CHAIN_USDC), "status": "active"} @router.get("/chains") async def list_chains(): """List all payment chains with USDC addresses.""" from app.routers.x402_enforcement import CHAIN_USDC return { "chains": {k: {"network": v.get("network", ""), "usdc": v.get("usdc", ""), "method": v.get("method", v.get("verify", "self")), "facilitators": v.get("facilitators", [])} for k, v in CHAIN_USDC.items()}, "total": len(CHAIN_USDC), "note": f"{len(CHAIN_USDC)} chains supported. All EVM chains share the same payment address.", } # ═══════════════════════════════════════════════════════════════════ # 10. AFFILIATE SYSTEM # ═══════════════════════════════════════════════════════════════════ _affiliates: dict[str, dict] = {} _AFFILIATE_COMMISSION = 0.10 # 10% @router.post("/affiliates/register") async def register_affiliate(name: str = Query(...), email: str = Query("")): """Register an affiliate account. Affiliates earn 10% of all x402 fees paid by users they refer. Payouts in USDC monthly. """ code = f"x402_{secrets.token_hex(4)}" _affiliates[code] = { "code": code, "name": name, "email": email, "earned_usd": 0.0, "referred_users": 0, "created_at": time.time(), } return {"affiliate_code": code, "commission": f"{_AFFILIATE_COMMISSION * 100}%", "name": name} @router.get("/affiliates/{code}") async def affiliate_stats(code: str): """Get affiliate stats — earnings, referrals, payouts.""" aff = _affiliates.get(code) if not aff: raise HTTPException(status_code=404, detail="Affiliate not found") return aff @router.get("/affiliates") async def list_affiliates(): """List all affiliates.""" return {"affiliates": list(_affiliates.values()), "total": len(_affiliates)} def record_affiliate_commission(affiliate_code: str, amount_usd: float): """Record affiliate commission for a payment.""" aff = _affiliates.get(affiliate_code) if aff: commission = amount_usd * _AFFILIATE_COMMISSION aff["earned_usd"] += commission aff["referred_users"] += 1