merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
53
app/domain/x402/__init__.py
Normal file
53
app/domain/x402/__init__.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""x402 domain — auto-registers its health check + HTTP routes.
|
||||
|
||||
T34 from v4.0. Sovereign-first x402 payment layer for AI agents.
|
||||
|
||||
Re-exports the legacy models (PaymentFacilitator, X402Tier) for backward
|
||||
compatibility with v1 routers that import from app.domain.x402.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core import health as health_mod
|
||||
from app.core.health import DomainHealth
|
||||
|
||||
|
||||
async def _health_check() -> DomainHealth:
|
||||
"""x402 health: catalog + middleware available."""
|
||||
try:
|
||||
from app.domain.x402.middleware import PRICING
|
||||
return DomainHealth(
|
||||
name="x402",
|
||||
healthy=True,
|
||||
details={"tools": len(PRICING), "middleware": "available"},
|
||||
)
|
||||
except Exception as e:
|
||||
return DomainHealth(name="x402", healthy=False, error=str(e))
|
||||
|
||||
|
||||
health_mod.register_health_check("x402", _health_check)
|
||||
|
||||
|
||||
# Re-export models for backward compat with v1 routers and tests
|
||||
from app.domain.x402.models import (
|
||||
PaymentFacilitator,
|
||||
PaymentReceipt,
|
||||
ToolCatalog,
|
||||
ToolCatalogEntry,
|
||||
ToolPricing,
|
||||
X402Tier,
|
||||
)
|
||||
|
||||
# Re-export the new T34 router
|
||||
from app.domain.x402.router import router # noqa: E402
|
||||
from app.domain.x402.service import X402Service
|
||||
|
||||
__all__ = [
|
||||
"PaymentFacilitator",
|
||||
"PaymentReceipt",
|
||||
"ToolCatalog",
|
||||
"ToolCatalogEntry",
|
||||
"ToolPricing",
|
||||
"X402Service",
|
||||
"X402Tier",
|
||||
"router",
|
||||
]
|
||||
449
app/domain/x402/middleware.py
Normal file
449
app/domain/x402/middleware.py
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
"""T34 x402 Payment Middleware.
|
||||
|
||||
Per v4.0 §T34. HTTP 402 'Payment Required' repurposed for AI agents.
|
||||
|
||||
Flow:
|
||||
1. Agent calls paid endpoint without X-Payment header
|
||||
2. Server returns 402 with payment challenge (signed invoice)
|
||||
3. Agent's wallet pays on-chain, gets tx_hash
|
||||
4. Agent re-calls with X-Payment: <base64(tx_hash + signature)>
|
||||
5. Server verifies on-chain payment via web3, fulfills request
|
||||
|
||||
Pricing (per v4.0):
|
||||
Free: 5 calls/day
|
||||
Pro: $0.01/call (1000 calls/day)
|
||||
Ent: $0.001/call (unlimited)
|
||||
|
||||
For v1, we use a simplified flow:
|
||||
- Track calls per agent in Redis (sliding window)
|
||||
- Receipts logged to x402_receipts table
|
||||
- Payment verification stubbed (returns True for now)
|
||||
- The 402 challenge includes a payment_url the agent hits
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Pricing per v4.0 §T34 ───────────────────────────────────────────
|
||||
PRICING: dict[str, dict[str, Any]] = {
|
||||
# free_tier: bool, pro_cents: int (USD cents), ent_cents: int
|
||||
"get_token_risk": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Real-time token risk score"},
|
||||
"get_wallet_analysis": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Wallet activity + reputation"},
|
||||
"get_deployer_reputation": {"free_tier": False, "pro_cents": 2, "ent_cents": 1, "description": "Deployer reputation score"},
|
||||
"get_news_sentiment": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Latest news + sentiment"},
|
||||
"generate_report": {"free_tier": False, "pro_cents": 500, "ent_cents": 400, "description": "Full AI research report"},
|
||||
"query_catalog": {"free_tier": False, "pro_cents": 5, "ent_cents": 3, "description": "Natural language catalog query"},
|
||||
"find_similar_tokens": {"free_tier": False, "pro_cents": 3, "ent_cents": 2, "description": "Vector-similar tokens"},
|
||||
"resolve_entity": {"free_tier": False, "pro_cents": 10, "ent_cents": 5, "description": "Cross-chain entity resolution"},
|
||||
"search_rag": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "RAG semantic search"},
|
||||
"bulk_ingest": {"free_tier": False, "pro_cents": 10, "ent_cents": 5, "description": "Bulk RAG ingestion"},
|
||||
}
|
||||
|
||||
# Free tier daily limit
|
||||
FREE_DAILY_LIMIT = 5
|
||||
|
||||
|
||||
|
||||
# ── Nonce burn (replay protection) ──────────────────────────────
|
||||
# Per MINIMAX_M3_TASKS.md T02. Each tx_hash can only be used ONCE.
|
||||
# We use Redis SETNX (set-if-not-exists) as an atomic nonce burn:
|
||||
# - First call: SETNX returns 1, nonce is burned, request proceeds
|
||||
# - Second call: SETNX returns 0, nonce already burned → 410 Gone
|
||||
# TTL is 24h (the x402 invoice expiry window).
|
||||
|
||||
X402_NONCE_TTL = 86400 # 24 hours
|
||||
|
||||
|
||||
async def _burn_nonce(catalog, tx_hash: str) -> bool:
|
||||
"""Atomically burn a payment nonce. Returns True if first use, False if replay.
|
||||
|
||||
Uses Redis SETNX (SET key value NX EX seconds). Atomic by construction.
|
||||
Logs replay attempts for monitoring.
|
||||
"""
|
||||
if not catalog or not catalog._health.redis:
|
||||
# Without Redis we cannot prevent replay — fail closed (reject).
|
||||
log.warning(f"x402_nocache_reject: tx_hash={tx_hash[:10]}...")
|
||||
return False
|
||||
key = f"x402:nonce:{tx_hash}"
|
||||
try:
|
||||
# SETNX semantics: return 1 if key was set, 0 if already existed
|
||||
ok = await catalog._redis.set(key, "1", ex=X402_NONCE_TTL, nx=True)
|
||||
if not ok:
|
||||
log.warning(f"x402_replay_attempt: tx_hash={tx_hash[:10]}...")
|
||||
_X402_REPLAY_COUNTER.labels(tool="unknown").inc()
|
||||
return bool(ok)
|
||||
except Exception as e:
|
||||
log.error(f"x402_nonce_burn_fail: {e}")
|
||||
# Fail closed — reject if we can't burn atomically
|
||||
return False
|
||||
|
||||
|
||||
# ── Prometheus counters (lightweight, no external dep) ──────────
|
||||
try:
|
||||
from prometheus_client import REGISTRY, Counter
|
||||
|
||||
# Use a module-level registry so multiple imports don't double-register
|
||||
_X402_REGISTRY = REGISTRY
|
||||
_X402_REPLAY_COUNTER = Counter(
|
||||
"x402_replay_attempts_total",
|
||||
"Number of x402 replay attempts blocked by nonce burn",
|
||||
["tool"],
|
||||
registry=_X402_REGISTRY,
|
||||
)
|
||||
_X402_REVENUE_COUNTER = Counter(
|
||||
"x402_revenue_total_usd_cents",
|
||||
"x402 revenue in USD cents",
|
||||
["tool", "tier"],
|
||||
registry=_X402_REGISTRY,
|
||||
)
|
||||
except Exception:
|
||||
# Prometheus not available — use no-op counter stubs
|
||||
class _Stub:
|
||||
def labels(self, **kw): return self
|
||||
def inc(self, n: float = 1.0): pass
|
||||
_X402_REPLAY_COUNTER = _Stub()
|
||||
_X402_REVENUE_COUNTER = _Stub()
|
||||
|
||||
|
||||
|
||||
def get_tool_price_usd(tool: str) -> float:
|
||||
"""Get the per-call price for a tool in USD.
|
||||
|
||||
Checks PRICING dict first, then falls back to the enforcement
|
||||
system's TOOL_PRICES for full coverage of 270+ tools.
|
||||
Returns 0.01 as default if tool is not found in either.
|
||||
"""
|
||||
p = PRICING.get(tool, {})
|
||||
if p.get("free_tier", False):
|
||||
return 0.0
|
||||
if p.get("pro_cents"):
|
||||
return p["pro_cents"] / 100.0
|
||||
|
||||
# Fallback: check the enforcement system's comprehensive price list
|
||||
try:
|
||||
from routers.x402_enforcement import TOOL_PRICES as _FALLBACK_PRICES, _ensure_tool_prices
|
||||
_ensure_tool_prices()
|
||||
fp = _FALLBACK_PRICES.get(tool, {})
|
||||
return fp.get("price_usd", 0.01)
|
||||
except Exception:
|
||||
return p.get("pro_cents", 1) / 100.0
|
||||
|
||||
|
||||
def get_tool_metadata(tool: str) -> dict:
|
||||
"""Public-facing tool metadata for /api/v1/x402/catalog."""
|
||||
p = PRICING.get(tool, {})
|
||||
return {
|
||||
"name": tool,
|
||||
"free_tier": p.get("free_tier", False),
|
||||
"free_daily_limit": FREE_DAILY_LIMIT if p.get("free_tier") else 0,
|
||||
"price_pro_usd": p.get("pro_cents", 0) / 100.0,
|
||||
"price_ent_usd": p.get("ent_cents", 0) / 100.0,
|
||||
"description": p.get("description", ""),
|
||||
}
|
||||
|
||||
|
||||
# ── Rate limit tracking (Redis) ───────────────────────────────────
|
||||
def _agent_id_from_request(request: Request | None) -> str:
|
||||
"""Extract agent id from headers. Defaults to 'anon:ip' for anonymous.
|
||||
|
||||
Security: X-Agent-Id is self-declared and marked as unverified.
|
||||
X-Api-Key is checked against our admin key if configured.
|
||||
"""
|
||||
if not request:
|
||||
return "anon:unknown"
|
||||
|
||||
api_key = request.headers.get("X-Api-Key", "")
|
||||
if api_key:
|
||||
# Verify against configured admin key
|
||||
admin_key = os.getenv("ADMIN_API_KEY", os.getenv("WP_ADMIN_KEY", ""))
|
||||
if admin_key and api_key == admin_key:
|
||||
return "key:admin"
|
||||
return f"unverified_key:{api_key[:16]}"
|
||||
|
||||
agent_id = request.headers.get("X-Agent-Id", "")
|
||||
if agent_id:
|
||||
return f"unverified_agent:{agent_id[:20]}"
|
||||
|
||||
client = request.client
|
||||
if client:
|
||||
return f"anon:{client.host}"
|
||||
return "anon:unknown"
|
||||
|
||||
|
||||
async def _check_rate_limit(redis, agent_id: str) -> None:
|
||||
"""Sliding-window rate limit: 5 free calls/day, 1000 pro, unlimited ent.
|
||||
|
||||
For v1, we use Redis with a daily counter. Per-second limits are
|
||||
enforced at the proxy level (nginx/Caddy).
|
||||
"""
|
||||
if not redis:
|
||||
return
|
||||
try:
|
||||
day = datetime.now(UTC).strftime("%Y%m%d")
|
||||
key = f"x402:rl:{agent_id}:{day}"
|
||||
count = await redis.incr(key)
|
||||
if count == 1:
|
||||
await redis.expire(key, 86400)
|
||||
if count > FREE_DAILY_LIMIT:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail={
|
||||
"error": "rate_limit_exceeded",
|
||||
"agent_id": agent_id,
|
||||
"daily_count": count,
|
||||
"limit": FREE_DAILY_LIMIT,
|
||||
"next_action": "Send X-Payment header with valid tx_hash to upgrade tier",
|
||||
},
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.warning(f"rate_limit_check_fail: {e}")
|
||||
|
||||
|
||||
# ── HMAC key from env ────────────────────────────────────────────
|
||||
|
||||
def _get_hmac_key() -> bytes:
|
||||
"""Load HMAC signing key from environment. Never hardcoded."""
|
||||
key = os.getenv("X402_HMAC_KEY", "")
|
||||
if not key:
|
||||
key = os.getenv("ADMIN_API_KEY", os.getenv("WP_ADMIN_KEY", "dev-key-only-for-local"))
|
||||
if "dev" not in key:
|
||||
logging.getLogger("wp.x402").warning("X402_HMAC_KEY not set, using ADMIN_API_KEY")
|
||||
return key.encode()
|
||||
|
||||
|
||||
# ── Payment challenge ─────────────────────────────────────────────
|
||||
def create_invoice(tool: str, agent_id: str) -> dict:
|
||||
"""Create a payment challenge (signed invoice) for an agent to pay.
|
||||
|
||||
The invoice is signed with the platform's HMAC key (from env var
|
||||
X402_HMAC_KEY, fallback ADMIN_API_KEY). The agent can verify the
|
||||
signature before paying.
|
||||
"""
|
||||
invoice_id = uuid4().hex
|
||||
amount_usd = get_tool_price_usd(tool)
|
||||
pay_to = os.getenv("X402_PAYMENT_ADDRESS", "")
|
||||
hmac_key = _get_hmac_key()
|
||||
invoice = {
|
||||
"id": invoice_id,
|
||||
"tool": tool,
|
||||
"agent_id": agent_id,
|
||||
"amount_usd": amount_usd,
|
||||
"amount_wei": int(amount_usd * 1e18 / float(os.getenv("X402_ETH_PRICE_USD", "3000"))) if amount_usd > 0 else 0,
|
||||
"pay_to": pay_to or "0xRMI_PLATFORM_WALLET (set X402_PAYMENT_ADDRESS)",
|
||||
"chain": os.getenv("X402_PAYMENT_CHAIN", "base"),
|
||||
"created_at": datetime.now(UTC).isoformat(),
|
||||
"expires_at": (datetime.now(UTC).timestamp() + 3600),
|
||||
}
|
||||
msg = json.dumps(invoice, sort_keys=True).encode()
|
||||
invoice["signature"] = base64.b64encode(
|
||||
hmac.new(hmac_key, msg, hashlib.sha256).digest()
|
||||
).decode()
|
||||
return invoice
|
||||
|
||||
|
||||
def verify_payment_header(x_payment: str, expected_amount_usd: float) -> tuple[str, str] | None:
|
||||
"""Decode the X-Payment header. Returns (tx_hash, signature) or None.
|
||||
|
||||
Format: base64({"tx_hash": "...", "signature": "..."})
|
||||
"""
|
||||
if not x_payment:
|
||||
return None
|
||||
try:
|
||||
decoded = base64.b64decode(x_payment).decode()
|
||||
data = json.loads(decoded)
|
||||
return data.get("tx_hash", ""), data.get("signature", "")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def verify_payment_on_chain(tx_hash: str, expected_amount_usd: float) -> bool:
|
||||
"""Verify an on-chain payment.
|
||||
|
||||
Checks via public RPC that the transaction:
|
||||
1. Exists on-chain
|
||||
2. Transferred >= expected_amount_usd of USDC
|
||||
3. Was sent to our payment address
|
||||
|
||||
Supports Base (USDC) and Solana (USDC) — our primary payment chains.
|
||||
Falls back to the main x402_enforcement self-verify for other chains.
|
||||
"""
|
||||
if not tx_hash:
|
||||
return False
|
||||
|
||||
# Route to the production x402 enforcement system
|
||||
try:
|
||||
from app.routers.x402_enforcement import verify_self_payment
|
||||
result = await verify_self_payment(tx_hash, expected_amount_usd)
|
||||
return result.get("verified", False)
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.getLogger("wp.x402").error(f"verify_payment_on_chain error: {e}")
|
||||
|
||||
# Local verification for Base USDC via public RPC
|
||||
if tx_hash.startswith("0x") and len(tx_hash) == 66:
|
||||
try:
|
||||
import httpx
|
||||
pay_to = os.getenv("X402_PAYMENT_ADDRESS", "")
|
||||
if not pay_to:
|
||||
return False
|
||||
|
||||
async with httpx.AsyncClient(timeout=15) as c:
|
||||
r = await c.post("https://mainnet.base.org", json={
|
||||
"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt",
|
||||
"params": [tx_hash],
|
||||
})
|
||||
data = r.json()
|
||||
if "result" not in data or data["result"] is None:
|
||||
return False
|
||||
|
||||
receipt = data["result"]
|
||||
if receipt.get("status") != "0x1":
|
||||
return False
|
||||
|
||||
from_address = receipt.get("from", "").lower()
|
||||
to_address = receipt.get("to", "").lower()
|
||||
|
||||
# Check recipient matches our payment address
|
||||
if to_address and pay_to.lower() != to_address:
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.getLogger("wp.x402").error(f"Base tx verify failed: {e}")
|
||||
return False
|
||||
|
||||
# Solana verification via public RPC
|
||||
if len(tx_hash) == 88: # Solana base58 signature length
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=15) as c:
|
||||
r = await c.post("https://api.mainnet-beta.solana.com", json={
|
||||
"jsonrpc": "2.0", "id": 1, "method": "getTransaction",
|
||||
"params": [tx_hash, {"encoding": "json", "maxSupportedTransactionVersion": 0}],
|
||||
})
|
||||
data = r.json()
|
||||
if "result" not in data or data["result"] is None:
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.getLogger("wp.x402").error(f"Solana tx verify failed: {e}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def record_receipt(catalog, tx_hash: str, tool: str, agent_id: str, amount_usd: float) -> bool:
|
||||
"""Log the payment receipt to x402_receipts table."""
|
||||
if not catalog._health.postgres:
|
||||
return False
|
||||
try:
|
||||
async with catalog._pg_pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""INSERT INTO x402_receipts (tx_hash, agent_id, tool, amount_usd, chain, paid_at, tier)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW(), $6)
|
||||
ON CONFLICT (tx_hash) DO NOTHING""",
|
||||
tx_hash, agent_id, tool, amount_usd,
|
||||
os.getenv("X402_PAYMENT_CHAIN", "base"),
|
||||
os.getenv("X402_DEFAULT_TIER", "pro"),
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning(f"receipt_record_fail: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ── Main middleware ────────────────────────────────────────────────
|
||||
async def require_payment(
|
||||
tool: str,
|
||||
request: Request | None = None,
|
||||
catalog=None,
|
||||
) -> dict:
|
||||
"""Verify x402 payment. Returns payment context dict.
|
||||
|
||||
Raises HTTPException 402 if payment is required but not provided.
|
||||
Raises HTTPException 429 if rate limit is exceeded.
|
||||
"""
|
||||
agent_id = _agent_id_from_request(request)
|
||||
if not catalog or not catalog._health.redis:
|
||||
# No rate limiting available — log and pass
|
||||
log.debug(f"x402_no_redis: {tool} by {agent_id}")
|
||||
else:
|
||||
await _check_rate_limit(catalog._redis, agent_id)
|
||||
|
||||
# Free tool — no payment required
|
||||
if get_tool_price_usd(tool) == 0:
|
||||
return {"agent_id": agent_id, "tool": tool, "tier": "free", "paid_via_x402": None}
|
||||
|
||||
# Paid tool — check X-Payment header
|
||||
if request is None:
|
||||
invoice = create_invoice(tool, agent_id)
|
||||
raise HTTPException(
|
||||
status_code=402,
|
||||
detail={
|
||||
"error": "payment_required",
|
||||
"invoice": invoice,
|
||||
"payment_url": f"{os.getenv('X402_PAYMENT_URL', 'https://pay.rugmunch.io')}/{invoice['id']}",
|
||||
"instructions": "Pay on-chain, then retry with X-Payment header (base64 of {tx_hash, signature})",
|
||||
},
|
||||
)
|
||||
|
||||
x_payment = request.headers.get("X-Payment")
|
||||
if not x_payment:
|
||||
invoice = create_invoice(tool, agent_id)
|
||||
raise HTTPException(
|
||||
status_code=402,
|
||||
detail={
|
||||
"error": "payment_required",
|
||||
"invoice": invoice,
|
||||
"payment_url": f"{os.getenv('X402_PAYMENT_URL', 'https://pay.rugmunch.io')}/{invoice['id']}",
|
||||
"instructions": "Pay on-chain, then retry with X-Payment header (base64 of {tx_hash, signature})",
|
||||
},
|
||||
)
|
||||
|
||||
decoded = verify_payment_header(x_payment, get_tool_price_usd(tool))
|
||||
if not decoded:
|
||||
raise HTTPException(status_code=402, detail="invalid X-Payment format")
|
||||
tx_hash, _sig = decoded
|
||||
if not await verify_payment_on_chain(tx_hash, get_tool_price_usd(tool)):
|
||||
raise HTTPException(status_code=402, detail="invalid payment proof")
|
||||
|
||||
# T02: Burn the nonce atomically. Same tx_hash cannot be used twice.
|
||||
if not await _burn_nonce(catalog, tx_hash):
|
||||
raise HTTPException(
|
||||
status_code=410,
|
||||
detail={
|
||||
"error": "payment_already_used",
|
||||
"tx_hash": tx_hash,
|
||||
"message": "This payment receipt has already been used. Each tx_hash can only be used once.",
|
||||
},
|
||||
)
|
||||
_X402_REVENUE_COUNTER.labels(tool=tool, tier="pro").inc(int(get_tool_price_usd(tool) * 100))
|
||||
|
||||
# Record the receipt
|
||||
if catalog:
|
||||
await record_receipt(
|
||||
catalog, tx_hash, tool, agent_id, get_tool_price_usd(tool)
|
||||
)
|
||||
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"tool": tool,
|
||||
"tier": "pro",
|
||||
"paid_via_x402": tx_hash,
|
||||
}
|
||||
86
app/domain/x402/models.py
Normal file
86
app/domain/x402/models.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""Pydantic v2 models for the x402 domain."""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class X402Tier(StrEnum):
|
||||
"""x402 payment tier — maps to scanner tiers."""
|
||||
|
||||
FREE = "free"
|
||||
PRO = "pro"
|
||||
ELITE = "elite"
|
||||
INTERNAL = "internal"
|
||||
|
||||
|
||||
class ToolPricing(BaseModel):
|
||||
"""Per-tool pricing in x402."""
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
tool: str
|
||||
price_usd: float = Field(default=0.0, ge=0)
|
||||
price_crypto: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description="Crypto amounts: {'sol': '0.01', 'eth': '0.001', 'btc': '0.00001', 'trx': '5'}",
|
||||
)
|
||||
tier_required: X402Tier = X402Tier.FREE
|
||||
|
||||
|
||||
class ToolCatalogEntry(BaseModel):
|
||||
"""A single tool in the x402 catalog."""
|
||||
|
||||
name: str
|
||||
description: str = ""
|
||||
category: str = ""
|
||||
tier_required: X402Tier = X402Tier.FREE
|
||||
price_usd: float = 0.0
|
||||
endpoint: str = ""
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class ToolCatalog(BaseModel):
|
||||
"""Full x402 tool catalog."""
|
||||
|
||||
tools: list[ToolCatalogEntry] = Field(default_factory=list)
|
||||
total: int = 0
|
||||
categories: list[str] = Field(default_factory=list)
|
||||
fetched_at: str = ""
|
||||
|
||||
|
||||
class PaymentFacilitator(BaseModel):
|
||||
"""x402 payment facilitator info."""
|
||||
|
||||
name: str
|
||||
url: str
|
||||
enabled: bool = True
|
||||
chains: list[str] = Field(default_factory=list)
|
||||
health: str = "unknown"
|
||||
|
||||
|
||||
class PaymentRequest(BaseModel):
|
||||
"""x402 payment request."""
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
tool: str
|
||||
chain: str = Field(default="solana")
|
||||
user_address: str | None = None
|
||||
|
||||
@field_validator("chain")
|
||||
@classmethod
|
||||
def _chain_lowercase(cls, v: str) -> str:
|
||||
return v.lower().strip()
|
||||
|
||||
|
||||
class PaymentReceipt(BaseModel):
|
||||
"""x402 payment receipt."""
|
||||
|
||||
tool: str
|
||||
chain: str
|
||||
tx_hash: str | None = None
|
||||
amount_paid: dict[str, str] = Field(default_factory=dict)
|
||||
status: str = "pending" # pending | confirmed | failed
|
||||
timestamp: str = ""
|
||||
155
app/domain/x402/router.py
Normal file
155
app/domain/x402/router.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"""T34 x402 Paid Tools Catalog.
|
||||
|
||||
Per v4.0 §T34. Endpoints:
|
||||
GET /api/v1/x402/catalog — list all tools with pricing tiers
|
||||
GET /api/v1/x402/usage — per-agent usage stats
|
||||
GET /api/v1/x402/receipts/{tx_hash} — payment receipt
|
||||
POST /api/v1/x402/verify — verify a payment header
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.catalog.service import get_catalog
|
||||
from app.domain.x402.middleware import (
|
||||
PRICING,
|
||||
get_tool_metadata,
|
||||
get_tool_price_usd,
|
||||
verify_payment_header,
|
||||
verify_payment_on_chain,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/x402", tags=["x402"])
|
||||
|
||||
|
||||
class CatalogResponse(BaseModel):
|
||||
server: str = "rugmunch-intelligence"
|
||||
version: str = "4.0"
|
||||
tools: list[dict[str, Any]] = Field(default_factory=list)
|
||||
payment_protocol: str = "x402"
|
||||
|
||||
|
||||
class UsageResponse(BaseModel):
|
||||
agent_id: str
|
||||
daily_calls: int
|
||||
free_limit: int
|
||||
paid_calls: int
|
||||
total_usd: float
|
||||
recent_receipts: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ReceiptResponse(BaseModel):
|
||||
tx_hash: str
|
||||
tool: str
|
||||
agent_id: str | None = None
|
||||
amount_usd: float
|
||||
chain: str | None = None
|
||||
paid_at: str | None = None
|
||||
tier: str | None = None
|
||||
|
||||
|
||||
@router.get("/catalog", response_model=CatalogResponse)
|
||||
async def catalog() -> CatalogResponse:
|
||||
"""List all paid tools with pricing tiers."""
|
||||
return CatalogResponse(tools=[get_tool_metadata(t) for t in PRICING])
|
||||
|
||||
|
||||
@router.get("/usage", response_model=UsageResponse)
|
||||
async def usage(request: Request) -> UsageResponse:
|
||||
"""Per-agent usage stats: calls made, $ spent, top tools."""
|
||||
catalog = get_catalog()
|
||||
await catalog._init_stores()
|
||||
agent_id = request.headers.get("X-Agent-Id") or request.headers.get("X-Api-Key") or "anon"
|
||||
daily_calls = 0
|
||||
paid_calls = 0
|
||||
total_usd = 0.0
|
||||
recent: list[dict] = []
|
||||
if catalog._health.redis:
|
||||
try:
|
||||
from datetime import UTC, datetime
|
||||
day = datetime.now(UTC).strftime("%Y%m%d")
|
||||
key = f"x402:rl:{agent_id}:{day}"
|
||||
daily_calls = int(await catalog._redis.get(key) or 0)
|
||||
except Exception:
|
||||
pass
|
||||
if catalog._health.postgres:
|
||||
try:
|
||||
async with catalog._pg_pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"SELECT tx_hash, tool, amount_usd, paid_at FROM x402_receipts "
|
||||
"WHERE agent_id = $1 ORDER BY paid_at DESC LIMIT 10",
|
||||
agent_id,
|
||||
)
|
||||
for r in rows:
|
||||
paid_calls += 1
|
||||
total_usd += float(r["amount_usd"] or 0)
|
||||
recent.append({
|
||||
"tx_hash": r["tx_hash"],
|
||||
"tool": r["tool"],
|
||||
"amount_usd": float(r["amount_usd"] or 0),
|
||||
"paid_at": r["paid_at"].isoformat() if r["paid_at"] else None,
|
||||
})
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).warning(f"usage_query_fail: {e}")
|
||||
return UsageResponse(
|
||||
agent_id=agent_id, daily_calls=daily_calls, free_limit=5,
|
||||
paid_calls=paid_calls, total_usd=round(total_usd, 4),
|
||||
recent_receipts=recent,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/receipts/{tx_hash}", response_model=ReceiptResponse)
|
||||
async def receipt(tx_hash: str) -> ReceiptResponse:
|
||||
"""Payment receipt for a specific on-chain tx."""
|
||||
catalog = get_catalog()
|
||||
await catalog._init_stores()
|
||||
if not catalog._health.postgres:
|
||||
raise HTTPException(503, "postgres unavailable")
|
||||
try:
|
||||
async with catalog._pg_pool.acquire() as conn:
|
||||
r = await conn.fetchrow(
|
||||
"SELECT * FROM x402_receipts WHERE tx_hash=$1", tx_hash
|
||||
)
|
||||
if not r:
|
||||
raise HTTPException(404, "receipt not found")
|
||||
d = dict(r)
|
||||
return ReceiptResponse(
|
||||
tx_hash=d["tx_hash"],
|
||||
tool=d["tool"],
|
||||
agent_id=d.get("agent_id"),
|
||||
amount_usd=float(d.get("amount_usd", 0)),
|
||||
chain=d.get("chain"),
|
||||
paid_at=d["paid_at"].isoformat() if d.get("paid_at") else None,
|
||||
tier=d.get("tier"),
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"receipt_query_fail: {e}")
|
||||
|
||||
|
||||
@router.post("/verify")
|
||||
async def verify_payment(request: Request) -> dict:
|
||||
"""Verify an X-Payment header for a given tool."""
|
||||
body = await request.json()
|
||||
tool = body.get("tool", "")
|
||||
x_payment = body.get("x_payment", "")
|
||||
if tool not in PRICING:
|
||||
raise HTTPException(404, f"unknown tool: {tool}")
|
||||
if get_tool_price_usd(tool) == 0:
|
||||
return {"valid": True, "tier": "free", "amount_usd": 0}
|
||||
decoded = verify_payment_header(x_payment, get_tool_price_usd(tool))
|
||||
if not decoded:
|
||||
return {"valid": False, "reason": "invalid format"}
|
||||
tx_hash, _ = decoded
|
||||
valid = await verify_payment_on_chain(tx_hash, get_tool_price_usd(tool))
|
||||
return {
|
||||
"valid": valid,
|
||||
"tool": tool,
|
||||
"amount_usd": get_tool_price_usd(tool),
|
||||
"tx_hash": tx_hash,
|
||||
}
|
||||
130
app/domain/x402/service.py
Normal file
130
app/domain/x402/service.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""x402 service — facade over legacy x402 routers.
|
||||
|
||||
Calls into the existing x402 catalog/enforcement routers. As those
|
||||
routers are rewritten into proper domain modules, this service
|
||||
stops calling them and uses the new modules instead.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.domain.x402.models import (
|
||||
PaymentFacilitator,
|
||||
ToolCatalog,
|
||||
ToolCatalogEntry,
|
||||
X402Tier,
|
||||
)
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class X402Service:
|
||||
"""Async facade over the x402 payment system."""
|
||||
|
||||
async def get_catalog(self) -> ToolCatalog:
|
||||
"""Fetch the full x402 tool catalog.
|
||||
|
||||
Delegates to app.routers.x402_catalog.list_tools_catalog() (the
|
||||
legacy catalog endpoint function). As that gets migrated, the
|
||||
service will use the new domain catalog module directly.
|
||||
"""
|
||||
log.info("x402_catalog_fetch_started")
|
||||
try:
|
||||
import app.routers.x402_catalog as _catalog_mod
|
||||
raw = await _catalog_mod.list_tools_catalog()
|
||||
return self._wrap_catalog(raw)
|
||||
except Exception as e:
|
||||
log.warning("x402_catalog_fetch_failed", error=str(e))
|
||||
return ToolCatalog()
|
||||
|
||||
async def get_facilitators(self) -> list[PaymentFacilitator]:
|
||||
"""Fetch list of enabled payment facilitators.
|
||||
|
||||
The legacy enforcement module doesn't expose a single getter
|
||||
function — facilitator info is in module-level constants. This
|
||||
facade returns a derived list from those constants when the
|
||||
function is missing, and falls back to empty otherwise.
|
||||
"""
|
||||
log.info("x402_facilitators_fetch_started")
|
||||
try:
|
||||
import app.routers.x402_enforcement as _enf_mod
|
||||
getter = getattr(_enf_mod, "get_facilitator_health", None)
|
||||
if getter is None:
|
||||
return self._default_facilitators()
|
||||
raw = getter()
|
||||
if asyncio.iscoroutine(raw):
|
||||
raw = await raw
|
||||
return self._wrap_facilitators(raw)
|
||||
except Exception as e:
|
||||
log.warning("x402_facilitators_fetch_failed", error=str(e))
|
||||
return self._default_facilitators()
|
||||
|
||||
@staticmethod
|
||||
def _default_facilitators() -> list[PaymentFacilitator]:
|
||||
"""Default facilitator list — matches the legacy enforcement config."""
|
||||
return [
|
||||
PaymentFacilitator(name="Coinbase CDP", url="https://api.cdp.coinbase.com", enabled=True, chains=["base", "ethereum"], health="unknown"),
|
||||
PaymentFacilitator(name="PayAI", url="https://payai.example", enabled=True, chains=["solana", "base"], health="unknown"),
|
||||
PaymentFacilitator(name="Cloudflare x402", url="https://x402.cloudflare.com", enabled=True, chains=["base", "polygon"], health="unknown"),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _wrap_catalog(raw: Any) -> ToolCatalog:
|
||||
"""Convert legacy catalog response → Pydantic ToolCatalog."""
|
||||
tools: list[ToolCatalogEntry] = []
|
||||
categories: set[str] = set()
|
||||
if isinstance(raw, dict):
|
||||
raw_tools = raw.get("tools", [])
|
||||
total = raw.get("total", len(raw_tools))
|
||||
categories = set(raw.get("categories", []))
|
||||
elif isinstance(raw, list):
|
||||
raw_tools = raw
|
||||
total = len(raw)
|
||||
else:
|
||||
return ToolCatalog()
|
||||
|
||||
for t in raw_tools:
|
||||
if isinstance(t, dict):
|
||||
try:
|
||||
tier_str = t.get("tier_required", t.get("tier", "free"))
|
||||
entry = ToolCatalogEntry(
|
||||
name=t.get("name", t.get("tool", "")),
|
||||
description=t.get("description", ""),
|
||||
category=t.get("category", ""),
|
||||
tier_required=X402Tier(tier_str.lower() if isinstance(tier_str, str) else "free"),
|
||||
price_usd=float(t.get("price_usd", 0) or 0),
|
||||
endpoint=t.get("endpoint", ""),
|
||||
enabled=bool(t.get("enabled", True)),
|
||||
)
|
||||
tools.append(entry)
|
||||
if entry.category:
|
||||
categories.add(entry.category)
|
||||
except Exception:
|
||||
continue
|
||||
return ToolCatalog(
|
||||
tools=tools,
|
||||
total=total,
|
||||
categories=sorted(categories),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _wrap_facilitators(raw: Any) -> list[PaymentFacilitator]:
|
||||
"""Convert legacy facilitator list → Pydantic list."""
|
||||
out: list[PaymentFacilitator] = []
|
||||
items: list[dict] = []
|
||||
if isinstance(raw, dict):
|
||||
items = raw.get("facilitators", raw.get("data", []))
|
||||
elif isinstance(raw, list):
|
||||
items = raw
|
||||
for f in items:
|
||||
if isinstance(f, dict):
|
||||
out.append(PaymentFacilitator(
|
||||
name=f.get("name", ""),
|
||||
url=f.get("url", ""),
|
||||
enabled=bool(f.get("enabled", True)),
|
||||
chains=f.get("chains", []) or [],
|
||||
health=f.get("health", "unknown"),
|
||||
))
|
||||
return out
|
||||
157
app/domain/x402/tools/__init__.py
Normal file
157
app/domain/x402/tools/__init__.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"""x402 tools registry - main entry point for x402 tool implementations.
|
||||
|
||||
Per v4.0 §T26. Provides a unified interface to all x402-powered tools.
|
||||
Tools are delegated to domain/x402/tools/*.py for organization.
|
||||
|
||||
Registry shape:
|
||||
tools = {
|
||||
"tool_name": {
|
||||
"function": async function,
|
||||
"free_tier": bool,
|
||||
"pro_cents": int | None,
|
||||
"ent_cents": int | None,
|
||||
"description": str,
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
from app.domain.x402.tools.label_tools import get_entity_labels, get_wallet_labels
|
||||
from app.domain.x402.tools.market_tools import get_market_overview, get_trending
|
||||
from app.domain.x402.tools.news_tools import get_news, get_news_sentiment
|
||||
from app.domain.x402.tools.report_tools import generate_report, get_report
|
||||
from app.domain.x402.tools.scanner_tools import get_scan_result, run_scan
|
||||
from app.domain.x402.tools.token_tools import get_token_info, get_token_risk
|
||||
from app.domain.x402.tools.wallet_tools import get_wallet_analysis, get_wallet_history
|
||||
|
||||
# Tool registry
|
||||
TOOLS = {
|
||||
"get_token_risk": {
|
||||
"function": get_token_risk,
|
||||
"free_tier": True,
|
||||
"pro_cents": 50,
|
||||
"ent_cents": 40,
|
||||
"description": "Analyze token for rug pull indicators, honeypots, mintability",
|
||||
},
|
||||
"get_token_info": {
|
||||
"function": get_token_info,
|
||||
"free_tier": True,
|
||||
"pro_cents": 10,
|
||||
"ent_cents": 8,
|
||||
"description": "Get basic token metadata (name, symbol, supply, holders)",
|
||||
},
|
||||
"get_wallet_analysis": {
|
||||
"function": get_wallet_analysis,
|
||||
"free_tier": False,
|
||||
"pro_cents": 200,
|
||||
"ent_cents": 150,
|
||||
"description": "Deep wallet behavior analysis (PnL, trading patterns, personas)",
|
||||
},
|
||||
"get_wallet_history": {
|
||||
"function": get_wallet_history,
|
||||
"free_tier": False,
|
||||
"pro_cents": 100,
|
||||
"ent_cents": 80,
|
||||
"description": "Get transaction history for a wallet",
|
||||
},
|
||||
"get_market_overview": {
|
||||
"function": get_market_overview,
|
||||
"free_tier": True,
|
||||
"pro_cents": 50,
|
||||
"ent_cents": 40,
|
||||
"description": "Market overview for a chain (top tokens, volume, trend)",
|
||||
},
|
||||
"get_trending": {
|
||||
"function": get_trending,
|
||||
"free_tier": True,
|
||||
"pro_cents": 30,
|
||||
"ent_cents": 25,
|
||||
"description": "Get trending tokens on a chain",
|
||||
},
|
||||
"run_scan": {
|
||||
"function": run_scan,
|
||||
"free_tier": True,
|
||||
"pro_cents": 100,
|
||||
"ent_cents": 80,
|
||||
"description": "Run full SENTINEL scan on a token",
|
||||
},
|
||||
"get_scan_result": {
|
||||
"function": get_scan_result,
|
||||
"free_tier": False,
|
||||
"pro_cents": 50,
|
||||
"ent_cents": 40,
|
||||
"description": "Get scan result for a token",
|
||||
},
|
||||
"generate_report": {
|
||||
"function": generate_report,
|
||||
"free_tier": False,
|
||||
"pro_cents": 500,
|
||||
"ent_cents": 400,
|
||||
"description": "Full AI research report (7 sections, $5)",
|
||||
},
|
||||
"get_report": {
|
||||
"function": get_report,
|
||||
"free_tier": False,
|
||||
"pro_cents": 200,
|
||||
"ent_cents": 150,
|
||||
"description": "Get previously generated report",
|
||||
},
|
||||
"get_news": {
|
||||
"function": get_news,
|
||||
"free_tier": True,
|
||||
"pro_cents": 30,
|
||||
"ent_cents": 25,
|
||||
"description": "Get news mentions for a token/wallet",
|
||||
},
|
||||
"get_news_sentiment": {
|
||||
"function": get_news_sentiment,
|
||||
"free_tier": True,
|
||||
"pro_cents": 50,
|
||||
"ent_cents": 40,
|
||||
"description": "Get news sentiment analysis",
|
||||
},
|
||||
"get_wallet_labels": {
|
||||
"function": get_wallet_labels,
|
||||
"free_tier": True,
|
||||
"pro_cents": 20,
|
||||
"ent_cents": 15,
|
||||
"description": "Get labels for a wallet address",
|
||||
},
|
||||
"get_entity_labels": {
|
||||
"function": get_entity_labels,
|
||||
"free_tier": False,
|
||||
"pro_cents": 50,
|
||||
"ent_cents": 40,
|
||||
"description": "Get entity labels from Neo4j graph",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def list_tools() -> list[dict]:
|
||||
"""List all available x402 tools."""
|
||||
return [
|
||||
{
|
||||
"tool_id": name,
|
||||
"description": info["description"],
|
||||
"free_tier": info["free_tier"],
|
||||
"pro_cents": info.get("pro_cents"),
|
||||
"ent_cents": info.get("ent_cents"),
|
||||
}
|
||||
for name, info in TOOLS.items()
|
||||
]
|
||||
|
||||
|
||||
def get_tool(tool_id: str) -> dict | None:
|
||||
"""Get a tool by ID."""
|
||||
return TOOLS.get(tool_id)
|
||||
|
||||
|
||||
async def call_tool(tool_id: str, **kwargs) -> dict:
|
||||
"""Call a tool by ID with arguments."""
|
||||
tool = TOOLS.get(tool_id)
|
||||
if not tool:
|
||||
return {"error": f"Unknown tool: {tool_id}"}
|
||||
try:
|
||||
result = await tool["function"](**kwargs)
|
||||
return {"tool_id": tool_id, "result": result}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
27
app/domain/x402/tools/label_tools.py
Normal file
27
app/domain/x402/tools/label_tools.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""x402 label tools - get_wallet_labels, get_entity_labels."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def get_wallet_labels(wallet_address: str) -> dict[str, Any]:
|
||||
"""Get labels for a wallet."""
|
||||
return {
|
||||
"wallet_address": wallet_address,
|
||||
"labels": [],
|
||||
}
|
||||
|
||||
|
||||
async def get_entity_labels(entity_id: str) -> dict[str, Any]:
|
||||
"""Get entity labels from Neo4j graph."""
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"labels": [],
|
||||
}
|
||||
|
||||
|
||||
async def get_entity_by_labels(labels: list[str]) -> dict[str, Any]:
|
||||
"""Get entity by labels."""
|
||||
return {
|
||||
"labels": labels,
|
||||
"entities": [],
|
||||
}
|
||||
37
app/domain/x402/tools/market_tools.py
Normal file
37
app/domain/x402/tools/market_tools.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""x402 market tools - get_market_overview, get_trending."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def get_market_overview(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get market overview for a chain."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"market_status": "active",
|
||||
"top_tokens": [],
|
||||
"volume_24h": 0,
|
||||
"trending": [],
|
||||
}
|
||||
|
||||
|
||||
async def get_trending(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get trending tokens."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"trending_tokens": [],
|
||||
"time_window": "24h",
|
||||
}
|
||||
|
||||
|
||||
async def market_overview(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Alias for get_market_overview."""
|
||||
return get_market_overview(chain)
|
||||
|
||||
|
||||
async def token_deep_dive(token_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Deep dive on a token."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"deep_dive": "pending",
|
||||
}
|
||||
31
app/domain/x402/tools/news_tools.py
Normal file
31
app/domain/x402/tools/news_tools.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""x402 news tools - get_news, get_news_sentiment."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def get_news(token: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get news mentions for a token."""
|
||||
return {
|
||||
"token": token,
|
||||
"chain": chain,
|
||||
"news": [],
|
||||
"count": 0,
|
||||
}
|
||||
|
||||
|
||||
async def get_news_sentiment(token: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get news sentiment analysis."""
|
||||
return {
|
||||
"token": token,
|
||||
"chain": chain,
|
||||
"sentiment": "neutral",
|
||||
"articles": 0,
|
||||
}
|
||||
|
||||
|
||||
async def social_signal(query: str) -> dict[str, Any]:
|
||||
"""Get social signal."""
|
||||
return {
|
||||
"query": query,
|
||||
"signal": "pending",
|
||||
}
|
||||
220
app/domain/x402/tools/report_tools.py
Normal file
220
app/domain/x402/tools/report_tools.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"""x402 report tools - generate_report, get_report."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def generate_report(subject_type: str, subject_id: str, model: str = "deepseek-v3") -> dict[str, Any]:
|
||||
"""Generate a research report."""
|
||||
return {
|
||||
"report_id": "pending",
|
||||
"subject_type": subject_type,
|
||||
"subject_id": subject_id,
|
||||
"sections": {},
|
||||
"risk_score": 50,
|
||||
"markdown": "# Pending Report\n\n[Report generation pending]",
|
||||
}
|
||||
|
||||
|
||||
async def get_report(report_id: str) -> dict[str, Any]:
|
||||
"""Get a previously generated report."""
|
||||
return {
|
||||
"report_id": report_id,
|
||||
"status": "pending",
|
||||
"result": {},
|
||||
}
|
||||
|
||||
|
||||
async def social_sentiment(token: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get social sentiment analysis."""
|
||||
return {
|
||||
"token": token,
|
||||
"chain": chain,
|
||||
"sentiment": "neutral",
|
||||
"mentions": 0,
|
||||
}
|
||||
|
||||
|
||||
async def cluster_detection(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Run cluster detection."""
|
||||
return {
|
||||
"wallet_address": wallet_address,
|
||||
"chain": chain,
|
||||
"clusters": [],
|
||||
}
|
||||
|
||||
|
||||
async def insider_tracker(creator: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Track insider activity."""
|
||||
return {
|
||||
"creator": creator,
|
||||
"chain": chain,
|
||||
"insider_activity": [],
|
||||
}
|
||||
|
||||
|
||||
async def twitter_profile(query: str) -> dict[str, Any]:
|
||||
"""Get Twitter profile."""
|
||||
return {
|
||||
"query": query,
|
||||
"profile": {},
|
||||
}
|
||||
|
||||
|
||||
async def twitter_timeline(query: str) -> dict[str, Any]:
|
||||
"""Get Twitter timeline."""
|
||||
return {
|
||||
"query": query,
|
||||
"tweets": [],
|
||||
}
|
||||
|
||||
|
||||
async def twitter_search(query: str) -> dict[str, Any]:
|
||||
"""Search Twitter."""
|
||||
return {
|
||||
"query": query,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
async def launch_radar(chain: str = "solana", window_minutes: int = 5) -> dict[str, Any]:
|
||||
"""Get launch radar."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"window_minutes": window_minutes,
|
||||
"launches": [],
|
||||
}
|
||||
|
||||
|
||||
async def launch_intel(chain: str = "solana", hours: int = 24) -> dict[str, Any]:
|
||||
"""Get launch intelligence."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"hours": hours,
|
||||
"launches": [],
|
||||
}
|
||||
|
||||
|
||||
async def token_pulse(token_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get token pulse."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"pulse": "pending",
|
||||
}
|
||||
|
||||
|
||||
async def anomaly_detector(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Run anomaly detection."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"anomalies": [],
|
||||
}
|
||||
|
||||
|
||||
async def whale_decoder(address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Decode whale activity."""
|
||||
return {
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"whale_activity": [],
|
||||
}
|
||||
|
||||
|
||||
async def chain_health(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Check chain health."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"health": "pending",
|
||||
}
|
||||
|
||||
|
||||
async def portfolio_tracker(addresses: list[str], chain: str = "solana") -> dict[str, Any]:
|
||||
"""Track portfolio."""
|
||||
return {
|
||||
"addresses": addresses,
|
||||
"chain": chain,
|
||||
"portfolio": [],
|
||||
}
|
||||
|
||||
|
||||
async def copy_trade_finder(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Find copy trade opportunities."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"opportunities": [],
|
||||
}
|
||||
|
||||
|
||||
async def risk_monitor(address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Monitor risk."""
|
||||
return {
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"risk_level": "pending",
|
||||
}
|
||||
|
||||
|
||||
async def defi_yield_scanner(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Scan DeFi yield opportunities."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"opportunities": [],
|
||||
}
|
||||
|
||||
|
||||
async def nft_wash_detector(collection: str) -> dict[str, Any]:
|
||||
"""Detect NFT wash trading."""
|
||||
return {
|
||||
"collection": collection,
|
||||
"wash_trading": False,
|
||||
}
|
||||
|
||||
|
||||
async def bridge_security() -> dict[str, Any]:
|
||||
"""Check bridge security."""
|
||||
return {
|
||||
"bridge_security": "pending",
|
||||
}
|
||||
|
||||
|
||||
async def gas_forecast(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Forecast gas prices."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"gas_forecast": "pending",
|
||||
}
|
||||
|
||||
|
||||
async def sniper_alert(chain: str = "solana", hours: int = 24) -> dict[str, Any]:
|
||||
"""Get sniper alerts."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"hours": hours,
|
||||
"sniper_alerts": [],
|
||||
}
|
||||
|
||||
|
||||
async def liquidity_flow(address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Track liquidity flow."""
|
||||
return {
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"liquidity_flow": [],
|
||||
}
|
||||
|
||||
|
||||
async def rug_pull_predictor(address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Predict rug pull risk."""
|
||||
return {
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"risk_score": 50,
|
||||
}
|
||||
|
||||
|
||||
async def airdrop_finder(chain: str = "solana") -> dict[str, Any]:
|
||||
"""Find airdrop opportunities."""
|
||||
return {
|
||||
"chain": chain,
|
||||
"airdrops": [],
|
||||
}
|
||||
50
app/domain/x402/tools/scanner_tools.py
Normal file
50
app/domain/x402/tools/scanner_tools.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""x402 scanner tools - run_scan, get_scan_result."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def run_scan(token_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Run full SENTINEL scan."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"scan_status": "pending",
|
||||
"modules_run": [],
|
||||
"safety_score": 50,
|
||||
}
|
||||
|
||||
|
||||
async def get_scan_result(scan_id: str) -> dict[str, Any]:
|
||||
"""Get scan result by ID."""
|
||||
return {
|
||||
"scan_id": scan_id,
|
||||
"status": "pending",
|
||||
"result": {},
|
||||
}
|
||||
|
||||
|
||||
async def rug_shield(token_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Run rug shield scan."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"rug_shield": "pending",
|
||||
}
|
||||
|
||||
|
||||
async def honeypot_check(token_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Check honeypot status."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"is_honeypot": False,
|
||||
}
|
||||
|
||||
|
||||
async def token_forensics(token_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Run token forensics."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"forensics": "pending",
|
||||
}
|
||||
37
app/domain/x402/tools/token_tools.py
Normal file
37
app/domain/x402/tools/token_tools.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""x402 token tools - get_token_risk, get_token_info, deep_contract_audit."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def get_token_risk(token_address: str, chain: str = "ethereum") -> dict[str, Any]:
|
||||
"""Get risk analysis for a token."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"risk_score": 50, # Placeholder
|
||||
"risk_tier": "UNKNOWN",
|
||||
"risk_factors": [],
|
||||
"notes": "Token risk analysis - full implementation in x402_tools.py",
|
||||
}
|
||||
|
||||
|
||||
async def get_token_info(token_address: str, chain: str = "ethereum") -> dict[str, Any]:
|
||||
"""Get basic token info."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"symbol": "UNKNOWN",
|
||||
"name": "Unknown Token",
|
||||
"decimals": 0,
|
||||
"total_supply": 0,
|
||||
}
|
||||
|
||||
|
||||
async def deep_contract_audit(token_address: str, chain: str = "ethereum") -> dict[str, Any]:
|
||||
"""Deep contract audit."""
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"audit_result": "PENDING",
|
||||
"findings": [],
|
||||
}
|
||||
34
app/domain/x402/tools/wallet_tools.py
Normal file
34
app/domain/x402/tools/wallet_tools.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""x402 wallet tools - get_wallet_analysis, get_wallet_history, wallet_profiler."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def get_wallet_analysis(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get wallet behavior analysis."""
|
||||
return {
|
||||
"wallet_address": wallet_address,
|
||||
"chain": chain,
|
||||
"analysis": "pending",
|
||||
"personas": [],
|
||||
"risk_score": 50,
|
||||
}
|
||||
|
||||
|
||||
async def get_wallet_history(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Get wallet transaction history."""
|
||||
return {
|
||||
"wallet_address": wallet_address,
|
||||
"chain": chain,
|
||||
"transactions": [],
|
||||
"total_count": 0,
|
||||
}
|
||||
|
||||
|
||||
async def wallet_profiler(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
|
||||
"""Profile a wallet."""
|
||||
return {
|
||||
"wallet_address": wallet_address,
|
||||
"chain": chain,
|
||||
"profile": "pending",
|
||||
"metrics": {},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue