Conventions

Everything is GET or POST over HTTPS. Request bodies are JSON. Responses are JSON with one deliberate exception: the replay endpoints, whose entire job is to be wrong in a specified way. If /r/{id} returns a 418 with a body that shouldn't exist, that is not a bug in our bug database. It is the exhibit.

Every replay response carries:

X-Fault-Specimen: FD-2026-017
X-Fault-Variant: hang

so your logs can distinguish "we hit a specimen" from "someone deployed on a Friday".

Authentication: optional. Anonymous requests are limited per-IP; send X-API-Key: fd_... or ?api_key=fd_... for the pro tier. See pricing.

Catalog

GET /api/specimens

The whole registry. Signatures included — you could, in principle, implement the entire service from this one response, which tells you where the moat actually is: not the format, the curation.

curl -s https://faultydata.com/api/specimens | jq '.[0]'
{
  "id": "FD-2026-061",
  "title": "Memory leak served in responses (Cloudbleed, Feb 2017)",
  "category": "corruption",
  "severity": "fatal",
  "tests": ["parsing", "monitoring"],
  "citation": {
    "source": "Cloudflare — incident report on memory leak...",
    "url": "https://blog.cloudflare.com/incident-report-...",
    "date": "2017-02-23"
  },
  "anatomy": "A buffer overrun in Cloudflare's HTML parser...",
  "tags": ["real-world", "security", "memory-leak"],
  "signature": { "status": 200, "body": "...", "corrupt": "leak" }
}
fieldnotes
severityfatal | severe | moderate | trivial — drives "most fucked" ordering
testswhat your client is being examined on: retry-logic, timeouts, parsing, headers, auth, semantics, monitoring, loops
categorystatus | latency | corruption | sequence | composite | headers | redirect
citationpresent on real-world specimens — the postmortem it models

GET /api/specimens/{id}

One specimen. 404s with {"error":"not found"} — a genuine 404, not a specimen. Check the X-Fault-Specimen header if you're ever unsure which kind of broken you're looking at.

Replay

GET /r/{id}

Replays the fault described by the specimen. The response is deliberately wrong in exactly the documented way. Query params:

parameffect
?seed=Ndeterministic variant selection for weighted composites
?key=Sisolated sequence state — each key gets its own fail-then-recover progress
curl -i https://faultydata.com/r/FD-2026-001
HTTP/2 429
retry-after: 30
x-fault-specimen: FD-2026-001

{"error":{"type":"rate_limit_error",...}}

GET /run/{token}/{id}

Same replay, inside a battery. The attempt is logged server-side (including retry count — yes, we see your hammering) and feeds the report card. Same query params as /r/.

Auto-fuck

For when picking a specimen by hand is too much effort — automation for things that were previously only manually fucked.

GET /r/auto

Deals a random exhibit from the deck. Every request is a surprise; X-Fault-Auto tells you which specimen drew the short straw.

parameffect
?severity=Srestrict the deck — trivial | moderate | severe | fatal
?seed=Ndeterministic draw — same seed, same specimen
curl -i https://faultydata.com/r/auto
HTTP/2 451
x-fault-auto: FD-2026-067

GET /r/escalate?key=name

Punishes a client key with worsening exhibits the longer it survives: calm for the first three draws, unpleasant through seven, then severe/fatal for as long as it keeps coming back. X-Fault-Level reports how deep in the hole you are.

for i in $(seq 1 20); do
  curl -s -o /dev/null -w '%{http_code}\n' \
    'https://faultydata.com/r/escalate?key=my-client'
done

ANY /chaos?url=…

The chaos proxy: it fetches a real upstream and then fucks with it. Point your client at us, we point ourselves at your staging API, and somewhere in the middle the response gets mangled.

parameffect
?url=required — the http(s) upstream to fetch (private/loopback/link-local targets refused; we are not an SSRF oracle)
?rate=Fprobability the response gets mangled, 0–1 (default 0.5)
?seed=Ndeterministic fault selection

