deploy commited on
Commit
0e17b96
·
0 Parent(s):

deploy clean-read x402 endpoint

Browse files
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Space (Docker SDK) for the clean-read x402 endpoint.
2
+ # Free, no credit card, stable URL. Sleeps after ~48h idle, wakes on request.
3
+ FROM python:3.11-slim
4
+
5
+ WORKDIR /app
6
+ ENV PYTHONUNBUFFERED=1 PORT=7860
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY scripts ./scripts
12
+
13
+ EXPOSE 7860
14
+ # --proxy-headers + trust all forwarders so the x402 `resource` URL is built as
15
+ # https:// (HF terminates TLS at its proxy and forwards plain http internally);
16
+ # a scheme mismatch can make strict x402 verifiers reject the payment.
17
+ CMD ["uvicorn", "scripts.clean_read_api.main:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers", "--forwarded-allow-ips", "*"]
README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: clean-read
3
+ emoji: 📖
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # clean-read API
13
+
14
+ URL → clean Markdown for AI agents. Fetches any page, strips nav/ads/boilerplate
15
+ with trafilatura, returns the main content as Markdown with title + word count.
16
+
17
+ Paid via **x402** micropayments (USDC on Base, Dexter facilitator, 0% fee):
18
+
19
+ - `GET /` — service info (free)
20
+ - `GET /health` — health check (free)
21
+ - `POST /read` — `{"url": "..."}` → Markdown — `$0.005` USDC
22
+
23
+ Unpaid requests get `HTTP 402` with payment requirements. Source: github.com/eltociear/my-molt-agent
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # clean-read x402 API (x402 protocol v2, official SDK).
2
+ fastapi>=0.115.0
3
+ uvicorn[standard]>=0.23.0
4
+ httpx>=0.28.1
5
+ trafilatura>=2.0.0
6
+ x402[fastapi,evm,extensions]>=2.12.0
scripts/__init__.py ADDED
File without changes
scripts/clean_read_api/__init__.py ADDED
File without changes
scripts/clean_read_api/main.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """clean-read x402 API — URL → clean Markdown extraction as a paid service (x402 v2).
3
+
4
+ Deploy: uvicorn scripts.clean_read_api.main:app --host 0.0.0.0 --port $PORT
5
+ Local: uvicorn scripts.clean_read_api.main:app --port 8403
6
+
7
+ Endpoints:
8
+ GET / — Service info (free)
9
+ GET /health — Health check (free)
10
+ POST /read — Fetch URL, strip boilerplate, return main content as Markdown. $0.005/call (x402)
11
+
12
+ Same stack as scripts/x402_api/main.py (skill-audit): official x402 v2 SDK,
13
+ USDC on Base mainnet, Dexter facilitator (zero-gate, 0% seller fee, auto-lists
14
+ on Bazaar discovery after first settled payment).
15
+
16
+ Why this service: agents constantly need "give me the readable text of this page"
17
+ (the Jina-reader use case) — extraction via trafilatura, priced at high-volume
18
+ $0.005 like tokenguard (our best-selling price point).
19
+ """
20
+ import os
21
+ from datetime import datetime
22
+ from fastapi import FastAPI, HTTPException
23
+ from fastapi.middleware.cors import CORSMiddleware
24
+ from pydantic import BaseModel
25
+ from typing import Optional
26
+
27
+ WALLET = os.environ.get("BASE_WALLET_ADDRESS", "0x5bCDA55247B238a573A968B234F788a2D35664Dd")
28
+ BASE_MAINNET = "eip155:8453"
29
+ FACILITATOR_URL = os.environ.get("FACILITATOR_URL", "https://x402.dexter.cash")
30
+
31
+ app = FastAPI(
32
+ title="clean-read API",
33
+ description="URL to clean Markdown for AI agents. x402 v2 micropayments on Base.",
34
+ version="1.0.0",
35
+ )
36
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
37
+
38
+ _x402_available = False
39
+ try:
40
+ from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption
41
+ from x402.http.middleware.fastapi import PaymentMiddlewareASGI
42
+ from x402.http.types import RouteConfig
43
+ from x402.mechanisms.evm.exact import ExactEvmServerScheme
44
+ from x402.server import x402ResourceServer
45
+ from x402.extensions.bazaar import declare_discovery_extension, OutputConfig
46
+
47
+ facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL))
48
+ server = x402ResourceServer(facilitator)
49
+ server.register(BASE_MAINNET, ExactEvmServerScheme())
50
+
51
+ ext = declare_discovery_extension(
52
+ input={"url": "https://example.com/article"},
53
+ input_schema={
54
+ "properties": {
55
+ "url": {"type": "string", "format": "uri", "description": "Page to fetch and clean"},
56
+ "include_links": {"type": "boolean", "description": "Keep hyperlinks in the Markdown (default true)"},
57
+ },
58
+ "required": ["url"],
59
+ },
60
+ body_type="json",
61
+ output=OutputConfig(example={
62
+ "url": "https://example.com/article",
63
+ "title": "Article title",
64
+ "markdown": "# Article title\n\nMain content…",
65
+ "word_count": 1234,
66
+ }),
67
+ )
68
+ ext["bazaar"]["info"]["input"]["method"] = "POST"
69
+
70
+ routes = {
71
+ "POST /read": RouteConfig(
72
+ accepts=[PaymentOption(scheme="exact", pay_to=WALLET, price="$0.005", network=BASE_MAINNET)],
73
+ mime_type="application/json",
74
+ description="Fetch a URL and return its main content as clean Markdown (boilerplate stripped)",
75
+ extensions=ext,
76
+ ),
77
+ }
78
+ app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)
79
+ _x402_available = True
80
+ except Exception as e: # pragma: no cover
81
+ print(f" x402 v2 init warning: {type(e).__name__}: {e}")
82
+
83
+
84
+ class ReadRequest(BaseModel):
85
+ url: str
86
+ include_links: Optional[bool] = True
87
+ max_size: Optional[int] = 2_000_000 # 2MB raw HTML cap
88
+
89
+
90
+ @app.get("/")
91
+ async def root():
92
+ return {
93
+ "service": "clean-read API",
94
+ "version": "1.0.0",
95
+ "description": "URL → clean Markdown. Boilerplate/nav/ads stripped, main content only. Built for AI agents.",
96
+ "endpoints": {
97
+ "GET /": "Service info (free)",
98
+ "GET /health": "Health check (free)",
99
+ "POST /read": "Fetch URL → Markdown ($0.005 USDC)",
100
+ },
101
+ "payment": {
102
+ "method": "x402",
103
+ "x402_version": 2,
104
+ "currency": "USDC",
105
+ "network": "Base (eip155:8453)",
106
+ "facilitator": FACILITATOR_URL,
107
+ "wallet": WALLET,
108
+ "x402_enabled": _x402_available,
109
+ },
110
+ }
111
+
112
+
113
+ @app.get("/health")
114
+ async def health():
115
+ return {"status": "ok", "timestamp": datetime.utcnow().isoformat() + "Z", "x402_enabled": _x402_available}
116
+
117
+
118
+ @app.post("/read")
119
+ async def read_url(req: ReadRequest):
120
+ url = req.url
121
+ if not url or not url.startswith(("http://", "https://")):
122
+ raise HTTPException(400, "valid http/https URL required")
123
+
124
+ import httpx
125
+ try:
126
+ async with httpx.AsyncClient(follow_redirects=True, timeout=20.0) as client:
127
+ resp = await client.get(url, headers={"User-Agent": "Mozilla/5.0 (compatible; clean-read/1.0; +https://eltociear-clean-read.hf.space)"})
128
+ resp.raise_for_status()
129
+ except httpx.HTTPStatusError as e:
130
+ raise HTTPException(502, f"upstream returned {e.response.status_code}")
131
+ except Exception as e:
132
+ raise HTTPException(502, f"fetch failed: {type(e).__name__}: {e}")
133
+
134
+ html = resp.text
135
+ if len(html) > req.max_size:
136
+ html = html[: req.max_size]
137
+
138
+ import trafilatura
139
+ markdown = trafilatura.extract(
140
+ html,
141
+ output_format="markdown",
142
+ include_links=bool(req.include_links),
143
+ include_tables=True,
144
+ favor_recall=True,
145
+ )
146
+ if not markdown:
147
+ raise HTTPException(422, "could not extract main content from this page")
148
+
149
+ title = None
150
+ try:
151
+ meta = trafilatura.extract_metadata(html)
152
+ if meta:
153
+ title = meta.title
154
+ except Exception:
155
+ pass
156
+
157
+ return {
158
+ "url": str(resp.url),
159
+ "title": title,
160
+ "markdown": markdown,
161
+ "word_count": len(markdown.split()),
162
+ "fetched_at": datetime.utcnow().isoformat() + "Z",
163
+ }
164
+
165
+
166
+ if __name__ == "__main__":
167
+ import uvicorn
168
+ port = int(os.environ.get("PORT", 8403))
169
+ print(f"\n clean-read API (x402 v2) starting on :{port}")
170
+ print(f" x402: {'ENABLED' if _x402_available else 'DISABLED (pip install x402[fastapi,evm,extensions])'}")
171
+ print(f" Facilitator: {FACILITATOR_URL}")
172
+ print(f" Wallet: {WALLET}\n")
173
+ uvicorn.run(app, host="0.0.0.0", port=port)