"""SIBIDZ x402 adapter for an authenticated MCP host (Python 3.10+). Install py-algorand-sdk==2.12.0; its required PyNaCl dependency verifies signatures. Callbacks are synchronous; paid transport calls run on a bounded daemon thread. The host honors timeout/cancellation and owns OAuth/session transport, explicit wallet approval and protected checkpoint storage. This module reads no key, broadcasts nothing and makes no network request on import or execution. See examples/README.md. """ import base64 import json import re import threading import time from datetime import datetime from urllib.parse import urlsplit from algosdk import constants, encoding, transaction from nacl.signing import VerifyKey def _uint(value, label, positive=False): if (not isinstance(value, str) or not re.fullmatch(r"0|[1-9][0-9]{0,19}", value) or int(value) > 2**64 - 1 or (positive and int(value) == 0)): raise ValueError("Invalid " + label) return int(value) def _json(value): return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False) def _clone(value): encoded = _json(value) if len(encoded.encode("utf-8")) > 65536: raise ValueError("Payment data exceeds the supported bound") return json.loads(encoded) def _endpoint(value): url = urlsplit(value) if (url.scheme != "https" or not url.hostname or url.username or url.password or url.query or url.fragment or url.path != "/mcp"): raise ValueError("Use the exact HTTPS SIBIDZ /mcp endpoint") return value def _base64(value, label): if not isinstance(value, str): raise ValueError("Invalid " + label) try: decoded = base64.b64decode(value, validate=True) except (ValueError, TypeError) as error: raise ValueError("Invalid " + label) from error if base64.b64encode(decoded).decode() != value: raise ValueError("Invalid " + label) return decoded def _requirements(required, policy, endpoint): if (required.get("x402Version") != 2 or not isinstance(required.get("accepts"), list) or len(required["accepts"]) != 1 or required.get("resource", {}).get("url") != endpoint): raise ValueError("Unsupported x402 challenge or resource") accepted = _clone(required["accepts"][0]) extra = accepted.get("extra", {}) if (accepted.get("scheme") not in ("exact", "sibidz-exact-native") or not encoding.is_valid_address(accepted.get("payTo", "")) or accepted.get("maxTimeoutSeconds") != 300 or not extra): raise ValueError("Unsupported payment requirements") asset = _uint(accepted.get("asset"), "asset") amount = _uint(accepted.get("amount"), "amount", True) if (asset == 0) != (accepted["scheme"] == "sibidz-exact-native"): raise ValueError("Native ALGO requires the separate sibidz-exact-native adapter") if extra.get("policy_version") != "2026-10-31.x402-deposit-v1": raise ValueError("Unsupported deposit policy version") intent = extra.get("intent_id", "") if not re.fullmatch(r"[a-fA-F0-9-]{36}", intent) or extra.get("payment_reference") != "sibidz:x402:deposit:" + intent: raise ValueError("Invalid deposit reference") genesis = _base64(extra.get("genesis_hash"), "genesis hash") network = "algorand:" + base64.urlsafe_b64encode(genesis).decode().rstrip("=")[:32] if (len(genesis) != 32 or accepted.get("network") != network or not isinstance(extra.get("genesis_id"), str) or not extra["genesis_id"]): raise ValueError("Invalid Algorand network") first = _uint(extra.get("first_valid"), "first round") last = _uint(extra.get("last_valid"), "last round") if last < first or last - first > 100 or extra.get("max_fee_microalgo") != "10000": raise ValueError("Unsupported validity or fee bounds") permitted = next((item for item in policy.get("allowedAssets", []) if all( item.get(key) == accepted[key] for key in ("network", "asset", "scheme"))), None) if permitted is None or amount > _uint(permitted.get("maxAmount"), "authorised asset limit", True): raise ValueError("Payment exceeds the explicit asset/network spending policy") if "payTo" in policy and policy["payTo"] != accepted["payTo"]: raise ValueError("Receiving address differs from the authorised address") fee = _uint(policy.get("feeMicroalgo"), "network fee", True) maximum_fee = _uint(policy.get("maxFeeMicroalgo"), "authorised fee limit", True) if fee < 1000 or fee > maximum_fee or fee > 10000: raise ValueError("Network fee exceeds the authorised or supported limit") note = policy.get("note", extra["payment_reference"]) if note is not None and (not isinstance(note, str) or len(note.encode("utf-8")) > 1024): raise ValueError("Optional note exceeds the 1024-byte bound") return accepted, asset, amount, fee, first, last, note def _transaction(parsed, sender): accepted, asset, amount, fee, first, last, note = parsed extra = accepted["extra"] params = transaction.SuggestedParams(fee=fee, flat_fee=True, first=first, last=last, gh=extra["genesis_hash"], gen=extra["genesis_id"]) common = dict(sender=sender, receiver=accepted["payTo"], amt=amount, sp=params, note=None if note is None else note.encode("utf-8")) return transaction.PaymentTxn(**common) if asset == 0 else transaction.AssetTransferTxn(**common, index=asset) def _verify_signed(signed, expected, sender): if not isinstance(signed, bytes) or len(signed) > 12288: raise ValueError("The wallet must return bounded signed transaction bytes") decoded = encoding.msgpack_decode(base64.b64encode(signed).decode()) if (type(decoded) is not transaction.SignedTransaction or decoded.authorizing_address or not decoded.signature): raise ValueError("Only the approved sender's ordinary Ed25519 signature is supported") if (base64.b64decode(encoding.msgpack_encode(decoded)) != signed or encoding.msgpack_encode(decoded.transaction) != encoding.msgpack_encode(expected)): raise ValueError("The wallet changed the approved transaction") signature = _base64(decoded.signature, "signature") if len(signature) != 64: raise ValueError("The wallet signature is invalid") # Transaction signatures use TX, not the MX prefix of SDK verify_bytes(). VerifyKey(encoding.decode_address(sender)).verify( constants.txid_prefix + base64.b64decode(encoding.msgpack_encode(expected)), signature) def prepare_signed_payment(*, payment_required, endpoint, sender, policy, approve_payment, sign_transaction): """Sign only after exact approval; sign_transaction must never broadcast.""" endpoint = _endpoint(endpoint) if not encoding.is_valid_address(sender) or not callable(approve_payment) or not callable(sign_transaction): raise ValueError("A sender, payment approval and sign-only wallet callback are required") parsed = _requirements(payment_required, policy, endpoint) accepted, _, _, fee, _, _, note = parsed estimate = payment_required.get("extensions", {}).get("sibidz", {}) if approve_payment({"requirements": _clone(accepted), "sender": sender, "networkFeeMicroalgo": str(fee), "note": note, "creditsTarget": estimate.get("credits_target"), "creditEstimate": _clone(estimate.get("credit_estimate")), "amountPolicy": _clone(estimate.get("amount_policy")), "crediting": "Target credits are an estimate; actual deposits are valued after confirmation."}) is not True: raise ValueError("Payment was not approved") txn = _transaction(parsed, sender) signed = sign_transaction(base64.b64decode(encoding.msgpack_encode(txn)), {"transactionId": txn.get_txid(), "sender": sender}) _verify_signed(signed, txn, sender) return {"transactionId": txn.get_txid(), "payment": {"x402Version": 2, "resource": _clone(payment_required["resource"]), "accepted": accepted, "payload": {"paymentIndex": 0, "paymentGroup": [base64.b64encode(signed).decode()]}}} def _tool_outcome(result): if "structuredContent" in result: return result["structuredContent"] content = result.get("content", []) return json.loads(content[0]["text"]) if content and isinstance(content[0].get("text"), str) else {} def _challenge(result): value = result.get("structuredContent", {}) if result.get("isError") is not True or value.get("x402Version") != 2 or not isinstance(value.get("accepts"), list): return None content = result.get("content", []) text = content[0].get("text") if content else None if not isinstance(text, str) or len(text.encode("utf-8")) > 65536 or _json(json.loads(text)) != _json(value): raise ValueError("Payment challenge content and structuredContent disagree") return value def _recovery_policy(value): if value is False: return {"maxAttempts": 1, "maxWaitMs": 60000, "minDelayMs": 1000} if value is not None and not isinstance(value, dict): raise ValueError("Invalid recovery bounds") policy = {"maxAttempts": 4, "maxWaitMs": 60000, "minDelayMs": 1000, **(value or {})} for key, minimum, maximum in (("maxAttempts", 1, 8), ("maxWaitMs", 1, 120000), ("minDelayMs", 1000, 30000)): if type(policy[key]) is not int or not minimum <= policy[key] <= maximum: raise ValueError("Invalid recovery " + key) return policy class _RecoveryStopped(Exception): def __init__(self, reason): super().__init__("Payment recovery " + reason) self.reason = reason def _bounded_call(callback, params, deadline, cancel_event, legacy=False): """One active host call. A timeout cancels cooperatively and ends recovery. The host must honor timeout_seconds/cancel_event. A callback that ignores cancellation can outlive this invocation, so this helper never retries it. It runs in a daemon thread and cannot prevent interpreter shutdown. """ remaining = deadline - time.monotonic() if remaining <= 0: raise _RecoveryStopped("deadline") finished, transport_cancel = threading.Event(), threading.Event() outcome = [] def run(): try: options = {"timeout_seconds": remaining, "cancel_event": transport_cancel} outcome.append((True, callback(params) if legacy else callback(params, options))) except BaseException as error: outcome.append((False, error)) finally: finished.set() if cancel_event is not None and cancel_event.is_set(): raise _RecoveryStopped("cancelled") threading.Thread(target=run, name="sibidz-payment-recovery", daemon=True).start() while not finished.wait(min(0.05, max(0, deadline - time.monotonic()))): if cancel_event is not None and cancel_event.is_set(): transport_cancel.set() raise _RecoveryStopped("cancelled") if time.monotonic() >= deadline: transport_cancel.set() raise _RecoveryStopped("deadline") if not outcome[0][0]: raise outcome[0][1] return outcome[0][1] def _retry_at_ms(value): if not isinstance(value, str): return 0 try: return max(0, (datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() - time.time()) * 1000) except ValueError: return 0 def _recovery_status(result, saved): metadata = result.get("_meta", {}) settlement = metadata.get("x402/payment-response", {}) network = saved["payment"]["accepted"]["network"] if settlement.get("success") is True: if settlement.get("transaction") != saved["transactionId"] or settlement.get("network") != network: raise ValueError("Settlement response identifies a different payment") return {"confirmed": True} status = metadata.get("sibidz/payment-status") if status: recovery = status.get("recovery", {}) delay = recovery.get("retry_after_ms") if (status.get("state") != "pending" or status.get("nonterminal") is not True or status.get("operation_id") != saved["payment"]["accepted"]["extra"]["intent_id"] or status.get("transaction") != saved["transactionId"] or status.get("network") != network or recovery.get("tool") != "credits" or recovery.get("same_arguments") is not True or recovery.get("same_payment") is not True or recovery.get("new_payment_authorized") is not False or type(delay) not in (int, float) or not 0 <= delay < float("inf")): raise ValueError("Pending recovery identifies different or unsupported payment instructions") if result.get("isError") is True: return {"stop": "rejected"} retry_at = recovery.get("retry_at", status.get("retry_at", _tool_outcome(result).get("data", {}).get("retry_at"))) return {"delayMs": max(delay, _retry_at_ms(retry_at))} if settlement.get("success") is False and settlement.get("errorReason") in ("settlement_unknown", "settlement_not_submitted"): if settlement.get("network") != network or settlement.get("transaction") and settlement["transaction"] != saved["transactionId"]: raise ValueError("Pending response identifies a different payment") return {"delayMs": _retry_at_ms(_tool_outcome(result).get("data", {}).get("retry_at"))} return {"stop": "rejected" if result.get("isError") is True else "status_unavailable"} def execute_topup(*, endpoint, call_tool, args, sender, policy, approve_payment, sign_transaction, save_checkpoint, checkpoint=None, recovery=None, cancel_event=None): """One payment with bounded automatic recovery using identical signed bytes. call_tool(params, options) returns a plain MCP result dict; options contains timeout_seconds and cancel_event. Paid calls run on one daemon thread at a time. The host must honor transport cancellation and must be thread-safe. recovery=False uses a one-argument callback and makes one paid attempt. save_checkpoint must commit protected durable storage before returning. Retain identical arguments, policy and checkpoint after every uncertain result. """ bounds = _recovery_policy(recovery) if cancel_event is not None and cancel_event.is_set(): raise _RecoveryStopped("cancelled") endpoint = _endpoint(endpoint) if not callable(call_tool) or not callable(save_checkpoint): raise ValueError("MCP call_tool and durable save_checkpoint callbacks are required") if (args.get("action") != "x402" or args.get("route") not in ("algo", "usdc", "sibidz") or not re.fullmatch(r"[a-fA-F0-9-]{36}", args.get("request_id", "")) or _uint(args.get("credits"), "target credits", True) > 32000): raise ValueError("Use credits action=x402 with route, request_id and target whole credits") _uint(args.get("max_fee"), "explicit max_fee service-fee ceiling") args = _clone(args) saved = _clone(checkpoint) if checkpoint is not None else None if saved is not None: if (saved.get("version") != 1 or saved.get("endpoint") != endpoint or saved.get("arguments") != args or not saved.get("payment") or not saved.get("transactionId")): raise ValueError("Checkpoint belongs to different payment instructions") payment = saved["payment"] payload = payment.get("payload", {}) group = payload.get("paymentGroup") if payment.get("x402Version") != 2 or payload.get("paymentIndex") != 0 or not isinstance(group, list) or len(group) != 1: raise ValueError("Invalid payment checkpoint") parsed = _requirements({"x402Version": 2, "resource": payment.get("resource"), "accepts": [payment.get("accepted")]}, policy, endpoint) txn = _transaction(parsed, sender) _verify_signed(_base64(group[0], "checkpoint encoding"), txn, sender) if txn.get_txid() != saved["transactionId"]: raise ValueError("Checkpoint transaction identity changed") else: params = {"name": "credits", "arguments": _clone(args)} result = call_tool(params) if recovery is False else call_tool(params, {"timeout_seconds": 60, "cancel_event": cancel_event}) required = _challenge(result) if required is None: resume = _tool_outcome(result).get("data", {}).get("resume") if resume is not None and (resume.get("tool") != "credits" or resume.get("arguments") != args): raise ValueError("Preparation resume changed the authorised request") return {"result": result, "checkpoint": None, **({"state": "preparing", "resume": _clone(resume)} if resume else {})} if cancel_event is not None and cancel_event.is_set(): raise _RecoveryStopped("cancelled") signed = prepare_signed_payment(payment_required=required, endpoint=endpoint, sender=sender, policy=policy, approve_payment=approve_payment, sign_transaction=sign_transaction) saved = {"version": 1, "endpoint": endpoint, "arguments": args, **signed} save_checkpoint(_clone(saved)) deadline, attempts, result, error = time.monotonic() + bounds["maxWaitMs"] / 1000, 0, None, None def finish(reason, confirmed=False): return {"state": "payment_confirmed" if confirmed else "checking_payment", "checkpoint": saved, **({"result": result} if result is not None else {}), **({"error": error} if error is not None else {}), "recovery": {"attempts": attempts, "stopped": reason}} while True: if cancel_event is not None and cancel_event.is_set(): return finish("cancelled") if time.monotonic() >= deadline: return finish("deadline") attempts += 1 result, error = None, None try: params = {"name": "credits", "arguments": _clone(args), "_meta": {"x402/payment": _clone(saved["payment"])}} result = _bounded_call(call_tool, params, deadline, cancel_event, legacy=recovery is False) except _RecoveryStopped as caught: error = caught return finish(caught.reason) except Exception as caught: error = caught status = {"delayMs": 0} if result is not None: try: if _challenge(result): return finish("rejected") status = _recovery_status(result, saved) except Exception as caught: error = caught return finish("response_mismatch") if status.get("confirmed"): return finish("confirmed", True) if status.get("stop"): return finish(status["stop"]) if attempts >= bounds["maxAttempts"]: return finish("attempt_limit") delay = max(bounds["minDelayMs"], status["delayMs"]) / 1000 if delay >= deadline - time.monotonic(): return finish("retry_after_budget") # Cancellation interrupts the wait; no callback is active at this point. if (cancel_event or threading.Event()).wait(delay): return finish("cancelled") if __name__ == "__main__": print(__doc__)