"""Complete buyer for the SECONDED standard x402 door, with recovery built in. Requires Python >= 3.10. The default cap is $0.25 (Small); use --max-price explicitly to allow a larger check. pip install "x402[evm,httpx]==2.24.0" rfc8785 cryptography Put recover.py (from this directory) next to this file. Fund an ordinary (EOA) wallet and export its private key as BUYER_PRIVATE_KEY — use a small, dedicated wallet, never your main one. Example: python buy.py --network eip155:8453 \ --product scam_check \ --input '{"message": "Urgent: send funds to unlock your account."}' \ --record private/purchase-001.json The flow: one unpaid POST fetches the 402 terms (free); the payee, network and price are checked against your limits; the SDK's per-payment spend cap is set to EXACTLY the quoted price; recover.py's hooks write the private durable seconded-door-recovery/v2 record and purchase association before the paid send; then the payment wrapper is dropped and a plain HTTP client polls the same credential until a verified answer or a verified terminal no-charge receipt (state service_failed etc., charged "no"). It never signs twice for one purchase. Public 402 terms have no purchase IDs or salt; recover.py verifies the signed association against the original authorization, accepted terms and canonical request, plus billing. Exit codes: 0 verified answer, 2 verified terminal no-charge non-answer (record archived; nothing was charged), 1 anything else (record retained). """ from __future__ import annotations import argparse import asyncio from decimal import Decimal import importlib.util import json import os from pathlib import Path import sys from urllib.parse import urlsplit import httpx DEFAULT_URL = 'https://api.secondedoracle.xyz/v1/x402/checks' DEFAULT_PAY_TO = '0x010ab46D566cDe25Cca0ee55eb105e781C7Bcf3a' def load_recover(path): spec = importlib.util.spec_from_file_location('recover', path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def api_origin(url): parts = urlsplit(url) return f'{parts.scheme}://{parts.netloc}' def preflight(url, body, pay_to, max_price_usd): """Free unpaid POST: read the 402 terms, pin the payee, cap the price. Nothing is signed here. The paid request later gets its own fresh 402 at the same tier price; the spend caps set from this quote enforce that any different price is refused instead of paid. """ reply = httpx.post(url, json=body, follow_redirects=False, timeout=35) if reply.status_code != 402: raise SystemExit(f'expected 402 terms, got HTTP {reply.status_code}: {reply.text[:300]}') accepts = [a for a in reply.json()['accepts'] if a['network'] == body['options']['network']] if not accepts: raise SystemExit('no offer for the requested network; check options.network and funding') terms = accepts[0] if terms['payTo'].lower() != pay_to.lower(): raise SystemExit(f'payee mismatch: expected {pay_to}, offered {terms["payTo"]} — stop') if terms['scheme'] != 'exact': raise SystemExit(f'unsupported scheme {terms["scheme"]}') price_usd = Decimal(terms['amount']) / 1_000_000 # USDC and USDG use 6 decimals if price_usd > Decimal(max_price_usd): raise SystemExit(f'quoted ${price_usd} exceeds your --max-price ${max_price_usd}; nothing paid') return terms, price_usd async def purchase(url, body, terms, price_usd, record_path, recover_module): from eth_account import Account from x402 import x402Client from x402.mechanisms.evm.exact import ExactEvmScheme from x402.mechanisms.evm.signers import EthAccountSigner from x402.http.clients.httpx import x402AsyncTransport key = os.environ.get('BUYER_PRIVATE_KEY') if not key: raise SystemExit('set BUYER_PRIVATE_KEY (a dedicated wallet with a few dollars, never your main key)') signer = EthAccountSigner(Account.from_key(key)) client = x402Client() client.register(terms['network'], ExactEvmScheme(signer=signer)) # Per-purchase cap equal to the quoted price, at both levels: the dollar # cap and an integer-atomic cap on exactly the quoted asset and network. # For a Small quote this is $0.25 and 250000 atomic USDC/USDG. client.set_spend_controls({ 'max_amount_per_payment': f'${price_usd:.2f}', 'allowed_assets': [{'network': terms['network'], 'asset': terms['asset'], 'max_amount_per_payment': terms['amount']}], }) recover_module.install_hooks(client, record_path, url, body) async with httpx.AsyncClient(transport=x402AsyncTransport(client), timeout=60) as session: reply = await session.post(url, json=body) return reply.status_code def interpret(body, products_url): """Print the verdict. Act only on the answer label via answer_to_action.""" envelope = body['receipt']['envelope'] answer = envelope['answer'] print(f'answer: label_id={answer["label_id"]} option={answer.get("option")}') for item in (envelope.get('verification') or {}).get('coverage') or []: print(f'coverage: {item["fact"]}: {item["status"]}' + (f' ({item["reason"]})' if item.get('reason') else '')) print('a part marked not_checked was not verified, even next to an agreed answer') try: products = httpx.get(products_url, timeout=10).json() actions = {p['product']: p.get('answer_to_action', {}) for p in products.get('products', [])} action = actions.get(body['receipt']['envelope'].get('product'), {}).get(answer['label_id']) print(f'recommended action: {action}' if action else f'look up label_id {answer["label_id"]!r} in answer_to_action at {products_url}') except Exception: print(f'look up label_id {answer["label_id"]!r} in answer_to_action at {products_url}') def main(): p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument('--url', default=DEFAULT_URL, help='standard door URL') p.add_argument('--product', required=True) p.add_argument('--input', required=True, help='exact input JSON for the product schema') p.add_argument('--network', required=True, help='payment chain: eip155:8453, eip155:5042 or eip155:4663') p.add_argument('--record', required=True, help='NEW recovery record file in a private 0700 directory') p.add_argument('--max-price', default='0.25', help='most this check may cost, in USD (default 0.25, Small)') p.add_argument('--pay-to', default=DEFAULT_PAY_TO, help='expected payee; the purchase stops on a mismatch') p.add_argument('--keys', help='saved /v1/keys JSON; default fetches it from the API origin') p.add_argument('--recover-py', default=str(Path(__file__).with_name('recover.py'))) args = p.parse_args() recover_module = load_recover(args.recover_py) body = {'product': args.product, 'input': json.loads(args.input), 'options': {'network': args.network, 'max_price': args.max_price}} terms, price_usd = preflight(args.url, body, args.pay_to, args.max_price) print(f'quoted ${price_usd:.2f} on {terms["network"]}; paying with per-purchase cap ${price_usd:.2f}') if asyncio.run(purchase(args.url, body, terms, price_usd, args.record, recover_module)) == 202: print('202 accepted: check running; polling with the SAME credential (never signing again)') if args.keys: keys = json.loads(Path(args.keys).read_text()) else: keys = httpx.get(api_origin(args.url) + '/v1/keys', timeout=10).json() record = recover_module.load_record(args.record) try: reply = recover_module.recover(record, keys) # verifies signature, binding and billing except recover_module.PendingRecovery as error: print(error) return 1 envelope = reply['receipt']['envelope'] if 'answer' in envelope: interpret(reply, api_origin(args.url) + '/v1/products') return 0 archived = recover_module.archive_terminal(args.record, reply) guidance = ('a deliberate new purchase later, from a fresh 402 and a new record file, is allowed' if reply.get('hints', {}).get('new_quote_allowed') else 'do not start a new purchase for this input yet') print(f'terminal no-charge result: state={envelope.get("state")} reason={envelope.get("reason")}. ' f'Nothing was charged. Never retry this credential; {guidance}. Record archived at {archived}') return 2 if __name__ == '__main__': try: raise SystemExit(main()) except SystemExit: raise except Exception as error: raise SystemExit(f'failed_retain_private_record: {error}') from None