Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """clean-read x402 API — URL → clean Markdown extraction as a paid service (x402 v2). | |
| Deploy: uvicorn scripts.clean_read_api.main:app --host 0.0.0.0 --port $PORT | |
| Local: uvicorn scripts.clean_read_api.main:app --port 8403 | |
| Endpoints: | |
| GET / — Service info (free) | |
| GET /health — Health check (free) | |
| POST /read — Fetch URL, strip boilerplate, return main content as Markdown. $0.005/call (x402) | |
| Same stack as scripts/x402_api/main.py (skill-audit): official x402 v2 SDK, | |
| USDC on Base mainnet, Dexter facilitator (zero-gate, 0% seller fee, auto-lists | |
| on Bazaar discovery after first settled payment). | |
| Why this service: agents constantly need "give me the readable text of this page" | |
| (the Jina-reader use case) — extraction via trafilatura, priced at high-volume | |
| $0.005 like tokenguard (our best-selling price point). | |
| """ | |
| import os | |
| from datetime import datetime | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import Optional | |
| WALLET = os.environ.get("BASE_WALLET_ADDRESS", "0x5bCDA55247B238a573A968B234F788a2D35664Dd") | |
| BASE_MAINNET = "eip155:8453" | |
| FACILITATOR_URL = os.environ.get("FACILITATOR_URL", "https://x402.dexter.cash") | |
| app = FastAPI( | |
| title="clean-read API", | |
| description="URL to clean Markdown for AI agents. x402 v2 micropayments on Base.", | |
| version="1.0.0", | |
| ) | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) | |
| _x402_available = False | |
| try: | |
| from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption | |
| from x402.http.middleware.fastapi import PaymentMiddlewareASGI | |
| from x402.http.types import RouteConfig | |
| from x402.mechanisms.evm.exact import ExactEvmServerScheme | |
| from x402.server import x402ResourceServer | |
| from x402.extensions.bazaar import declare_discovery_extension, OutputConfig | |
| facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL)) | |
| server = x402ResourceServer(facilitator) | |
| server.register(BASE_MAINNET, ExactEvmServerScheme()) | |
| ext = declare_discovery_extension( | |
| input={"url": "https://example.com/article"}, | |
| input_schema={ | |
| "properties": { | |
| "url": {"type": "string", "format": "uri", "description": "Page to fetch and clean"}, | |
| "include_links": {"type": "boolean", "description": "Keep hyperlinks in the Markdown (default true)"}, | |
| }, | |
| "required": ["url"], | |
| }, | |
| body_type="json", | |
| output=OutputConfig(example={ | |
| "url": "https://example.com/article", | |
| "title": "Article title", | |
| "markdown": "# Article title\n\nMain content…", | |
| "word_count": 1234, | |
| }), | |
| ) | |
| ext["bazaar"]["info"]["input"]["method"] = "POST" | |
| routes = { | |
| "POST /read": RouteConfig( | |
| accepts=[PaymentOption(scheme="exact", pay_to=WALLET, price="$0.005", network=BASE_MAINNET)], | |
| mime_type="application/json", | |
| description="Fetch a URL and return its main content as clean Markdown (boilerplate stripped)", | |
| extensions=ext, | |
| ), | |
| } | |
| app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server) | |
| _x402_available = True | |
| except Exception as e: # pragma: no cover | |
| print(f" x402 v2 init warning: {type(e).__name__}: {e}") | |
| class ReadRequest(BaseModel): | |
| url: str | |
| include_links: Optional[bool] = True | |
| max_size: Optional[int] = 2_000_000 # 2MB raw HTML cap | |
| async def root(): | |
| return { | |
| "service": "clean-read API", | |
| "version": "1.0.0", | |
| "description": "URL → clean Markdown. Boilerplate/nav/ads stripped, main content only. Built for AI agents.", | |
| "endpoints": { | |
| "GET /": "Service info (free)", | |
| "GET /health": "Health check (free)", | |
| "POST /read": "Fetch URL → Markdown ($0.005 USDC)", | |
| }, | |
| "payment": { | |
| "method": "x402", | |
| "x402_version": 2, | |
| "currency": "USDC", | |
| "network": "Base (eip155:8453)", | |
| "facilitator": FACILITATOR_URL, | |
| "wallet": WALLET, | |
| "x402_enabled": _x402_available, | |
| }, | |
| } | |
| async def health(): | |
| return {"status": "ok", "timestamp": datetime.utcnow().isoformat() + "Z", "x402_enabled": _x402_available} | |
| async def read_url(req: ReadRequest): | |
| url = req.url | |
| if not url or not url.startswith(("http://", "https://")): | |
| raise HTTPException(400, "valid http/https URL required") | |
| import httpx | |
| try: | |
| async with httpx.AsyncClient(follow_redirects=True, timeout=20.0) as client: | |
| resp = await client.get(url, headers={"User-Agent": "Mozilla/5.0 (compatible; clean-read/1.0; +https://eltociear-clean-read.hf.space)"}) | |
| resp.raise_for_status() | |
| except httpx.HTTPStatusError as e: | |
| raise HTTPException(502, f"upstream returned {e.response.status_code}") | |
| except Exception as e: | |
| raise HTTPException(502, f"fetch failed: {type(e).__name__}: {e}") | |
| html = resp.text | |
| if len(html) > req.max_size: | |
| html = html[: req.max_size] | |
| import trafilatura | |
| markdown = trafilatura.extract( | |
| html, | |
| output_format="markdown", | |
| include_links=bool(req.include_links), | |
| include_tables=True, | |
| favor_recall=True, | |
| ) | |
| if not markdown: | |
| raise HTTPException(422, "could not extract main content from this page") | |
| title = None | |
| try: | |
| meta = trafilatura.extract_metadata(html) | |
| if meta: | |
| title = meta.title | |
| except Exception: | |
| pass | |
| return { | |
| "url": str(resp.url), | |
| "title": title, | |
| "markdown": markdown, | |
| "word_count": len(markdown.split()), | |
| "fetched_at": datetime.utcnow().isoformat() + "Z", | |
| } | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 8403)) | |
| print(f"\n clean-read API (x402 v2) starting on :{port}") | |
| print(f" x402: {'ENABLED' if _x402_available else 'DISABLED (pip install x402[fastapi,evm,extensions])'}") | |
| print(f" Facilitator: {FACILITATOR_URL}") | |
| print(f" Wallet: {WALLET}\n") | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |