diff --git a/x402_quantum_payments.ipynb b/x402_quantum_payments.ipynb new file mode 100644 index 0000000..39eae83 --- /dev/null +++ b/x402_quantum_payments.ipynb @@ -0,0 +1,581 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Monetizing Quantum Compute with x402 HTTP Payments\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ionq-samples/getting-started/blob/main/x402_quantum_payments.ipynb)\n", + "\n", + "The [x402 protocol](https://www.x402.org/) brings the HTTP `402 Payment Required` status code to life — letting any API gate access behind stablecoin micropayments, with no subscription or billing account needed.\n", + "\n", + "This notebook walks through the full protocol flow:\n", + "\n", + "1. **Public discovery** — find payment-gated endpoints and their pricing\n", + "2. **Unpaid 402 challenge** — inspect the payment requirement before any money moves\n", + "3. **Wallet approval** — user/agent explicitly authorizes the payment\n", + "4. **Signed payment retry** — resend with cryptographic payment proof\n", + "5. **Job execution** — gateway verifies payment on-chain, forwards to IonQ API\n", + "\n", + "## Why This Matters for Quantum\n", + "\n", + "Quantum compute has a natural fit with micropayments:\n", + "- **Per-shot pricing** already mirrors per-request billing\n", + "- **AI agents** need autonomous compute access without human billing setup\n", + "- **Low-friction onboarding** — run a circuit with just a wallet, no account creation\n", + "- **Cross-border access** — USDC payments work globally without merchant accounts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install dependencies\n", + "!pip install requests qiskit qiskit-ionq" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "### Gateway trust boundary\n", + "\n", + "An x402 gateway sits between your client and the upstream API. This means the\n", + "gateway operator can see any headers you send through it — **including API keys**.\n", + "\n", + "Before sending real credentials through any gateway, verify:\n", + "- **Who operates it** — is it the API provider, a known infrastructure partner, or a third party?\n", + "- **What it can see** — the gateway terminates TLS and can read request/response bodies\n", + "- **Whether it's contractually authorized** — does the upstream API provider endorse this gateway?\n", + "\n", + "⚠️ **Do not send real IonQ API keys through a third-party gateway unless you trust\n", + "the operator and have verified the trust relationship.** For this notebook, we use\n", + "the simulator with a placeholder gateway URL. Replace it with a gateway you trust\n", + "before using real credentials or QPU targets.\n", + "\n", + "Gateway options:\n", + "- **Self-host** using the [Coinbase x402 reference implementation](https://github.com/coinbase/x402)\n", + "- **Provider-operated** gateway run by the quantum compute provider (ideal — no third-party trust needed)\n", + "- **Third-party hosted** (e.g., `gateway.spraay.app`) — verify trust before sending API keys" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import requests\n", + "from datetime import datetime\n", + "\n", + "# ── Gateway URL ────────────────────────────────────────────\n", + "# Replace with a gateway you trust. See trust boundary notes above.\n", + "X402_GATEWAY = os.environ.get(\"X402_GATEWAY_URL\", \"YOUR_X402_GATEWAY_URL\")\n", + "\n", + "# ── IonQ API Key ──────────────────────────────────────────\n", + "# Only send this through a gateway you trust (see above).\n", + "IONQ_API_KEY = os.environ.get(\"IONQ_API_KEY\", \"your_ionq_api_key\")\n", + "\n", + "if X402_GATEWAY == \"YOUR_X402_GATEWAY_URL\":\n", + " print(\"⚠️ Set X402_GATEWAY_URL to a gateway you trust before running payment cells.\")\n", + " print(\" Self-host: https://github.com/coinbase/x402\")\n", + " print(\" Or use a provider-operated gateway.\")\n", + "else:\n", + " print(f\"Gateway: {X402_GATEWAY}\")\n", + " print(f\"Protocol: x402 (HTTP 402 Payment Required)\")\n", + " print(f\"Settlement: USDC on Base L2\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Public Discovery\n", + "\n", + "x402 gateways advertise payment-gated endpoints at well-known paths.\n", + "Common discovery endpoints across implementations include:\n", + "\n", + "| Path | Convention |\n", + "|------|------------|\n", + "| `/.well-known/x402` | Coinbase reference implementation |\n", + "| `/.well-known/x402.json` | JSON-specific variant |\n", + "| `/.well-known/x402-manifest` | Extended manifest format |\n", + "\n", + "The discovery step is read-only — no payment, no credentials needed.\n", + "This lets agents inspect what's available and at what price before committing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Common x402 discovery paths across gateway implementations\n", + "DISCOVERY_PATHS = [\n", + " \"/.well-known/x402\",\n", + " \"/.well-known/x402.json\",\n", + " \"/.well-known/x402-manifest\",\n", + "]\n", + "\n", + "\n", + "def discover_x402_endpoints(gateway_url, keyword=None):\n", + " \"\"\"\n", + " Probe a gateway's well-known paths for endpoint discovery.\n", + "\n", + " Tries common x402 discovery paths and returns the first\n", + " successful response. No credentials or payment needed.\n", + " \"\"\"\n", + " for path in DISCOVERY_PATHS:\n", + " try:\n", + " resp = requests.get(\n", + " f\"{gateway_url}{path}\",\n", + " headers={\"Accept\": \"application/json\"},\n", + " timeout=10,\n", + " )\n", + " if resp.status_code == 200:\n", + " data = resp.json()\n", + " endpoints = data.get(\"endpoints\", data.get(\"resources\", []))\n", + " print(f\"✅ Discovery succeeded at {path}\")\n", + " print(f\" Found {len(endpoints)} endpoint(s)\\n\")\n", + " if keyword:\n", + " endpoints = [\n", + " ep for ep in endpoints\n", + " if keyword.lower() in json.dumps(ep).lower()\n", + " ]\n", + " return {\"discovery_path\": path, \"endpoints\": endpoints}\n", + " except requests.RequestException:\n", + " continue\n", + "\n", + " print(\"❌ No discovery endpoint responded.\")\n", + " print(f\" Tried: {', '.join(DISCOVERY_PATHS)}\")\n", + " return None\n", + "\n", + "\n", + "# Run discovery (no payment, no credentials)\n", + "if X402_GATEWAY != \"YOUR_X402_GATEWAY_URL\":\n", + " discovery = discover_x402_endpoints(X402_GATEWAY, keyword=\"inference\")\n", + " if discovery:\n", + " for ep in discovery[\"endpoints\"][:5]:\n", + " print(f\" {ep.get('method', 'POST')} {ep.get('path', '')}\")\n", + " print(f\" Price: {ep.get('price', 'N/A')} USDC\")\n", + " print(f\" {ep.get('description', '')}\")\n", + " print()\n", + "else:\n", + " print(\"⏭️ Skipped — set X402_GATEWAY_URL first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Unpaid 402 Challenge Inspection\n", + "\n", + "Before any money moves, send an **unpaid request** to the payment-gated endpoint.\n", + "The gateway returns a `402 Payment Required` response containing the exact payment\n", + "requirement. This is a smoke test — verify the challenge shape before proceeding.\n", + "\n", + "A well-formed 402 challenge should include:\n", + "\n", + "| Field | Description |\n", + "|-------|-------------|\n", + "| `amount` / `price` | Cost in the settlement asset |\n", + "| `asset` / `currency` | Settlement token (e.g., USDC) |\n", + "| `network` | Settlement chain (e.g., base, ethereum) |\n", + "| `payTo` | Recipient address for payment |\n", + "| `resource` | The endpoint being gated |\n", + "| `expiry` | How long the quote is valid |\n", + "\n", + "Also inspect response headers for `Cache-Control` and CORS headers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def inspect_402_challenge(gateway_url, path, payload=None):\n", + " \"\"\"\n", + " Send an unpaid request and inspect the 402 challenge.\n", + "\n", + " This is a safe, no-cost validation step. The gateway returns\n", + " the payment requirement without executing anything or moving\n", + " any funds. Use this to verify the gateway is well-behaved\n", + " before authorizing payment.\n", + "\n", + " No API keys are sent in this step.\n", + " \"\"\"\n", + " try:\n", + " resp = requests.post(\n", + " f\"{gateway_url}{path}\",\n", + " json=payload or {},\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " timeout=15,\n", + " )\n", + " except requests.RequestException as e:\n", + " print(f\"❌ Request failed: {e}\")\n", + " return None\n", + "\n", + " print(f\"HTTP {resp.status_code}\")\n", + " print()\n", + "\n", + " if resp.status_code != 402:\n", + " print(f\"⚠️ Expected 402, got {resp.status_code}\")\n", + " print(f\" Body: {resp.text[:300]}\")\n", + " return None\n", + "\n", + " # Parse the 402 challenge body\n", + " try:\n", + " challenge = resp.json()\n", + " except ValueError:\n", + " print(\"⚠️ 402 response is not valid JSON\")\n", + " return None\n", + "\n", + " # Validate expected fields\n", + " expected_fields = [\"price\", \"amount\", \"currency\", \"asset\",\n", + " \"network\", \"payTo\", \"resource\", \"expiry\"]\n", + " print(\"── 402 Challenge Body ──────────────────────────\")\n", + " for key, value in challenge.items():\n", + " marker = \"✅\" if key in expected_fields else \" \"\n", + " print(f\" {marker} {key}: {value}\")\n", + "\n", + " # Check which expected fields are present\n", + " present = [f for f in expected_fields if f in challenge]\n", + " missing = [f for f in expected_fields if f not in challenge]\n", + " print(f\"\\n Present: {', '.join(present) if present else 'none'}\")\n", + " if missing:\n", + " # Some fields may use alternate names, so this is informational\n", + " print(f\" Not found: {', '.join(missing)} (may use alternate field names)\")\n", + "\n", + " # Inspect response headers\n", + " print(\"\\n── Response Headers (relevant) ────────────────\")\n", + " for header in [\"Cache-Control\", \"Access-Control-Allow-Origin\",\n", + " \"Access-Control-Allow-Headers\", \"X-Payment-Required\"]:\n", + " val = resp.headers.get(header)\n", + " if val:\n", + " print(f\" {header}: {val}\")\n", + "\n", + " print(\"────────────────────────────────────────────────\")\n", + " return challenge\n", + "\n", + "\n", + "# Smoke test — no payment, no API key\n", + "if X402_GATEWAY != \"YOUR_X402_GATEWAY_URL\":\n", + " print(\"Sending unpaid request to inspect 402 challenge...\\n\")\n", + " challenge = inspect_402_challenge(\n", + " X402_GATEWAY,\n", + " \"/v0.1/ai-inference\",\n", + " payload={\"target\": \"simulator\", \"shots\": 100, \"input\": {}}\n", + " )\n", + "else:\n", + " print(\"⏭️ Skipped — set X402_GATEWAY_URL first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Build a Quantum Circuit\n", + "\n", + "Before involving payments, build the circuit payload locally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bell State circuit in IonQ's native JSON format\n", + "bell_state = {\n", + " \"format\": \"ionq.circuit.v0\",\n", + " \"qubits\": 2,\n", + " \"circuit\": [\n", + " {\"gate\": \"h\", \"target\": 0},\n", + " {\"gate\": \"cnot\", \"control\": 0, \"target\": 1}\n", + " ]\n", + "}\n", + "\n", + "print(\"Circuit: H(q0) → CNOT(q0, q1)\")\n", + "print(\"Expected output: |00⟩ and |11⟩ with ~50/50 probability\")\n", + "print(f\"\\nPayload:\\n{json.dumps(bell_state, indent=2)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Signed Payment Submission\n", + "\n", + "The full x402 payment flow has three distinct phases:\n", + "\n", + "```\n", + "Phase 1: Unpaid request → 402 challenge (done in Step 2)\n", + "Phase 2: Wallet signs a payment authorization covering the quoted amount\n", + "Phase 3: Retry with signed proof in X-PAYMENT-PROOF header → 200 + result\n", + "```\n", + "\n", + "The code below shows the **complete flow** — but the actual payment signing\n", + "(Phase 2) requires a funded wallet and a client library. We mark the signing\n", + "step as pseudocode.\n", + "\n", + "### Client libraries for payment signing\n", + "\n", + "| Library | Language | Notes |\n", + "|---------|----------|-------|\n", + "| [`@coinbase/x402`](https://github.com/coinbase/x402) | TypeScript | Reference implementation |\n", + "| [Coinbase AgentKit](https://github.com/coinbase/agentkit) | Python/TS | AI agent wallet with x402 support |\n", + "| [Spraay Agent Wallet SDK](https://www.npmjs.com/package/spraay-agent-wallet) | TypeScript | Managed agent wallets on Base |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def submit_circuit_x402(gateway_url, circuit_json, shots=100, api_key=None):\n", + " \"\"\"\n", + " Submit a quantum circuit through an x402 gateway.\n", + "\n", + " Implements the full three-phase protocol flow:\n", + "\n", + " Phase 1 — Unpaid request: send the job without payment proof.\n", + " Gateway returns 402 with payment requirement.\n", + "\n", + " Phase 2 — Payment signing (pseudocode): a wallet client library\n", + " signs a USDC authorization covering the quoted amount.\n", + " This step requires a funded wallet and a real client SDK.\n", + "\n", + " Phase 3 — Paid retry: resend with X-PAYMENT-PROOF header containing\n", + " the signed payment. Gateway verifies on-chain and proxies\n", + " the authenticated request to IonQ.\n", + "\n", + " ⚠️ API key trust: if api_key is provided, it is sent to the gateway\n", + " in the Authorization header. Only provide this if you trust the\n", + " gateway operator. See the trust boundary notes at the top.\n", + " \"\"\"\n", + " payload = {\n", + " \"target\": \"simulator\",\n", + " \"shots\": shots,\n", + " \"input\": circuit_json,\n", + " }\n", + "\n", + " # ── Phase 1: Unpaid request → 402 challenge ──────────\n", + " print(\"Phase 1: Sending unpaid request...\")\n", + " resp = requests.post(\n", + " f\"{gateway_url}/v0.1/ai-inference\",\n", + " json=payload,\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " timeout=15,\n", + " )\n", + "\n", + " if resp.status_code != 402:\n", + " print(f\" Unexpected status: {resp.status_code}\")\n", + " return None\n", + "\n", + " challenge = resp.json()\n", + " amount = challenge.get(\"price\") or challenge.get(\"amount\", \"?\")\n", + " network = challenge.get(\"network\", \"?\")\n", + " pay_to = challenge.get(\"payTo\", \"?\")\n", + " print(f\" 402 received — {amount} USDC on {network}\")\n", + " print(f\" Pay to: {pay_to}\")\n", + "\n", + " # ── Phase 2: Payment signing (pseudocode) ────────────\n", + " #\n", + " # In production, a wallet client library handles this:\n", + " #\n", + " # from x402_client import sign_payment\n", + " # payment_proof = sign_payment(\n", + " # wallet=agent_wallet,\n", + " # amount=challenge[\"price\"],\n", + " # asset=\"USDC\",\n", + " # network=challenge[\"network\"],\n", + " # pay_to=challenge[\"payTo\"],\n", + " # resource=challenge.get(\"resource\", \"/v0.1/ai-inference\"),\n", + " # )\n", + " #\n", + " # The returned payment_proof is a signed authorization that the\n", + " # gateway can verify on-chain without the client broadcasting\n", + " # a transaction first — the gateway settles it.\n", + " #\n", + " print(\"\\nPhase 2: Payment signing (pseudocode — requires wallet SDK)\")\n", + " print(\" → In production: wallet signs USDC authorization\")\n", + " print(\" → Libraries: @coinbase/x402, Coinbase AgentKit, etc.\")\n", + " payment_proof = \"PSEUDOCODE_PROOF_PLACEHOLDER\"\n", + "\n", + " # ── Phase 3: Paid retry ──────────────────────────────\n", + " print(\"\\nPhase 3: Retrying with signed payment proof...\")\n", + " headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"X-PAYMENT-PROOF\": payment_proof,\n", + " }\n", + " if api_key:\n", + " # ⚠️ Only include if you trust the gateway operator\n", + " headers[\"Authorization\"] = f\"apiKey {api_key}\"\n", + "\n", + " resp = requests.post(\n", + " f\"{gateway_url}/v0.1/ai-inference\",\n", + " json=payload,\n", + " headers=headers,\n", + " timeout=30,\n", + " )\n", + "\n", + " if resp.status_code in (200, 201):\n", + " result = resp.json()\n", + " print(f\" ✅ Job submitted — ID: {result.get('id', 'N/A')}\")\n", + " print(f\" Status: {result.get('status', 'N/A')}\")\n", + " return result\n", + " elif resp.status_code == 402:\n", + " print(f\" ⚠️ Still 402 — payment proof was not accepted\")\n", + " print(f\" (Expected with pseudocode placeholder)\")\n", + " return {\"status\": \"payment_proof_required\", \"challenge\": challenge}\n", + " else:\n", + " print(f\" Error {resp.status_code}: {resp.text[:200]}\")\n", + " return None\n", + "\n", + "\n", + "# Run the full flow\n", + "if X402_GATEWAY != \"YOUR_X402_GATEWAY_URL\":\n", + " print(\"── Full x402 Flow: Bell State Circuit ──────────\\n\")\n", + " result = submit_circuit_x402(\n", + " X402_GATEWAY, bell_state, shots=1000,\n", + " # api_key=IONQ_API_KEY # uncomment only for trusted gateways\n", + " )\n", + "else:\n", + " print(\"⏭️ Skipped — set X402_GATEWAY_URL first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Qiskit Integration\n", + "\n", + "Build circuits with Qiskit, then convert and submit through the x402 flow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "def qiskit_to_ionq_json(qc):\n", + " \"\"\"Convert a Qiskit QuantumCircuit to IonQ native JSON.\"\"\"\n", + " gate_map = {\n", + " 'h': lambda q, _: {\"gate\": \"h\", \"target\": q[0]},\n", + " 'x': lambda q, _: {\"gate\": \"x\", \"target\": q[0]},\n", + " 'y': lambda q, _: {\"gate\": \"y\", \"target\": q[0]},\n", + " 'z': lambda q, _: {\"gate\": \"z\", \"target\": q[0]},\n", + " 'cx': lambda q, _: {\"gate\": \"cnot\", \"control\": q[0], \"target\": q[1]},\n", + " 'rz': lambda q, p: {\"gate\": \"rz\", \"target\": q[0], \"rotation\": p[0]},\n", + " 'ry': lambda q, p: {\"gate\": \"ry\", \"target\": q[0], \"rotation\": p[0]},\n", + " }\n", + " gates = []\n", + " for inst in qc.data:\n", + " name = inst.operation.name\n", + " if name in gate_map:\n", + " qubits = [qc.find_bit(q).index for q in inst.qubits]\n", + " gates.append(gate_map[name](qubits, inst.operation.params))\n", + " return {\"format\": \"ionq.circuit.v0\", \"qubits\": qc.num_qubits, \"circuit\": gates}\n", + "\n", + "\n", + "# Build a 3-qubit GHZ state\n", + "ghz = QuantumCircuit(3)\n", + "ghz.h(0)\n", + "ghz.cx(0, 1)\n", + "ghz.cx(1, 2)\n", + "\n", + "print(ghz.draw(output='text'))\n", + "print()\n", + "\n", + "ionq_payload = qiskit_to_ionq_json(ghz)\n", + "print(f\"IonQ JSON:\\n{json.dumps(ionq_payload, indent=2)}\")\n", + "\n", + "# Submit via x402 (same flow as Bell state)\n", + "if X402_GATEWAY != \"YOUR_X402_GATEWAY_URL\":\n", + " print(\"\\n── Full x402 Flow: GHZ Circuit ─────────────────\\n\")\n", + " result = submit_circuit_x402(X402_GATEWAY, ionq_payload, shots=1000)\n", + "else:\n", + " print(\"\\n⏭️ Skipped payment flow — set X402_GATEWAY_URL first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding x402 to Your Own Quantum API\n", + "\n", + "If you're a quantum compute provider, here's the minimal server-side pattern.\n", + "x402 is **additive** — it doesn't replace existing API key auth, it runs alongside it.\n", + "\n", + "```python\n", + "from flask import Flask, request, jsonify\n", + "\n", + "app = Flask(__name__)\n", + "\n", + "@app.route('/v1/jobs', methods=['POST'])\n", + "def submit_job():\n", + " # If no payment header, use existing auth flow (unchanged)\n", + " if 'X-PAYMENT-PROOF' not in request.headers:\n", + " if 'Authorization' in request.headers:\n", + " return existing_api_key_flow(request)\n", + " # No auth at all — return 402 challenge\n", + " return jsonify({\n", + " 'price': '0.01',\n", + " 'currency': 'USDC',\n", + " 'network': 'base',\n", + " 'payTo': '0xYourReceiverAddress',\n", + " 'resource': '/v1/jobs',\n", + " 'expiry': 300, # seconds\n", + " }), 402\n", + "\n", + " # Verify the signed payment proof on-chain\n", + " proof = request.headers['X-PAYMENT-PROOF']\n", + " if verify_usdc_payment(proof, expected_amount='0.01'):\n", + " return process_quantum_job(request)\n", + " else:\n", + " return jsonify({'error': 'payment_verification_failed'}), 402\n", + "```\n", + "\n", + "Key points:\n", + "- Existing API key customers are **unaffected** — the x402 path only activates when payment headers are present\n", + "- The 402 challenge includes `resource` and `expiry` so clients know exactly what they're paying for\n", + "- Payment verification happens on-chain (Base L2) — no credit card processor needed\n", + "\n", + "## Resources\n", + "\n", + "| Resource | Link |\n", + "|----------|------|\n", + "| x402 Protocol Specification | [x402.org](https://www.x402.org/) |\n", + "| Coinbase x402 Reference (server + client) | [github.com/coinbase/x402](https://github.com/coinbase/x402) |\n", + "| Coinbase AgentKit (wallet + x402 signing) | [github.com/coinbase/agentkit](https://github.com/coinbase/agentkit) |\n", + "| IonQ API Reference | [docs.ionq.com](https://docs.ionq.com/api-reference/v0.2/) |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}