| """ |
| AsterPay Facilitator — European x402 with EUR Off-Ramp |
| ======================================================== |
| European x402 Facilitator with EUR off-ramp via SEPA Instant. |
| MiCA compliant, ERC-8004 ready, ElizaOS plugin. |
| |
| First European-focused x402 infrastructure — allows users to |
| pay in EUR or crypto and receive EUR via SEPA. |
| """ |
|
|
| import logging |
| from typing import Any |
|
|
| import aiohttp |
|
|
| from app.facilitators.base import ( |
| Facilitator, |
| FacilitatorType, |
| NetworkSupport, |
| SettlementResult, |
| SettlementType, |
| VerificationResult, |
| ) |
| from app.facilitators.config import FacilitatorConfig, get_config |
|
|
| logger = logging.getLogger("facilitator.asterpay") |
|
|
|
|
| class AsterPayFacilitator(Facilitator): |
| """ |
| AsterPay — European x402 facilitator with SEPA EUR off-ramp. |
| MiCA compliant. Supports EUR and crypto payments. |
| """ |
|
|
| def __init__(self, config: FacilitatorConfig | None = None): |
| self._config = config or get_config() |
|
|
| @property |
| def name(self) -> str: |
| return "asterpay" |
|
|
| @property |
| def facilitator_type(self) -> FacilitatorType: |
| return FacilitatorType.HOSTED |
|
|
| @property |
| def settlement_type(self) -> SettlementType: |
| return SettlementType.OFF_RAMP |
|
|
| @property |
| def priority(self) -> int: |
| return 25 |
|
|
| @property |
| def verify_url(self) -> str | None: |
| return self._config.asterpay_verify_url |
|
|
| @property |
| def supported_networks(self) -> NetworkSupport: |
| return NetworkSupport( |
| chains=["ethereum", "base", "arbitrum", "sepa"], |
| chain_ids=[1, 8453, 42161], |
| networks=["eip155:1", "eip155:8453", "eip155:42161", "sepa:eur"], |
| tokens=["USDC", "EUR", "USDT"], |
| token_addresses={ |
| "ethereum:USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", |
| }, |
| ) |
|
|
| @property |
| def description(self) -> str: |
| return "AsterPay — European x402 with SEPA EUR off-ramp, MiCA compliant" |
|
|
| async def verify( |
| self, |
| payload: dict[str, Any], |
| requirements: dict[str, Any] | None = None, |
| ) -> VerificationResult: |
| if not self._config.asterpay_api_key: |
| return self._format_error("AsterPay API key not configured") |
|
|
| accepted = payload.get("accepted", {}) |
| body = { |
| "x402Version": payload.get("x402Version", 2), |
| "paymentPayload": payload, |
| } |
| if requirements: |
| body["paymentRequirements"] = requirements |
|
|
| headers = { |
| "Content-Type": "application/json", |
| "Authorization": f"Bearer {self._config.asterpay_api_key}", |
| "X-AsterPay-Version": "2026-05", |
| } |
|
|
| try: |
| timeout = aiohttp.ClientTimeout(total=self._config.facilitator_verify_timeout) |
| async with aiohttp.ClientSession(timeout=timeout) as session, session.post( |
| self._config.asterpay_verify_url, |
| json=body, |
| headers=headers, |
| ) as resp: |
| data = await resp.json() |
|
|
| if resp.status != 200: |
| return self._format_error(f"AsterPay: {data.get('error', f'HTTP {resp.status}')}") |
|
|
| if data.get("isValid") or data.get("verified"): |
| return self._format_success( |
| tx_hash=data.get("txHash"), |
| payer=data.get("payer"), |
| amount=accepted.get("amount"), |
| amount_usd=data.get("eurValue"), |
| chain="sepa" if data.get("isEur") else accepted.get("network", "").split(":")[-1], |
| token=data.get("currency", "USDC"), |
| settlement_id=data.get("settlementId"), |
| ) |
|
|
| return self._format_error(f"AsterPay: {data.get('invalidReason', 'Unknown')}") |
|
|
| except TimeoutError: |
| return self._format_error("AsterPay verification timed out") |
| except aiohttp.ClientError as e: |
| return self._format_error(f"AsterPay connection error: {e}") |
| except Exception as e: |
| logger.exception(f"AsterPay verify error: {e}") |
| return self._format_error(f"AsterPay internal error: {e}") |
|
|
| async def health(self) -> bool: |
| try: |
| timeout = aiohttp.ClientTimeout(total=10) |
| async with aiohttp.ClientSession(timeout=timeout) as session: |
| headers = {"Authorization": f"Bearer {self._config.asterpay_api_key}"} |
| async with session.get( |
| self._config.asterpay_verify_url.replace("/verify", "/health"), |
| headers=headers, |
| ) as resp: |
| return resp.status < 500 |
| except Exception as e: |
| logger.warning(f"AsterPay health check failed: {e}") |
| return False |
|
|
| async def settle(self, payment_data: dict[str, Any]) -> SettlementResult: |
| """Settle via AsterPay — may trigger SEPA off-ramp for EUR.""" |
| try: |
| body = {"paymentData": payment_data} |
| |
| if payment_data.get("currency") == "EUR" and self._config.asterpay_sepa_iban: |
| body["offramp"] = { |
| "method": "sepa_instant", |
| "iban": self._config.asterpay_sepa_iban, |
| } |
|
|
| timeout = aiohttp.ClientTimeout(total=30) |
| async with aiohttp.ClientSession(timeout=timeout) as session, session.post( |
| self._config.asterpay_settle_url, |
| json=body, |
| headers={ |
| "Content-Type": "application/json", |
| "Authorization": f"Bearer {self._config.asterpay_api_key}", |
| }, |
| ) as resp: |
| data = await resp.json() |
| if resp.status == 200: |
| return SettlementResult( |
| settled=True, |
| settlement_id=data.get("settlementId"), |
| tx_hash=data.get("txHash"), |
| chain=payment_data.get("chain"), |
| facilitator=self.name, |
| reason=f"Settled via AsterPay{' (SEPA EUR off-ramp)' if body.get('offramp') else ''}", |
| extra=data, |
| ) |
| return SettlementResult( |
| settled=False, |
| reason=f"AsterPay settle failed: {data.get('error', 'HTTP ' + str(resp.status))}", |
| facilitator=self.name, |
| ) |
| except Exception as e: |
| return SettlementResult(settled=False, reason=f"AsterPay settlement error: {e}", facilitator=self.name) |
|
|