Observed since 20 Oct, 23:02 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
AgentPulse for Python: detect AI agents, negotiate Markdown and declare Content Signals (FastAPI/Starlette).
AgentPulse for Python: detect AI agents, serve them clean Markdown and declare Content Signals in FastAPI, Starlette or any ASGI app. Standard library only.
pip install agentpulse-asgi
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
from agentpulse_asgi import AgentPulseMiddleware, ContentSignals, content_signals_robots_txt
signals = ContentSignals(search=True, ai_input=True, ai_train=False)
app = FastAPI()
app.add_middleware(AgentPulseMiddleware, content_signals=signals, on_event=print)
@app.get("/robots.txt", response_class=PlainTextResponse)
def robots() -> str:
return content_signals_robots_txt(signals)
Requests that prefer text/markdown (RFC 9110 q-values) get the HTML page converted to Markdown;
everyone else gets the original HTML. HTML responses carry Vary: Accept. Markdown responses carry
x-markdown-tokens / x-original-tokens estimates (Cloudflare's Markdown for Agents stopped sending
them on 2026-10-09) and, as Cloudflare does, no ETag/Last-Modified, since conditional requests cannot
be honored for a converted body.
AgentVerifier + get_client_ip=client_ip_from_scope): checks the
client IP against the ranges each operator publishes → verified, mismatch (likely spoofed) or
declared. Lists download in the background; failures fall back to declared.http(s)/mailto/relative URLs kept, nesting capped at 512.title, description, image from <meta>, with name taking
precedence over Open Graph) whose values are JSON-quoted strings that cannot inject keys, and the
page's valid JSON-LD appended as one fenced JSON value (an array when there are several blocks;
malformed blocks and blocks with javascript:/vbscript:/data: strings are dropped; 64 KB total).max_body_bytes (2 MB) read per response; the app is told to stop
(http.disconnect) once enough has been read; conversion runs in a worker thread; a conversion
cache and per-client/global rate limits protect the CPU (rate-limited requests get the original HTML).on_event (sync or async) runs in the background with the path and agent — never
the client IP or the query string. IngestClient(write_key) sends events to the AgentPulse API
(app.add_middleware(AgentPulseMiddleware, on_event=telemetry.on_event), then
await telemetry.shutdown() in your lifespan). It keeps a bounded queue (500 events; the oldest
are dropped and counted), sends batches every 10 s or at 50 events, and retries with backoff and
Retry-After. Paths are redacted (emails, UUIDs, long numbers, hex and tokens) before they leave
your server. Plain human requests are sent only as a counter.html.parser security fixes (CVE-2025-6069, gh-102555,
gh-135661). Older patch releases leak unclosed HTML comments as text and parse some inputs in
quadratic time; the middleware checks the interpreter at startup and refuses to run on a
vulnerable one (UnsafeHtmlParserError).uvicorn --proxy-headers --forwarded-allow-ips=...); never read X-Forwarded-For yourself.Vary could serve Markdown to browsers; configure them to vary on
Accept or not to cache negotiated pages.MIT
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.1.0 | 14 Oct 2026 | 1 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 23:02 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:45400e28e5b7310ac0d2e487c8528f0a053ddf8aa50befaf4e732dfdf6702b44