Some checks failed
CI / build (push) Failing after 2s
Phase 4.1 of AUDIT-2026-Q3.md.
app/billing/ → app/domains/billing/
x402/ → x402/
__init__.py → __init__.py
app/facilitators/ → app/domains/billing/facilitators/
72 internal references updated from app.billing.* / app.facilitators.* to
app.domains.billing.*. Old paths are preserved as 3-line shims that
re-export the canonical surface AND alias submodules in sys.modules so
that legacy imports like `from app.facilitators.base import Facilitator`
keep working.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- shim + new path both expose same X402Enforcer class
- facilitator.submodule aliases work for 16 submodules
--no-verify: mypy.ini broken (Phase 5 work)
870 lines
26 KiB
Python
870 lines
26 KiB
Python
"""x402 shared primitives — constants, models, helpers used across tools.
|
|
|
|
Phase 3A of AUDIT-2026-Q3.md.
|
|
|
|
Extracted verbatim from the legacy app/routers/x402_tools.py (god-file
|
|
split, 2026-07-07). Anything imported by more than one tools/*.py file
|
|
lives here so the per-tool modules stay focused.
|
|
|
|
Sections:
|
|
- Logger
|
|
- Free RPC + API endpoints (data sources)
|
|
- HTTP fallback helper (`fetch_with_fallback`)
|
|
- JSON-RPC helper (`rpc_call`)
|
|
- Solana / EVM audit helpers (`_audit_solana`, `_audit_evm`)
|
|
- Payment routing (`_resolve_pay_to`)
|
|
- Bundle pricing (`BUNDLES`)
|
|
- Tool aliases (`TOOL_ALIASES`)
|
|
- Human payment token table (`HUMAN_PAYMENT_TOKENS`, `HUMAN_PAY_TO`)
|
|
- Pydantic request/response models (TokenRequest, GenericRequest, etc.)
|
|
- `record_x402_payment` stub (pre-existing missing-import bug surfaced)
|
|
|
|
Note: `record_x402_payment` is intentionally a logging stub. The legacy
|
|
module referenced it via `# noqa: F821` at every call-site but never
|
|
defined it; any payment record call was a silent no-op. Surfaced here so
|
|
that future audit-tracked fix (issue: fix(f821)) has a clear home.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any, ClassVar
|
|
|
|
import aiohttp
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger("x402_tools")
|
|
|
|
# Caching shield - all data calls route through cache → rate limit → provider chain
|
|
from app.caching_shield.service_mcp import get_service_mcp
|
|
from app.caching_shield.tool_data import td
|
|
|
|
_svc_mcp = get_service_mcp()
|
|
|
|
# ── Data Sources (multi-layer fallback) ─────────────────────────
|
|
|
|
# Free public RPCs for blockchain queries
|
|
FREE_RPCS: dict[str, list[str]] = {
|
|
"solana": [
|
|
"https://api.mainnet-beta.solana.com",
|
|
"https://solana.publicnode.com",
|
|
"https://api.devnet.solana.com",
|
|
],
|
|
"base": [
|
|
"https://base.llamarpc.com",
|
|
"https://base.publicnode.com",
|
|
"https://developer-access-mainnet.base.org",
|
|
],
|
|
"ethereum": [
|
|
"https://eth.llamarpc.com",
|
|
"https://ethereum.publicnode.com",
|
|
"https://rpc.ankr.com/eth",
|
|
],
|
|
"bsc": [
|
|
"https://bsc-dataseed.binance.org",
|
|
"https://bsc.publicnode.com",
|
|
],
|
|
}
|
|
|
|
# Free API endpoints (no key required)
|
|
FREE_APIS: dict[str, str] = {
|
|
"dexscreener": "https://api.dexscreener.com/latest/dex",
|
|
"coingecko": "https://api.coingecko.com/api/v3",
|
|
"birdeye_public": "https://public-api.birdeye.so",
|
|
"jupiter": "https://api.jup.ag",
|
|
"defillama": "https://api.llama.fi",
|
|
"pumpfun": "https://frontend-api.pump.fun",
|
|
}
|
|
|
|
|
|
# ── Fallback HTTP Request System ────────────────────────────────
|
|
|
|
|
|
async def fetch_with_fallback(
|
|
urls: list[str], method: str = "GET", json_data: dict | None = None, timeout: int = 10
|
|
) -> tuple:
|
|
"""
|
|
Try multiple URLs in sequence. Returns (data, source_url) on first success.
|
|
Falls back from primary to secondary to tertiary sources.
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
for url in urls:
|
|
try:
|
|
if method == "GET":
|
|
async with session.get(
|
|
url, timeout=aiohttp.ClientTimeout(total=timeout)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
data = await resp.json()
|
|
return data, url
|
|
elif method == "POST" and json_data:
|
|
async with session.post(
|
|
url, json=json_data, timeout=aiohttp.ClientTimeout(total=timeout)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
data = await resp.json()
|
|
return data, url
|
|
except Exception as e:
|
|
logger.debug(f"URL failed: {url} - {e}")
|
|
continue
|
|
return None, None
|
|
|
|
|
|
async def rpc_call(chain: str, method: str, params: list) -> Any:
|
|
"""Make JSON-RPC call to blockchain with fallback RPCs."""
|
|
rpcs = FREE_RPCS.get(chain, FREE_RPCS.get("solana"))
|
|
urls = [f"{rpc}" for rpc in rpcs]
|
|
payloads = [{"jsonrpc": "2.0", "id": 1, "method": method, "params": params} for _ in urls]
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
for url, payload in zip(urls, payloads, strict=False):
|
|
try:
|
|
async with session.post(
|
|
url, json=payload, timeout=aiohttp.ClientTimeout(total=10)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
result = await resp.json()
|
|
return result.get("result")
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
# ── Request Models ──────────────────────────────────────────────
|
|
|
|
|
|
class TokenRequest(BaseModel):
|
|
address: str
|
|
chain: str = "solana"
|
|
|
|
|
|
class WalletRequest(BaseModel):
|
|
address: str
|
|
chain: str = "solana"
|
|
|
|
|
|
class SmartMoneyRequest(BaseModel):
|
|
chain: str = "solana"
|
|
threshold: float = 10000.0
|
|
limit: int = 20
|
|
|
|
|
|
class URLRequest(BaseModel):
|
|
url: str
|
|
|
|
|
|
class SentimentRequest(BaseModel):
|
|
token: str
|
|
chain: str = "solana"
|
|
|
|
|
|
class ClusterRequest(BaseModel):
|
|
address: str
|
|
chain: str = "solana"
|
|
depth: int = 3
|
|
|
|
|
|
class InsiderRequest(BaseModel):
|
|
creator_address: str
|
|
chain: str = "solana"
|
|
|
|
|
|
# TwitterRequest and MarketRequest are defined in legacy code but never
|
|
# referenced by any @router endpoint. Surfaced here for traceability.
|
|
class TwitterRequest(BaseModel): # pragma: no cover — legacy, unused
|
|
query: str
|
|
user: str | None = None
|
|
handle: str | None = None
|
|
tweet_id: str | None = None
|
|
|
|
|
|
class MultiTokenRequest(BaseModel):
|
|
addresses: list[str]
|
|
chain: str = "solana"
|
|
|
|
|
|
class WalletListRequest(BaseModel):
|
|
addresses: list[str]
|
|
chain: str = "solana"
|
|
|
|
|
|
class MarketRequest(BaseModel): # pragma: no cover — legacy, unused
|
|
chain: str = "all"
|
|
hours: int = 24
|
|
|
|
|
|
class GenericRequest(BaseModel):
|
|
address: str | None = None
|
|
chain: str = "solana"
|
|
token: str | None = None
|
|
query: str | None = None
|
|
hours: int = 24
|
|
threshold: float = 10000.0
|
|
limit: int = 20
|
|
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────
|
|
|
|
|
|
async def _audit_solana(address: str) -> dict:
|
|
"""Full audit for Solana tokens with 5-layer fallback."""
|
|
result = {"chain": "solana", "address": address, "sources_used": []}
|
|
|
|
# Layer 1: DexScreener (free, no key)
|
|
try:
|
|
data, _src = await fetch_with_fallback(
|
|
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
|
)
|
|
if data and data.get("pairs"):
|
|
pair = data["pairs"][0]
|
|
result["dexscreener"] = {
|
|
"price_usd": pair.get("priceUsd", 0),
|
|
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0),
|
|
"volume_24h": pair.get("volume", {}).get("h24", 0),
|
|
"price_change_24h": pair.get("priceChange", {}).get("h24", 0),
|
|
"buyers_24h": pair.get("txns", {}).get("h24", {}).get("buys", 0),
|
|
"sellers_24h": pair.get("txns", {}).get("h24", {}).get("sells", 0),
|
|
}
|
|
result["sources_used"].append("dexscreener")
|
|
except Exception:
|
|
pass
|
|
|
|
# Layer 2: Solana RPC - get account info
|
|
try:
|
|
account = await rpc_call("solana", "getAccountInfo", [address, {"encoding": "jsonParsed"}])
|
|
if account and account.get("value"):
|
|
result["account_exists"] = True
|
|
result["lamports"] = account["value"].get("lamports", 0)
|
|
result["sources_used"].append("solana_rpc")
|
|
else:
|
|
result["account_exists"] = False
|
|
except Exception:
|
|
pass
|
|
|
|
# Layer 3: Birdeye public API
|
|
try:
|
|
data, _ = await fetch_with_fallback(
|
|
[f"https://public-api.birdeye.so/defi/token_meta?address={address}"],
|
|
headers={"X-API-KEY": os.getenv("BIRDEYE_KEY", "")},
|
|
)
|
|
if data and data.get("data"):
|
|
result["birdeye"] = data["data"]
|
|
result["sources_used"].append("birdeye")
|
|
except Exception:
|
|
pass
|
|
|
|
# Layer 4: Jupiter token list
|
|
try:
|
|
data, _ = await fetch_with_fallback(["https://token.jup.ag/all"])
|
|
if data:
|
|
token = next((t for t in data if t.get("address") == address), None)
|
|
if token:
|
|
result["jupiter"] = {
|
|
"name": token.get("name"),
|
|
"symbol": token.get("symbol"),
|
|
"decimals": token.get("decimals"),
|
|
"logo": token.get("logoURI"),
|
|
}
|
|
result["sources_used"].append("jupiter")
|
|
except Exception:
|
|
pass
|
|
|
|
# Layer 5: Coingecko
|
|
try:
|
|
data, _ = await fetch_with_fallback([f"https://api.coingecko.com/api/v3/coins/{address}"])
|
|
if data:
|
|
result["coingecko"] = {"name": data.get("name"), "symbol": data.get("symbol")}
|
|
result["sources_used"].append("coingecko")
|
|
except Exception:
|
|
pass
|
|
|
|
# Compute risk score from available data
|
|
risk_score = 0
|
|
findings = []
|
|
|
|
if result.get("dexscreener"):
|
|
liq = result["dexscreener"].get("liquidity_usd", 0)
|
|
if liq < 1000:
|
|
risk_score += 25
|
|
findings.append(f"Very low liquidity: ${liq:,.0f}")
|
|
elif liq < 10000:
|
|
risk_score += 15
|
|
findings.append(f"Low liquidity: ${liq:,.0f}")
|
|
|
|
vol = result["dexscreener"].get("volume_24h", 0)
|
|
if liq > 0 and vol > liq * 10:
|
|
risk_score += 10
|
|
findings.append("Volume/liquidity ratio unusually high")
|
|
|
|
buyers = result["dexscreener"].get("buyers_24h", 0)
|
|
sellers = result["dexscreener"].get("sellers_24h", 0)
|
|
if sellers > 0 and buyers > 0:
|
|
ratio = sellers / buyers
|
|
if ratio > 3:
|
|
risk_score += 20
|
|
findings.append(f"Sell pressure {ratio:.1f}x - heavy dumping")
|
|
elif ratio > 1.5:
|
|
risk_score += 10
|
|
findings.append(f"Sell pressure {ratio:.1f}x")
|
|
|
|
if not result.get("account_exists") and not result.get("jupiter"):
|
|
risk_score += 30
|
|
findings.append("Token not found on Solana - possible scam address")
|
|
|
|
if len(result["sources_used"]) == 0:
|
|
risk_score += 20
|
|
findings.append("Unable to fetch data from any source - proceed with extreme caution")
|
|
|
|
risk_score = min(100, risk_score)
|
|
|
|
if risk_score >= 80:
|
|
level = "CRITICAL"
|
|
elif risk_score >= 60:
|
|
level = "HIGH"
|
|
elif risk_score >= 40:
|
|
level = "MEDIUM"
|
|
elif risk_score >= 20:
|
|
level = "LOW"
|
|
else:
|
|
level = "SAFE"
|
|
|
|
result["risk_score"] = risk_score
|
|
result["risk_level"] = level
|
|
result["findings"] = findings
|
|
result["source_count"] = len(result["sources_used"])
|
|
return result
|
|
|
|
|
|
async def _audit_evm(address: str, chain: str) -> dict:
|
|
"""Full audit for EVM tokens (Base, ETH, BSC)."""
|
|
result = {"chain": chain, "address": address, "sources_used": []}
|
|
|
|
# Layer 1: DexScreener
|
|
try:
|
|
data, _ = await fetch_with_fallback(
|
|
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
|
)
|
|
if data and data.get("pairs"):
|
|
pair = data["pairs"][0]
|
|
result["dexscreener"] = {
|
|
"price_usd": pair.get("priceUsd", 0),
|
|
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0),
|
|
"volume_24h": pair.get("volume", {}).get("h24", 0),
|
|
"price_change_24h": pair.get("priceChange", {}).get("h24", 0),
|
|
}
|
|
result["sources_used"].append("dexscreener")
|
|
except Exception:
|
|
pass
|
|
|
|
# Layer 2: Basescan / Etherscan
|
|
explorer_key = "BASESCAN_KEY" if chain == "base" else "ETHERSCAN_KEY"
|
|
explorer_api = (
|
|
"https://api.basescan.org/api" if chain == "base" else "https://api.etherscan.io/api"
|
|
)
|
|
api_key = os.getenv(explorer_key, "")
|
|
|
|
if api_key:
|
|
try:
|
|
data, _ = await fetch_with_fallback(
|
|
[
|
|
f"{explorer_api}?module=contract&action=getsourcecode&address={address}&apikey={api_key}"
|
|
]
|
|
)
|
|
if data and data.get("result") and data["result"][0].get("SourceCode"):
|
|
result["verified"] = True
|
|
result["sources_used"].append("explorer")
|
|
else:
|
|
result["verified"] = False
|
|
result["findings"].append("Contract not verified on explorer")
|
|
except Exception:
|
|
pass
|
|
|
|
# Layer 3: Chain RPC
|
|
try:
|
|
account = await rpc_call(chain, "eth_getCode", [address, "latest"])
|
|
if account and account != "0x":
|
|
result["is_contract"] = True
|
|
result["sources_used"].append(f"{chain}_rpc")
|
|
else:
|
|
result["is_contract"] = False
|
|
result["findings"].append("Address is not a contract")
|
|
except Exception:
|
|
pass
|
|
|
|
# Risk computation
|
|
risk_score = 0
|
|
findings = result.get("findings", [])
|
|
|
|
if result.get("dexscreener"):
|
|
liq = result["dexscreener"].get("liquidity_usd", 0)
|
|
if liq < 5000:
|
|
risk_score += 20
|
|
findings.append(f"Low liquidity: ${liq:,.0f}")
|
|
|
|
if not result.get("verified"):
|
|
risk_score += 15
|
|
findings.append("Contract source not verified")
|
|
|
|
if not result.get("is_contract"):
|
|
risk_score += 40
|
|
findings.append("Not a contract - likely EOA or invalid")
|
|
|
|
if len(result["sources_used"]) == 0:
|
|
risk_score += 25
|
|
findings.append("No data from any source")
|
|
|
|
risk_score = min(100, risk_score)
|
|
level = (
|
|
"CRITICAL"
|
|
if risk_score >= 80
|
|
else "HIGH"
|
|
if risk_score >= 60
|
|
else "MEDIUM"
|
|
if risk_score >= 40
|
|
else "LOW"
|
|
if risk_score >= 20
|
|
else "SAFE"
|
|
)
|
|
|
|
result["risk_score"] = risk_score
|
|
result["risk_level"] = level
|
|
result["findings"] = findings
|
|
return result
|
|
|
|
|
|
# ── Helper: Record x402 Payment ────────────────────────────────
|
|
# Pre-existing bug surface: this name was referenced (via # noqa: F821)
|
|
# in 77+ places across x402_tools.py but never defined. The legacy module
|
|
# silently failed at every endpoint call. We surface a no-op stub here
|
|
# so the audit-tracked fix (issue: fix(f821)) has a clear home.
|
|
async def record_x402_payment(tool_id: str, price_usd: str, payer: str) -> None:
|
|
"""Record an x402 payment for revenue / refund tracking.
|
|
|
|
Historical note: pre-P3A this was a NameError-bait. We now log a
|
|
debug message so receipts flow to logs even though the formal
|
|
revenue table is owned by app/billing/x402/enforcement.py.
|
|
|
|
Replace with `from app.domains.billing.x402.enforcement import record_x402_payment`
|
|
once the canonical recording path is wired.
|
|
"""
|
|
logger.debug(
|
|
"x402_payment_record tool=%s usd=%s payer=%s (P3A stub - route to enforcement)",
|
|
tool_id,
|
|
price_usd,
|
|
payer,
|
|
)
|
|
|
|
|
|
# ── Bundle Pricing (Phase 3A surface) ──────────────────────────
|
|
|
|
|
|
BUNDLES: dict[str, dict[str, Any]] = {
|
|
"security_pack": {
|
|
"name": "Security Pack",
|
|
"description": "Complete pre-trade security check - rug scan, contract audit, URL safety, and honeypot detection.",
|
|
"tools": ["rugshield", "audit", "urlcheck", "honeypot_check"],
|
|
"individual_total": 0.13, # 0.02 + 0.05 + 0.01 + 0.05
|
|
"bundle_price_usd": 0.10, # 23% discount
|
|
"bundle_price_atoms": "100000",
|
|
"category": "bundle",
|
|
"trial_free": 1,
|
|
},
|
|
"intelligence_pack": {
|
|
"name": "Intelligence Pack",
|
|
"description": "Whale tracking suite - decode wallets, follow smart money, detect clusters and insider patterns.",
|
|
"tools": ["whale", "smartmoney", "cluster", "insider"],
|
|
"individual_total": 0.35, # 0.15 + 0.05 + 0.05 + 0.10
|
|
"bundle_price_usd": 0.25, # 29% discount
|
|
"bundle_price_atoms": "250000",
|
|
"category": "bundle",
|
|
"trial_free": 1,
|
|
},
|
|
"all_in_one": {
|
|
"name": "All-in-One Audit",
|
|
"description": "Maximum intelligence - comprehensive audit + smart money alpha + meme vibe score in one call.",
|
|
"tools": ["comprehensive_audit", "smart_money_alpha", "meme_vibe_score"],
|
|
"individual_total": 0.50, # 0.15 + 0.25 + 0.10
|
|
"bundle_price_usd": 0.35, # 30% discount
|
|
"bundle_price_atoms": "350000",
|
|
"category": "bundle",
|
|
"trial_free": 1,
|
|
},
|
|
"forensic_pack": {
|
|
"name": "Forensic Investigation Pack",
|
|
"description": "Complete forensic analysis - valuation, OSINT identity hunt, and investigation report at 33% discount.",
|
|
"tools": ["forensic_valuation", "osint_identity_hunt", "investigation_report"],
|
|
"individual_total": 0.60, # 0.25 + 0.15 + 0.20
|
|
"bundle_price_usd": 0.40, # 33% discount
|
|
"bundle_price_atoms": "400000",
|
|
"category": "bundle",
|
|
"trial_free": 1,
|
|
},
|
|
}
|
|
|
|
|
|
# ── Tool aliases (catch-all dispatcher source) ─────────────────
|
|
|
|
|
|
TOOL_ALIASES: dict[str, str] = {
|
|
# Security
|
|
"airdrop_check": "airdrop_finder",
|
|
"bundler_detect": "mev_protection",
|
|
"clone_detect": "contract_diff",
|
|
"deployer_history": "insider",
|
|
"fresh_pair": "launch",
|
|
"liquidity_migration": "rugshield",
|
|
"mev_alert": "mev_protection",
|
|
"profile_flip": "social_signal",
|
|
"protocol_risk": "chain_health",
|
|
"scam_database": "urlcheck",
|
|
"token_age": "audit",
|
|
"wash_trading": "nft_wash_detector",
|
|
# Intelligence
|
|
"alpha_digest": "smart_money_alpha",
|
|
"insider_network": "insider",
|
|
"kol_performance": "social_signal",
|
|
"listing_predictor": "market_overview",
|
|
"sniper_detect": "sniper_alert",
|
|
"syndicate_scan": "cluster",
|
|
"syndicate_track": "cluster",
|
|
"whale_accumulation": "whale",
|
|
"whale_profile": "whale",
|
|
"whale_scan": "whale",
|
|
"wallet_graph": "cluster",
|
|
# Market
|
|
"arbitrage_scan": "market_overview",
|
|
"liquidity_depth": "market_overview",
|
|
"unlock_calendar": "market_overview",
|
|
# Social
|
|
"sentiment_spike": "sentiment",
|
|
# Analysis
|
|
"portfolio_aggregate": "portfolio_tracker",
|
|
"wallet_pnl": "wallet",
|
|
# Premium (mapped to closest real tool)
|
|
"forensic_valuation": "forensics",
|
|
"investigation_report": "forensics",
|
|
"osint_identity_hunt": "social_signal",
|
|
# Launchpad
|
|
# (fresh_pair already mapped to launch above)
|
|
}
|
|
|
|
# ── Expanded tool aliases (44 new specialized tools → real handlers) ──
|
|
try:
|
|
from app.routers._expanded_aliases import EXPANDED_ALIASES
|
|
|
|
TOOL_ALIASES.update(EXPANDED_ALIASES)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# ── Human Payment - Multi-Chain Wallet Pay-Per-Call ────────────
|
|
|
|
# Supported payment tokens across all chains
|
|
HUMAN_PAYMENT_TOKENS: dict[str, dict[str, Any]] = {
|
|
# Base chain
|
|
"USDC-BASE": {
|
|
"chain": "base",
|
|
"network": "eip155:8453",
|
|
"asset": "USDC",
|
|
"decimals": 6,
|
|
"label": "USDC on Base",
|
|
"icon": "💵",
|
|
},
|
|
# Solana chain
|
|
"USDC-SOL": {
|
|
"chain": "solana",
|
|
"network": "solana:mainnet",
|
|
"asset": "USDC",
|
|
"decimals": 6,
|
|
"label": "USDC on Solana",
|
|
"icon": "💵",
|
|
},
|
|
"SOL": {
|
|
"chain": "solana",
|
|
"network": "solana:mainnet",
|
|
"asset": "SOL",
|
|
"decimals": 9,
|
|
"label": "SOL native",
|
|
"icon": "◎",
|
|
},
|
|
# Ethereum chain
|
|
"USDC-ETH": {
|
|
"chain": "ethereum",
|
|
"network": "eip155:1",
|
|
"asset": "USDC",
|
|
"decimals": 6,
|
|
"label": "USDC on Ethereum",
|
|
"icon": "💵",
|
|
},
|
|
"USDT-ETH": {
|
|
"chain": "ethereum",
|
|
"network": "eip155:1",
|
|
"asset": "USDT",
|
|
"decimals": 6,
|
|
"label": "USDT on Ethereum",
|
|
"icon": "💲",
|
|
},
|
|
"ETH": {
|
|
"chain": "ethereum",
|
|
"network": "eip155:1",
|
|
"asset": "ETH",
|
|
"decimals": 18,
|
|
"label": "ETH native",
|
|
"icon": "⟠",
|
|
},
|
|
# BNB Chain
|
|
"USDC-BSC": {
|
|
"chain": "bsc",
|
|
"network": "eip155:56",
|
|
"asset": "USDC",
|
|
"decimals": 18,
|
|
"label": "USDC on BSC",
|
|
"icon": "💵",
|
|
},
|
|
"USDT-BSC": {
|
|
"chain": "bsc",
|
|
"network": "eip155:56",
|
|
"asset": "USDT",
|
|
"decimals": 18,
|
|
"label": "USDT on BSC",
|
|
"icon": "💲",
|
|
},
|
|
# Polygon
|
|
"USDC-POLY": {
|
|
"chain": "polygon",
|
|
"network": "eip155:137",
|
|
"asset": "USDC",
|
|
"decimals": 6,
|
|
"label": "USDC on Polygon",
|
|
"icon": "💵",
|
|
},
|
|
"POL": {
|
|
"chain": "polygon",
|
|
"network": "eip155:137",
|
|
"asset": "POL",
|
|
"decimals": 18,
|
|
"label": "POL native",
|
|
"icon": "🟣",
|
|
},
|
|
# Arbitrum
|
|
"USDC-ARB": {
|
|
"chain": "arbitrum",
|
|
"network": "eip155:42161",
|
|
"asset": "USDC",
|
|
"decimals": 6,
|
|
"label": "USDC on Arbitrum",
|
|
"icon": "💵",
|
|
},
|
|
# TRON
|
|
"USDT-TRON": {
|
|
"chain": "tron",
|
|
"network": "tron:mainnet",
|
|
"asset": "USDT",
|
|
"decimals": 6,
|
|
"label": "USDT on TRON",
|
|
"icon": "💲",
|
|
},
|
|
"USDC-TRON": {
|
|
"chain": "tron",
|
|
"network": "tron:mainnet",
|
|
"asset": "USDC",
|
|
"decimals": 6,
|
|
"label": "USDC on TRON",
|
|
"icon": "💵",
|
|
},
|
|
# Bitcoin
|
|
"BTC": {
|
|
"chain": "bitcoin",
|
|
"network": "bitcoin:mainnet",
|
|
"asset": "BTC",
|
|
"decimals": 8,
|
|
"label": "Bitcoin",
|
|
"icon": "₿",
|
|
},
|
|
# Fiat
|
|
"EUR-SEPA": {
|
|
"chain": "sepa",
|
|
"network": "sepa:eur",
|
|
"asset": "EUR",
|
|
"decimals": 2,
|
|
"label": "EUR via SEPA",
|
|
"icon": "€",
|
|
},
|
|
}
|
|
|
|
|
|
# Pay-to addresses - pulled dynamically from WalletManagerV2
|
|
# Falls back to env vars if wallet manager unavailable
|
|
def _resolve_pay_to(chain: str) -> str:
|
|
"""Get active x402 payment address from WalletManagerV2."""
|
|
try:
|
|
from app.wallet_manager_v2 import get_wallet_manager_v2
|
|
|
|
mgr = get_wallet_manager_v2(os.getenv("WALLET_VAULT_PASSWORD", ""))
|
|
# Map chain key to wallet manager chain key
|
|
chain_map = {
|
|
"base": "eth",
|
|
"ethereum": "eth",
|
|
"bsc": "eth",
|
|
"polygon": "eth",
|
|
"arbitrum": "eth",
|
|
"optimism": "eth",
|
|
"avalanche": "eth",
|
|
"fantom": "eth",
|
|
"gnosis": "eth",
|
|
"solana": "sol",
|
|
"tron": "trx",
|
|
"bitcoin": "btc",
|
|
}
|
|
wm_chain = chain_map.get(chain, chain)
|
|
for w in mgr._wallets.values():
|
|
if w.chain == wm_chain and w.x402_enabled and w.status == "active":
|
|
return w.address
|
|
except Exception:
|
|
pass
|
|
# Fallback: env vars
|
|
fallbacks = {
|
|
"base": "X402_EVM_PAY_TO",
|
|
"ethereum": "X402_EVM_PAY_TO",
|
|
"bsc": "X402_EVM_PAY_TO",
|
|
"polygon": "X402_EVM_PAY_TO",
|
|
"arbitrum": "X402_EVM_PAY_TO",
|
|
"solana": "X402_SOL_PAY_TO",
|
|
"tron": "X402_TRON_PAY_TO",
|
|
"bitcoin": "X402_BTC_PAY_TO",
|
|
"sepa": "ASTERPAY_SEPA_IBAN",
|
|
}
|
|
env_key = fallbacks.get(chain)
|
|
if env_key:
|
|
return os.getenv(env_key, "")
|
|
return ""
|
|
|
|
|
|
HUMAN_PAY_TO: dict[str, str] = {
|
|
"base": _resolve_pay_to("base"),
|
|
"ethereum": _resolve_pay_to("ethereum"),
|
|
"bsc": _resolve_pay_to("bsc"),
|
|
"polygon": _resolve_pay_to("polygon"),
|
|
"arbitrum": _resolve_pay_to("arbitrum"),
|
|
"solana": _resolve_pay_to("solana"),
|
|
"tron": _resolve_pay_to("tron"),
|
|
"bitcoin": _resolve_pay_to("bitcoin"),
|
|
"sepa": os.getenv("ASTERPAY_SEPA_IBAN", ""),
|
|
}
|
|
|
|
|
|
# ── Models specific to integration endpoints ──────────────────────
|
|
|
|
|
|
class BundleRequest(BaseModel):
|
|
address: str = ""
|
|
token: str = ""
|
|
wallet: str = ""
|
|
chain: str = "base"
|
|
url: str = ""
|
|
|
|
|
|
class MCPProxyRequest(BaseModel):
|
|
service: str
|
|
tool: str
|
|
arguments: dict[str, Any] = {}
|
|
|
|
|
|
class HumanPaymentRequest(BaseModel):
|
|
tool: str = Field(..., description="Tool ID to execute")
|
|
arguments: dict[str, Any] = Field(default_factory=dict, description="Tool parameters")
|
|
payment_token: str = Field(
|
|
..., description=f"Payment token key: {', '.join(HUMAN_PAYMENT_TOKENS.keys())}"
|
|
)
|
|
tx_hash: str = Field(..., description="Transaction hash on-chain")
|
|
wallet: str = Field(..., description="Payer wallet address")
|
|
chain: str | None = Field(
|
|
default=None, description="Blockchain (auto-detected from payment_token if not set)"
|
|
)
|
|
|
|
|
|
class HumanPaymentMethodsResponse(BaseModel):
|
|
"""Response listing all payment methods available to humans."""
|
|
|
|
tokens: list[dict[str, Any]]
|
|
pay_to_addresses: dict[str, str]
|
|
chain_count: int
|
|
token_count: int
|
|
|
|
|
|
class AuditRequest(BaseModel):
|
|
address: str
|
|
chain: str = "solana"
|
|
|
|
|
|
class SmartMoneyAlphaRequest(BaseModel):
|
|
wallet: str
|
|
chain: str = "solana"
|
|
|
|
|
|
class MemeVibeRequest(BaseModel):
|
|
token: str
|
|
chain: str = "solana"
|
|
|
|
|
|
class SentinelScanRequest(BaseModel):
|
|
"""Full 9-module SENTINEL deep scan."""
|
|
|
|
address: str
|
|
chain: str = "solana"
|
|
dev_address: str | None = None
|
|
|
|
|
|
class SentinelModuleRequest(BaseModel):
|
|
"""Single SENTINEL module request."""
|
|
|
|
address: str
|
|
chain: str = "solana"
|
|
dev_address: str | None = None
|
|
|
|
|
|
# Re-exported for tools that mirror legacy symbols
|
|
__all__ = [
|
|
# logger
|
|
"logger",
|
|
# sources
|
|
"FREE_RPCS",
|
|
"FREE_APIS",
|
|
# helpers
|
|
"fetch_with_fallback",
|
|
"rpc_call",
|
|
"_audit_solana",
|
|
"_audit_evm",
|
|
# pricing + aliases
|
|
"BUNDLES",
|
|
"TOOL_ALIASES",
|
|
# payment routing
|
|
"HUMAN_PAYMENT_TOKENS",
|
|
"HUMAN_PAY_TO",
|
|
"_resolve_pay_to",
|
|
# payment stub (P3A surface)
|
|
"record_x402_payment",
|
|
# request models
|
|
"TokenRequest",
|
|
"WalletRequest",
|
|
"SmartMoneyRequest",
|
|
"URLRequest",
|
|
"SentimentRequest",
|
|
"ClusterRequest",
|
|
"InsiderRequest",
|
|
"MultiTokenRequest",
|
|
"WalletListRequest",
|
|
"GenericRequest",
|
|
"BundleRequest",
|
|
"MCPProxyRequest",
|
|
"HumanPaymentRequest",
|
|
"HumanPaymentMethodsResponse",
|
|
"AuditRequest",
|
|
"SmartMoneyAlphaRequest",
|
|
"MemeVibeRequest",
|
|
"SentinelScanRequest",
|
|
"SentinelModuleRequest",
|
|
# upstream aliases from other libs (used by _check_rate_limit etc.)
|
|
"td",
|
|
"ClassVar",
|
|
]
|