Faults injected: wrong Content-Type, truncated body, malformed tail, synthetic 5xx, empty 200, latency tax, embedded NULs. X-Chaos-Fault names the mechanism, X-Chaos-Fault-Specimen cites the registry exhibit it models (e.g. truncate → FD-2026-011), and a Link: rel="describedby" header points at the specimen page — even chaos stays cross-referenced to canon.

curl -i 'https://faultydata.com/chaos?url=https://api.staging.example.com/users&rate=0.8'
HTTP/2 200
x-chaos-fault: truncate
x-chaos-upstream-status: 200

Batteries

POST /battery

curl -X POST https://faultydata.com/battery \
  -H 'content-type: application/json' \
  -d '{"client":"httpx/0.27"}'

Returns:

{
  "token": "9f3c...20hex",
  "client": "httpx/0.27",
  "specimens": ["FD-2026-001", "FD-2026-002", ...],
  "report": "/report/9f3c...20hex"
}
fieldnotes
client (in)free-form name for the thing under test; appears on the report card
tokenthe battery session id — embeds in all /run/ and /report/ URLs
specimensthe full battery list, most-fucked first

GET /run/{token}/auto · GET /run/{token}/escalate

Auto-fuck inside a battery. /auto deals a random exhibit (same ?severity/?seed params); /escalate climbs the punishment ladder per battery — keep drawing and watch X-Fault-Level deepen until the client dies. Every draw is logged against the report card under the specimen it landed on, so the card records which exhibit actually bit you.

POST /run/{token}/{id}/report

curl -X POST https://faultydata.com/run/$TOKEN/FD-2026-017/report \
  -H 'content-type: application/json' \
  -d '{"outcome":"timeout","ms":5002,"detail":"max-time fired as expected"}'
fieldnotes
outcomeok | status_error | timeout | parse_error | crash | skipped
mselapsed ms — optional but it makes the card honest
detailfree-text, shown on the report

Reports

GET /report/{token}

The report card — HTML. Grade letter, score, pass count, and per-specimen verdicts including server-observed attempt counts.

GET /api/reports/{token}

{
  "token": "9f3c...",
  "client": "httpx/0.27",
  "score": 87,
  "grade": "B+",
  "results": [
    {"id":"FD-2026-001","outcome":"status_error","ms":42,"attempts":1},
    {"id":"FD-2026-017","outcome":"timeout","ms":5002,"attempts":1}
  ]
}

Badges

GET /badge/{token}.svg

![FaultyData grade](https://faultydata.com/badge/9f3c....svg)

SVG, color-coded by score. Green means your error handling survived the museum. Red means it didn't, and now everyone can see.

Meta

endpointreturns
GET /healthzok — the one endpoint guaranteed healthy
GET /.well-known/security.txtcontact for security reports

Errors & rate limits

statuswhen
404genuinely not found (check X-Fault-Specimen — its absence means this one is real)
400malformed report body — invalid JSON or unknown outcome
429you exceeded the tier limit — 60/min anonymous, 600/min with a key

Rate-limit responses include Retry-After. If you ignore it you'll hit FD-2026-001 for real, which would be poetic.

Full examples

Python — run one specimen with httpx

import httpx
try:
    r = httpx.get("https://faultydata.com/r/FD-2026-017", timeout=5)
    print("survived:", r.status_code)
except httpx.TimeoutException:
    print("correctly gave up waiting for a socket that never replies")

Node — fetch with a deadline

const res = await fetch("https://faultydata.com/r/FD-2026-016",
  { signal: AbortSignal.timeout(3000) });
console.log(res.status, res.headers.get("x-fault-specimen"));

Go — battery loop

token := mustPOST("/battery", `{"client":"go-http/1.0"}`)
for _, id := range specimens(token) {
    ctx, cancel := context.WithTimeout(bg, 5*time.Second)
    outcome := classify(do(ctx, "/run/"+token+"/"+id))
    mustPOST("/run/"+token+"/"+id+"/report", outcome)
    cancel()
}