#!/usr/bin/env python3 """ Aegis /route — 5-minute quickstart for agent developers ======================================================= Aegis /route lets an Algorand agent reach the whole x402 ecosystem (1,600+ services on Algorand *and* Base) with ONE payment endpoint. You pay once in USDC on Algorand. Aegis pays the upstream on the upstream's own chain and relays the result back. * no swap, no second wallet, no second chain for you * upstream is trust-ranked; failover is automatic * AUTH-THEN-CAPTURE — your payment is only captured if an upstream delivered Install: pip install x402 x402-avm algosdk httpx Try it free (no wallet, no key) — this only needs the network: python3 route_quickstart.py --need weather Pay for real (needs a funded Algorand wallet): export ALGO_KEY="<25-word mnemonic>" # USDC ASA 31566704 on mainnet python3 route_quickstart.py --need weather --pay Done. Two calls. That is the whole integration. """ import argparse import json import os import sys import uuid import httpx BASE = os.environ.get("AEGIS_BASE", "https://algo.borisinc.com") ALGO_CAIP2 = "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=" # ───────────────────────────────────────────────────────────────────── # STEP 1 — free quote. No wallet, no key, no payment. Tell your agent # what a route costs before it commits to anything. # ───────────────────────────────────────────────────────────────────── def quote(need: str, max_usd: float | None = None) -> dict: params = {"need": need} if max_usd is not None: params["max"] = max_usd r = httpx.get(f"{BASE}/route/quote", params=params, timeout=30) r.raise_for_status() return r.json() # ───────────────────────────────────────────────────────────────────── # STEP 2 — the wallet. An x402 AVM signer: address + sign_transactions. # Signs ONLY the transactions where we are the sender, so the # facilitator's gasless fee-payer leg is left untouched. # ───────────────────────────────────────────────────────────────────── class AlgoKeySigner: def __init__(self, mnemonic: str): from algosdk import account, mnemonic as algo_mnemonic self._sk = algo_mnemonic.to_private_key(mnemonic.strip()) self._addr = account.address_from_private_key(self._sk) @property def address(self) -> str: return self._addr def sign_transactions(self, unsigned_txns, indexes_to_sign): import base64 from algosdk import encoding as _enc out = [None] * len(unsigned_txns) for i in indexes_to_sign: raw = unsigned_txns[i] if isinstance(raw, str): raw = base64.b64decode(raw) txn = _enc.msgpack_decode(base64.b64encode(raw).decode()) out[i] = base64.b64decode(_enc.msgpack_encode(txn.sign(self._sk))) return out def build_client(mnemonic: str): """One x402 client registered for Algorand (add Base the same way).""" from x402 import x402ClientSync from x402.http import x402HTTPClientSync, PaymentRoundTripper from x402.mechanisms.avm.exact import ExactAvmClientScheme client = x402ClientSync() client.register( ALGO_CAIP2, ExactAvmClientScheme( AlgoKeySigner(mnemonic), algod_url=os.environ.get("ALGOD_URL", "https://mainnet-api.4160.nodely.dev"), ), ) return PaymentRoundTripper(x402HTTPClientSync(client)) # ───────────────────────────────────────────────────────────────────── # STEP 3 — call /route. The round-tripper is a RESPONSE HANDLER, not a # client: issue an ordinary GET, hand it the 402 + a retry # function, and it signs, pays and retries for you. # ───────────────────────────────────────────────────────────────────── def route(need: str, mnemonic: str, max_usd: float | None = None) -> dict: params = {"need": need} if max_usd is not None: params["max"] = max_usd buyer = build_client(mnemonic) with httpx.Client(timeout=120, follow_redirects=True) as s: first = s.get(f"{BASE}/route", params=params) final = buyer.handle_response( str(uuid.uuid4()), first.status_code, dict(first.headers), first.content, lambda headers: s.get(f"{BASE}/route", params=params, headers=headers), ) try: body = final.json() except Exception: body = final.text[:2000] return {"status": final.status_code, "body": body} # ───────────────────────────────────────────────────────────────────── def show_challenge(need: str) -> None: """Dry run: hit /route unpaid and print exactly what it would cost.""" r = httpx.get(f"{BASE}/route", params={"need": need}, timeout=30) if r.status_code == 402: ch = r.json() acc = (ch.get("accepts") or [{}])[0] amount = int(acc.get("amount") or acc.get("maxAmountRequired") or 0) / 1e6 print(f"402 — Aegis quotes ${amount:.6f} USDC on {acc.get('network')}") print(f" pay to : {acc.get('payTo')}") print(f" asset : {acc.get('asset')} (USDC, 6 decimals)") print(f" fee : gasless — feePayer {(acc.get('extra') or {}).get('feePayer')}") print("\nFund that wallet with USDC on Algorand mainnet, then re-run with --pay.") else: print(f"HTTP {r.status_code}: {r.text[:400]}") def main() -> int: ap = argparse.ArgumentParser(description="Aegis /route quickstart") ap.add_argument("--need", required=True, help="capability to route, e.g. weather") ap.add_argument("--max", type=float, default=None, help="max upstream USD to accept") ap.add_argument("--pay", action="store_true", help="actually pay (default: free quote only)") a = ap.parse_args() q = quote(a.need, a.max) print(json.dumps(q, indent=2)) if not q.get("routable"): print(f"\nNo routable upstream for '{a.need}'. Try /discover?query={a.need}") return 1 if not a.pay: print() show_challenge(a.need) return 0 mnemonic = os.environ.get("ALGO_KEY") if not mnemonic: print("\nSet ALGO_KEY to your 25-word Algorand mnemonic to pay.", file=sys.stderr) return 2 out = route(a.need, mnemonic, a.max) print(json.dumps(out, indent=2)) return 0 if out["status"] == 200 else 1 if __name__ == "__main__": raise SystemExit(main())