""" MoneyTools x402 payment guard — copy-paste snippet (not a PyPI package). Plug Pay Precheck + Settle spend caps into any x402 client as a before_pay hook. wrap_requests / wrap_httpx sit *inside* your payment wrapper so a 402 is gated before anything is signed. session = wrap_requests(requests.Session(), {"payer": payer, "max_usd": 0.05}) # then attach your x402 payment adapter to this session This module never loads a key, never signs, and never sends a payment. Set a User-Agent: default Python-urllib is blocked on some workers.dev hosts. Live free tiers (UTC day) and prices, Nov 2026: Pay Precheck /v1/precheck — HTTP 5/IP/endpoint/day, then 0.005 USDC Settle /v1/spend/check — trial 50 calls/7 days (1/client/day), then 0.005 USDC Approval Relay /v1/approvals — 3/day, then 0.005 USDC Download: https://moneytools.dev/products/payment-safety/examples/x402-guard.py """ from __future__ import annotations import json import urllib.error import urllib.parse import urllib.request from typing import Any, Callable, Mapping, Optional from urllib.parse import urlparse __version__ = "0.1.0" DEFAULTS = { "precheck_url": "https://api.moneytools.dev/v1/precheck", "spend_check_url": "https://settle.moneytools.dev/v1/spend/check", "relay_url": "https://relay.moneytools.dev/v1/approvals", "user_agent": f"moneytools-x402-guard/{__version__}", } PAYMENT_HDRS = ("payment-signature", "payment-signature-header", "x-payment") class PaymentBlockedError(Exception): def __init__(self, result: dict): super().__init__(result.get("reason") or "payment blocked") self.result = result class MoneyToolsApiError(Exception): def __init__(self, status: int, message: str, body: Any = None): super().__init__(message) self.status = status self.body = body def has_payment_header(headers: Optional[Mapping] = None) -> bool: if not headers: return False lower = {str(k).lower(): v for k, v in headers.items()} for name in PAYMENT_HDRS: v = lower.get(name) if isinstance(v, str) and v.strip(): return True return False def host_of(url: str) -> Optional[str]: try: return urlparse(url).hostname.lower() if urlparse(url).hostname else None except Exception: return None def pick_amount_usd(pre: Any, fallback: Any = None) -> Optional[str]: options = (pre or {}).get("options") or [] rec_idx = (pre or {}).get("recommendedOption") rec = options[rec_idx] if isinstance(rec_idx, int) and 0 <= rec_idx < len(options) else None valid = [o for o in options if o and o.get("valid") and not (o.get("sanctions") or {}).get("hit")] hit = rec if rec and rec.get("valid") and not (rec.get("sanctions") or {}).get("hit") else (valid[0] if valid else None) raw = (hit or {}).get("amountUsd", fallback) if hit is not None else fallback if raw is None or raw == "": return None return str(raw) def _header_get(headers: Optional[Mapping], name: str) -> Optional[str]: if not headers: return None if hasattr(headers, "get"): for key in (name, name.upper(), name.title()): v = headers.get(key) if v: return v if isinstance(v, str) else str(v) try: for k, v in headers.items(): if str(k).lower() == name.lower() and v: return v if isinstance(v, str) else str(v) except Exception: return None return None def _mt_call(cfg: dict, url: str, method: str = "GET", body: Any = None) -> Any: opener: Callable = cfg.get("mt_opener") or _default_opener headers = { "User-Agent": cfg.get("user_agent") or DEFAULTS["user_agent"], "Accept": "application/json", } if cfg.get("api_key"): headers["X-API-Key"] = cfg["api_key"] extra = cfg.get("headers") or {} headers.update(extra) data = None if body is not None: data = json.dumps(body).encode() headers["Content-Type"] = "application/json" status, text = opener(url, method=method, headers=headers, data=data) parsed: Any = text try: parsed = json.loads(text) if text else None except json.JSONDecodeError: parsed = text if status in (401, 402): raise MoneyToolsApiError( status, f"MoneyTools API {url} returned HTTP {status} (free tier exhausted or key required; pass api_key or wait for the daily reset). This guard does not pay MoneyTools automatically.", parsed, ) if status < 200 or status >= 300: raise MoneyToolsApiError(status, f"MoneyTools API {url} returned HTTP {status}", parsed) return parsed def _default_opener(url: str, method: str = "GET", headers: Optional[dict] = None, data: Optional[bytes] = None): req = urllib.request.Request(url, data=data, headers=headers or {}, method=method) try: with urllib.request.urlopen(req, timeout=20) as r: return r.status, r.read().decode() except urllib.error.HTTPError as e: return e.code, e.read().decode() if e.fp else "" def _should_escalate(cfg: dict, inp: dict, cap_denied: bool) -> bool: mode = cfg.get("escalate", "off") if not mode or mode in (False, "off"): return False if mode == "always": return True if mode == "deny" and cap_denied: return True if mode == "high" and (inp.get("risk") or cfg.get("risk")) == "high": return True return False def _maybe_relay(cfg: dict, inp: dict, reason: str) -> dict: amount = inp.get("amount_usd") if inp.get("amount_usd") is not None else "?" host = inp.get("host") or host_of(inp.get("seller_url") or "") or "unknown host" body = { "action_summary": f"Pay {amount} USDC to {host} at {inp.get('seller_url')}"[:500], "risk": inp.get("risk") or cfg.get("risk") or "med", "expires_in_s": 3600, } try: appr = _mt_call(cfg, cfg.get("relay_url") or DEFAULTS["relay_url"], method="POST", body=body) return { "proceed": False, "step": "approval_relay", "reason": reason, "approval": { "approval_id": appr.get("approval_id") if isinstance(appr, dict) else None, "approve_url": appr.get("approve_url") if isinstance(appr, dict) else None, "poll_url": appr.get("poll_url") if isinstance(appr, dict) else None, "status": appr.get("status") if isinstance(appr, dict) else None, }, } except Exception as err: return { "proceed": False, "step": "approval_relay", "reason": f"{reason} (Approval Relay failed: {err})", } def before_pay(inp: dict, cfg: Optional[dict] = None) -> dict: cfg = dict(cfg or {}) host = inp.get("host") or host_of(inp.get("seller_url") or "") do_pre = cfg.get("precheck", True) do_cap = cfg.get("spend_check", True) pre = None if do_pre: try: q = urllib.parse.urlencode({ "url": inp["seller_url"], "method": (inp.get("method") or "GET").upper(), }) pre = _mt_call(cfg, f"{cfg.get('precheck_url') or DEFAULTS['precheck_url']}?{q}") except MoneyToolsApiError as err: return {"proceed": False, "step": "upstream", "reason": str(err)} except Exception as err: if cfg.get("on_precheck_error") == "allow": pre = None else: return {"proceed": False, "step": "precheck", "reason": f"precheck failed (fail closed): {err}"} if pre: verdict = pre.get("verdict") if verdict in ("block", "not_402") or pre.get("ok") is False: return {"proceed": False, "step": "precheck", "reason": pre.get("summary") or f"precheck verdict={verdict}", "pre": pre} if verdict == "warn" and not cfg.get("allow_warn"): return {"proceed": False, "step": "precheck", "reason": pre.get("summary") or "precheck verdict=warn (set allow_warn to proceed)", "pre": pre} amount_usd = pick_amount_usd(pre, inp.get("amount_usd")) pay_to = inp.get("pay_to") if pay_to is None and isinstance(pre, dict): for o in pre.get("options") or []: if o and o.get("valid") and not (o.get("sanctions") or {}).get("hit"): pay_to = o.get("payTo") break scoped = {**inp, "amount_usd": amount_usd, "host": host, "pay_to": pay_to} max_usd = cfg.get("max_usd") if max_usd is not None and amount_usd is not None and float(amount_usd) > float(max_usd): blocked = { "proceed": False, "step": "budget", "reason": f"amount {amount_usd} USDC exceeds local max_usd {max_usd}", "pre": pre, } if _should_escalate(cfg, scoped, False): return _maybe_relay(cfg, scoped, blocked["reason"]) return blocked cap = None if do_cap: if not cfg.get("payer"): return {"proceed": False, "step": "spend_check", "reason": "spend_check is on but cfg.payer is missing", "pre": pre} cap = _mt_call(cfg, cfg.get("spend_check_url") or DEFAULTS["spend_check_url"], method="POST", body={ "payer": cfg["payer"], "paymentRequired": inp.get("payment_required"), "amountUsd": amount_usd, "payTo": pay_to, "host": host, "network": inp.get("network"), "sessionId": cfg.get("session_id"), "reserve": bool(cfg.get("reserve")), }) if isinstance(cap, dict) and cap.get("allow") is False: reasons = cap.get("reasons") or [] first = reasons[0] if reasons else None if isinstance(first, dict): reason = first.get("message") or first.get("code") or "spend cap denied" else: reason = str(first or "spend cap denied") if _should_escalate(cfg, scoped, True): relayed = _maybe_relay(cfg, scoped, reason) relayed["pre"] = pre relayed["cap"] = cap return relayed return {"proceed": False, "step": "spend_check", "reason": reason, "pre": pre, "cap": cap} if _should_escalate(cfg, scoped, False): relayed = _maybe_relay(cfg, scoped, "escalated to Approval Relay") relayed["pre"] = pre relayed["cap"] = cap return relayed return {"proceed": True, "step": "ok", "amount_usd": amount_usd, "host": host, "pay_to": pay_to, "pre": pre, "cap": cap} def assert_allowed(inp: dict, cfg: Optional[dict] = None) -> dict: result = before_pay(inp, cfg) if not result.get("proceed"): raise PaymentBlockedError(result) return result def wrap_requests(session: Any, cfg: Optional[dict] = None) -> Any: """Wrap a requests.Session.send so 402s are gated before a payment adapter retries.""" cfg = dict(cfg or {}) original = session.send def send(request, *args, **kwargs): headers = getattr(request, "headers", None) if has_payment_header(headers): return original(request, *args, **kwargs) resp = original(request, *args, **kwargs) if getattr(resp, "status_code", None) == 402: pr = _header_get(getattr(resp, "headers", None), "payment-required") or getattr(resp, "text", "") url = getattr(request, "url", None) or getattr(resp, "url", "") assert_allowed({ "seller_url": url, "method": getattr(request, "method", None) or "GET", "payment_required": pr, "host": host_of(url or ""), }, cfg) return resp session.send = send return session def wrap_httpx(client: Any, cfg: Optional[dict] = None) -> Any: """Wrap an httpx.Client.send so 402s are gated before a payment adapter retries.""" cfg = dict(cfg or {}) original = client.send def send(request, *args, **kwargs): headers = getattr(request, "headers", None) if has_payment_header(headers): return original(request, *args, **kwargs) resp = original(request, *args, **kwargs) if getattr(resp, "status_code", None) == 402: pr = _header_get(getattr(resp, "headers", None), "payment-required") or getattr(resp, "text", "") url = str(getattr(request, "url", "") or "") assert_allowed({ "seller_url": url, "method": getattr(request, "method", None) or "GET", "payment_required": pr, "host": host_of(url), }, cfg) return resp client.send = send return client