---
id: w7/m3/t001
title: "Capture Render's documented rate-limit contract into docs/ADR006-bex-api.md"
worker: worker7
status: done
estimate: 30m
depends_on: []
---

## Objective

Record, with evidence pointers, exactly what contract bex must match: Render's documented API rate limits (per-endpoint-class request budgets), the 429 response shape, and its retry headers (`Retry-After`, any `ratelimit-*` headers) — into a new rate-limits section of docs/ADR006-bex-api.md, so t002 implements against a captured contract instead of guesses.

## Context

- Render's API docs document rate limiting (api-docs.render.com — reference/rate-limiting page; the OpenAPI spec `render-public-api-1.json` marks 429 responses per endpoint).
- The parity ledger method (docs/ADR018-render-parity.md §Method) is the model: cite the live doc/spec, note divergences deliberately.
- bex serves three adapters off one core (docs/ADR006-bex-api.md) — the contract section should state how a 429 surfaces on GraphQL (top-level error with extension code) and MCP, since Render's docs only cover REST.
- **MCP layer — decide and record here (2026-07-11):** an outer mux-level middleware can only emit a transport-level HTTP 429 on the `/mcp` streamable-HTTP POST, not a JSON-RPC tool error. Pick one: (a) transport-level 429 + `Retry-After` (verify MCP SDK clients surface it sanely and document what they see), or (b) an in-dispatch limit check returning an MCP tool error. t002 implements whichever this doc records — the two tasks must not assume different layers.
- **Client-IP keying — decide and record here (2026-07-11):** on prod the only ingress is the Hetzner LB with proxy protocol **OFF** (`deploy/gitops/overlays/prod/values/traefik.values.yaml` — `uses-proxyprotocol: "false"`, and no `forwardedHeaders`/`proxyProtocol` trust anywhere in deploy/gitops), so bex-api's `r.RemoteAddr` is Traefik's pod IP and X-Forwarded-For carries the LB-SNAT address — **all anonymous traffic currently shares one bucket**. The contract must record either (a) enable proxy protocol + trusted forwarded headers (see t002 for the concrete knobs), or (b) deliberately accept one shared, generously sized anonymous bucket and mark the divergence parity-ledger style.

## Steps

1. Fetch Render's rate-limiting reference + the 429 entries in its OpenAPI spec; extract limits, headers, and error body shape.
2. Write docs/ADR006-bex-api.md §Rate limits: the captured contract, bex's chosen budgets (env-default), and the GraphQL/MCP mapping.
3. `npx prettier@3.4.2 --write "**/*.md"`.

## Files

- `docs/ADR006-bex-api.md` (new §Rate limits)

## Acceptance criteria

- [ ] Section cites Render's live doc/spec URLs and quotes the 429 + header shape
- [ ] bex's default budgets and the GraphQL/MCP error mapping are stated
- [ ] The MCP layer decision (transport HTTP 429 vs in-dispatch tool error) is recorded explicitly
- [ ] The client-IP keying decision (proxy protocol + trusted headers vs shared anonymous bucket) is recorded explicitly
- [ ] Divergences (if any) marked deliberately, parity-ledger style

## Out of scope

- Implementation (t002); updating docs/ADR018-render-parity.md rows (t005 owns ledger sync)
