# if.api deep dive v1.0.1 (locked) (adapters, boundaries, examples)

Danny Stocker | Infrafabric Research | ds@infrafabric.io | 2026-02-01

Status:
- LOCKED snapshot (v1.0.1). Do not edit this file in place.
- Future changes MUST be published as a new numbered doc.

Supersedes:
- `docs/228-if-api-deep-dive-whitepaper-2026-01-31.md`
- `docs/231-if-api-deep-dive-whitepaper-2026-02-01.md` (v1.0)

Errata (v1.0 → v1.0.1):
- Correct `if.bus` posture: `if.bus` is `preview` (not `roadmap`) and a runtime exists (no public bus endpoints claimed).

Scaffolding: if.whitepapers.bible (docs/182-if-whitepapers-bible.md)

Black/white:
- Verified claims in this document are limited to repo-local bytes under `/root/`, public URLs explicitly listed, and deterministic local behavior when a command is shown under "Verify".
- This document does not claim a deployed `if.api` runtime service on this host; `if.api` is `status=preview` in `if.registry.json`.
- This document does not claim correctness, completeness, safety, or compliance guarantees. Receipts (when present) prove byte integrity only.
- This document contains no secret values. It may reference opaque secret pointers (e.g., `secret_ref`) but never raw tokens/keys.

If you cannot reproduce a statement with a verify command, a referenced file, or a public URL, treat it as narrative, not evidence.

## Table of contents

- 0) Executive summary (one screen)
- 0b) Ecosystem primer (for first-time readers)
- 1) What `if.api` is (and is not)
- 2) The adapter boundary (why it exists)
- 2b) Normalization example (before/after)
- 3) Contracts: `if.api.adapter` and `if.api.adapter_spec`
- 3b) Quick start: building your first adapter
- 3c) Schema evolution: v1 → v2 (parallel run)
- 3d) Multi-tenant adapters: discovery + isolation
- 3e) Anti-patterns (what not to normalize)
- 4a) Where `if.api` sits relative to `if.bus`, `if.trace`, `/llm`, `/mcp`
- 4b) Cross-cutting: `if.security` posture
- 4c) Composition: `if.api` → `if.api` + (optional) `if.bus` envelopes
- 4d) Agent/LLM messages: structured requests (no vibes)
- 5) Example 1: GitHub webhook normalization (demo)
- 6) Example 2: DOI resolver (demo)
- 6b) Example 2b: Resolver dispatch/chaining (`if.api` → `if.api`)
- 7) Example 3: Ad decisioning (Revive Adserver) (proposed)
- 8) Guardrails: rate limits, caching, idempotency, secrets, classification
- 8b) Failure modes: drift, 429s, signature failures, spec conflicts
- 9) Ship checklist: making an integration reviewable
- 10) Verify checklist (boring on purpose)
- 11) Appendix (links, one URL per line)

*If “integration” isn’t reproducible without private access, it’s not an integration — it’s a story.*

---

## 0) Executive summary (one screen)

`if.api` is the “expansion slot” layer: it normalizes provider quirks at the edge so the rest of the system can stay boring.

What it does:
- Defines how external APIs/webhooks are described (`adapter`) and how those descriptions become enforceable contracts (`adapter_spec`).
- Encourages black/white capability claims and evidence pinning (what is verified vs unknown).
- Enables stable, reviewable demos where outputs can be bound to `if.trace` receipts (brand `if.trace`).

What it does not do:
- It is not the message backbone (`if.bus`).
- It is not the public evidence surface (`/llm`).
- It is not a secret store (it only carries *references* to secrets).
- It is not “an API gateway that proxies everything” (it is a normalization boundary with explicit scope).

One-line mental model:
- `if.api` is the adapter at the wall: it translates “provider reality” into “our contract”, then gets out of the way.

*If you put provider quirks into every downstream component, you’re choosing sprawl — you just haven’t paid the invoice yet.*

---

## 0b) Ecosystem primer (for first-time readers)

You can read this paper without any other Infrafabric docs, but these names show up a lot:

- `if.registry.json`: the canonical registry of product/module IDs, their `status`, and preferred `path` shapes.
- `if.trace`: receipt-backed evidence for “these bytes exist” (not “these bytes are true”).
- `/llm`: public, no-login review surface (HTML-first) for packs, inventories, and derived views.
- `/mcp`: token-gated surface for tenant/NDA artifacts; same black/white discipline, different classification.
- `if.bus`: async envelope contract + internal runtime (`status=preview`); do not assume public endpoints exist.

*If a system needs insider context to review, it will fail its first external audit.*

---

## 1) What `if.api` is (and is not)

### What it is

`if.api` is the layer where you:
- decide what an integration *means* (IDs normalized, canonical fields, minimal claims),
- encode constraints (rate limits, payload limits),
- pin evidence for claims (docs URLs + retrieval time + sha256),
- and produce outputs that can be receipt-backed.

In this repo, the canonical “truth” about `if.api` identity is registry-driven (product id + path shape).

Verified (repo-local):
- `if.api` registry entry: `if.registry.json` (`status=preview`, `path=/if/api`)
- `if.api` contract schemas:
  - `schemas/if-api/adapter.schema.json`
  - `schemas/if-api/adapter_spec.schema.json`
- `if.api` public inventory (black/white): `docs/32-if-api-integrations-inventory.md`

### What it is not

`if.api` is not:
- a claim that “we have all these integrations deployed”
- a compliance wrapper
- a replacement for deterministic evidence (`if.trace`) or the published review surface (`/llm`)
- a generic “proxy” where any call is allowed without an adapter spec

*If an integration can’t say what it is not, it will quietly become everything.*

---

## 2) The adapter boundary (why it exists)

Every provider has a different kind of “weird”:
- IDs sometimes change type across endpoints (int vs string).
- rate limits are undocumented, conditional, or per-tenant.
- pagination semantics differ (cursor vs page vs time window).
- webhook deliveries are lossy, duplicated, or unordered.
- auth is a moving target (OAuth scopes, token refresh, app review, etc.).

Without an adapter boundary, downstream code ends up re-implementing each provider’s quirks repeatedly, and you lose:
- consistency (same entity has different shapes in different places),
- auditability (nobody knows which endpoint was used),
- enforceability (rate limits and payload bounds are “guidelines” not gates),
- reviewability (no stable contract to bind receipts against).

Two “first principles” rules:
1) **Normalize at the edge**: convert provider-specific shapes into canonical shapes immediately.
2) **Make claims testable**: every capability claim should be true/false/unknown, with evidence pins where possible.

```mermaid
flowchart LR
  Raw["Provider reality: JSON/webhook/HTML"]

  subgraph Edge["Adapter boundary (if.api)"]
    Norm["Normalize + enforce: if.api adapter"]
    Spec["adapter_spec: limits + claims"]
    Spec --> Norm
  end

  Raw --> Norm
  Norm --> Canon["Canonical output: stable JSON contract"]

  subgraph Consumers["Downstream (stable contract)"]
    Trace["if.trace receipt (optional)"]
    LLM["/llm pack (public)"]
    MCP["/mcp pack (private)"]
    Bus["if.bus envelope (preview; internal)"]
  end

  Canon --> Trace
  Canon --> LLM
  Canon --> MCP
  Canon --> Bus

```

*If you don’t build a wall, every downstream component becomes the wall.*

---

## 2b) Normalization example (before/after)

Provider payloads are allowed to be messy. Canonical outputs are not.

Illustrative “before” (provider reality):
```json
{
  "id": 123,
  "createdAt": "2026-01-31T12:34:56Z",
  "url": "https://example.com/x?y=1",
  "extra": {"weird": true}
}
```

Illustrative “after” (canonical contract):
```json
{
  "event_type": "provider.example.v1",
  "provider": "example",
  "id": "123",
  "occurred_at": "2026-01-31T12:34:56Z",
  "target_url": "https://example.com/x?y=1"
}
```

What changed (the point of the wall):
- IDs are normalized (often strings) so type drift doesn’t leak downstream.
- Timestamps are normalized (UTC ISO8601).
- Provider-only fields either become explicit canonical fields or get dropped.
- The adapter spec names what is enforced (rate limits, payload bounds, signature checks), so downstream code doesn’t have to.

*If the “after” shape isn’t enforceable, you didn’t normalize — you translated vibes.*

---

## 3) Contracts: `if.api.adapter` and `if.api.adapter_spec`

`if.api` has two separate but related “contract layers”:

1) `if.api.adapter` (metadata)
- identity (`adapter_id`, `label`, `version`)
- provider metadata (`provider`, `base_url`)
- auth *type* (never raw secrets)
- high-level capabilities

2) `if.api.adapter_spec` (enforceable spec)
- endpoint map (names + methods + paths)
- structured rate limits with pinned evidence records
- black/white capability claims (`true` / `false` / `unknown`)
- constraints (max payload, etc.)

This split is deliberate:
- The adapter is the “what exists” label.
- The spec is the “what we can safely claim and enforce” contract.

Schema anchors (repo-local):
- `schemas/if-api/adapter.schema.json`
- `schemas/if-api/adapter_spec.schema.json`

*If the contract is only prose, it will be re-interpreted by whoever is late and tired.*

---

## 3b) Quick start: building your first adapter

1) Define scope (and the explicit “not” list).

2) Choose a versioning strategy:
- Recommend SemVer for `adapter.version` (`MAJOR.MINOR.PATCH`).
- Breaking change policy: bump `MAJOR` when the output contract changes shape/meaning; keep the previous major runnable until consumers migrate.

3) Write the contracts first:
- `if.api.adapter`: identity + provider metadata + auth type (no secrets).
- `if.api.adapter_spec`: endpoints, constraints, rate limits + evidence pins, capability claims.

4) Implement normalization + enforcement:
- Validate inputs; fail fast on malformed requests.
- Call provider; record “observed behavior” (status codes, retry headers) without leaking secrets.
- Emit canonical JSON outputs; keep provider quirks out of consumers.
- Emit observability: structured logs (`adapter_id`, request id, `outcome`, latency bucket) + minimal counters.
- Warning: do not log raw provider bodies (or raw inbound payloads) unless you have sanitized/redacted them first; never publish raw captures on `/llm`.

5) Plan for evolution:
- Prefer additive changes (new fields; never re-meaning old ones).
- If you must break shape/type, introduce a new `event_type` or a new adapter major version; do not silently mutate existing outputs.

6) Multi-tenant discipline (when `tenant_id` exists):
- Treat `tenant_id` as a first-class partition key for rate limits, caches, and secrets.
- Never let one tenant’s spikes or failures consume another tenant’s budgets.

7) Make it reviewable:
- Provide a runnable demo + `if.trace` receipts + an HTML-first review surface.

Anti-patterns (fast smell test):
- “We’ll just proxy the provider and normalize later.”
- “We’ll log headers so we can debug auth.”
- “We’ll change the JSON shape; the frontend can adapt.”

See also:
- Section 3c for parallel-run mechanics (v1 + v2 simultaneously).
- Section 3d for multi-tenant isolation strategies.
- Section 3e for a fuller anti-pattern list.

*If you can’t write the contract before the code, you’re not building an adapter — you’re building a one-off.*

---

## 3c) Schema evolution: v1 → v2 (parallel run)

If the output contract changes shape or meaning, treat it as a new contract, not an in-place edit.

Goal:
- keep existing consumers working (v1) while new consumers migrate (v2)
- make routing explicit so caches, receipts, and debugging stay unambiguous

Three things that must be versioned together (or you will lie to yourself):
- the adapter implementation (producer identity)
- the output event type (contract identity)
- the validateable schema (shape)

Parallel run (conceptual routing):
```mermaid
flowchart LR
  Client -->|v1| A1["Adapter v1"] --> Provider
  Client -->|v2| A2["Adapter v2"] --> Provider
  A1 --> O1["Output contract v1"]
  A2 --> O2["Output contract v2"]

```

Common routing patterns (pick one; don’t mix accidentally):
- **Versioned paths**: `/.../v1/...` and `/.../v2/...` (explicit; cache-friendly)
- **Versioned negotiation**: `Accept: application/vnd.if.api.<name>.v1+json` (requires careful cache variation)
- **Explicit version parameter**: request includes `contract_major=1|2` (easy to reason about; easy to misuse)

Migration checklist (OSE: ops + security + evidence):
- run both majors for a defined deprecation window; do not “flip” silently
- partition caches and rate limits by `(tenant_id, adapter_id, major)` to avoid cross-talk
- publish receipts for v1 and v2 outputs separately (integrity only), and keep verify commands runnable

Deprecation discipline (make it measurable):
- Publish a migration note with v2 (what changed, what to test).
- Define v1 sunset criteria (date and/or sustained near-zero usage); do not assume migration.
- Remove v1 only after the criteria are met; keep the last-known-good v1 demo/receipt links available for audit.

Black/white: this repo contains schemas, specs, and demos; it does not claim a deployed routing plane.

*If you can’t run v1 and v2 at the same time, you don’t have migration — you have a flag day.*

---

## 3d) Multi-tenant adapters: discovery + isolation

Multi-tenancy is mostly an ops problem disguised as a feature.

Discovery (where to look first):
- Inventory (repo-local): `docs/32-if-api-integrations-inventory.md`
- Product identity and path shapes (repo-local): `if.registry.json` (`if.api` is `preview`)
- Example spec file (repo-local): `docs/data/if_api_adapter_spec.ggq_revive_ads.v1.json`

Isolation rules (design intent):
- Every request must resolve a `tenant_id` (explicit or derived) *before* any provider call.
- Partition these by `tenant_id`:
  - rate limit buckets (per tenant, per endpoint)
  - caches (include `tenant_id` and contract major in cache keys)
  - secret resolution (`secret_ref` includes a tenant segment)
  - receipts and published artifacts (public `/llm` vs tenant `/mcp`)
- Do not share raw provider responses across tenants; normalize first, then cache canonical outputs if you can.

Tenant discovery failure (fail closed):
- If `tenant_id` cannot be resolved before the provider call, stop and return a 4xx; do not fall back to a shared/default tenant bucket.
- Derive `tenant_id` from authenticated context; do not trust untrusted query params/headers as tenant selectors.

Rate limits (practical):
- Providers often mix “per-app” and “per-user” limits. Model both:
  - a global limiter (protects the provider relationship)
  - a per-tenant limiter (protects tenant fairness)
- When in doubt: fail closed (429) rather than create retry storms that burn global budgets.

OSE note:
- **Ops** wants bounded retries + stable dashboards.
- **Security** wants zero secret leakage and explicit classification boundaries.
- **Evidence** wants receipts + runnable verify steps for any demo or claim.

*If you don’t name the partition key, the incident will.*

---

## 3e) Anti-patterns (what not to normalize)

These are the mistakes that turn an adapter boundary into a permanent debugging tax.

Data/contract anti-patterns:
- Normalizing timestamps to epoch seconds (lose timezone semantics); prefer UTC ISO8601.
- Leaking provider field names into canonical outputs (“just pass through `providerFooBar`”).
- Treating unstable numeric IDs as integers (overflow, string/int drift); normalize to strings when in doubt.
- Returning “whatever the provider returned” and calling it “canonical”.

Versioning/migration anti-patterns:
- Mutating an existing output contract in place (no major bump, no parallel run).
- Reusing the same `event_type` for a different meaning (“same name, new semantics”).
- Shipping v2 without publishing how to keep v1 consumers alive.

Secrets/logging anti-patterns:
- Logging headers or query params “for debugging” (auth leaks happen this way).
- Storing tokens/keys in adapter specs or receipts (even “temporarily”).

Multi-tenant anti-patterns:
- Sharing cache keys or rate limit buckets across tenants (one tenant can DoS the rest).
- Publishing tenant-specific payloads on `/llm` instead of `/mcp`.

Evidence anti-patterns:
- Treating `if.trace` receipts as proof of truth (receipts prove byte integrity only).
- Claiming limits/capabilities without pinned evidence (URL + retrieved time + sha256).

*If you normalize the wrong things, you’ll spend your life debugging the right things.*

---

## 4a) Where `if.api` sits relative to `if.bus`, `if.trace`, `/llm`, `/mcp`

Conceptual boundary (black/white):
- `if.api` is `preview` (registry); contracts + demos exist.
- `if.bus` is `preview` (registry); a reference runtime exists and an internal sandbox runtime is documented (no public bus endpoints claimed).

Practical integration rule of thumb:
- **Synchronous needs** (request/response): do it at the adapter boundary (`if.api`).
- **Asynchronous needs** (telemetry, retries, fanout, long-running work): route through a bus contract (`if.bus`) when available in your environment (no public endpoints are implied).
- **Evidence needs**: bind bytes to `if.trace` receipts, and publish on `/llm` (public) or `/mcp` (tenant/NDA) based on classification.

Public docs (black/white posture):
- `docs/30-if-bus-and-api-integrations.md`
- `docs/31-if-bus-whitepaper.md`
- `docs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md`

*If you use async for a decision the page needs right now, you’ve built latency by design.*

---

## 4b) Cross-cutting: `if.security` posture

`if.security` is the cross-cutting posture for how modules describe and act on security-relevant findings.

Black/white:
- `if.security` is `roadmap` in `if.registry.json` (no claim of a deployed security runtime).
- The posture is still useful: it defines shared primitives so each module doesn’t invent its own “security story”.

Verified anchors:
- Posture doc (repo-local): `docs/89-if-security-posture.md`
- Security signal schema (repo-local): `schemas/if-bus/security_signal.schema.json`

How it cross-cuts `if.api` (design target):
- Treat “secret-like material observed” and “signature validation failed” as **security signals**, not log spam.
- Use evidence kinds (`observed|derived|hypothesis`) so automation can fail closed (never automate CONTAIN/HALT on hypothesis).
- Keep `/llm` public-safe: any tenant payloads or NDA examples go to `/mcp`.

Security signal flow (conceptual):
```mermaid
flowchart LR
  Adapter["if.api adapter"] -->|signal| Sig["if.bus.security_signal (public-safe)"]
  Sig --> Triage["Ops/Sec triage (human)"]
  Sig --> Gov["if.gov deliberation (optional)"]
  Triage -.->|action (manual/policy-gated)| Ctrl["if.bus control plane (pause/quarantine)"]
  Gov -.->|action (manual/policy-gated)| Ctrl

```

Legend:
- Solid arrows = telemetry signals (public-safe; no secrets).
- Dashed arrows = side-effect actions; manual/policy-gated (not autonomous; not a public endpoint claim).

Minimal `security_signal` shape (illustrative; see schema for full contract):
```json
{
  "schema_id": "if.bus.security_signal",
  "schema_version": "1.0.0",
  "signal_id": "sec-01J...ULID",
  "emitted_utc": "2026-01-31T12:34:56Z",
  "detector": {"detector_id": "secrets.detect", "version": "1.0.0", "kind": "regex"},
  "severity": "high",
  "finding_type": "secret_like_token",
  "summary": "Detector matched a token-like pattern in an inbound payload; excerpt is redacted.",
  "recommended_action": "quarantine",
  "redactions": {"redacted_excerpt": "Authorization: Bearer [REDACTED]"},
  "extensions": {"if.security": {"evidence_kind": "observed"}}
}
```

Public pack (HTML-first):
https://infrafabric.io/llm/products/if-security/security-posture/2026-01-11/index.html

Preview demo receipt (`if.security.secrets.detect`, integrity only):
https://infrafabric.io/static/trace/n6DSejaslZrB2ciVjLiBCg

Canonical path shape (registry):
https://infrafabric.io/if/security/

*If every module writes its own security posture, none of them have one.*

---

## 4c) Composition: `if.api` → `if.api` + (optional) `if.bus` envelopes

Composition means adapter outputs are stable enough to become inputs to other adapters without re-learning provider quirks.

Three useful composition patterns:
- **Dispatch**: one adapter selects a specific resolver/adapter based on input scheme, then delegates.
- **Enrichment**: adapter A produces a canonical event, adapter B adds derived fields (still canonical; no provider leaks).
- **Fanout** (optional): adapter output is wrapped in an `if.bus` envelope for async delivery and retries.

Envelopes (important distinction):
- `if.api` produces **canonical JSON payloads** (your contract).
- `if.bus` wraps those payloads in a **bus envelope** (transport contract).

Verified schema anchor (repo-local):
- `schemas/if-bus/envelope.schema.json`

Note (black/white): `if.bus` is `preview` in `if.registry.json`; the envelope example below is schema-backed, but no public bus endpoints are implied.

Minimal `if.bus` envelope example (illustrative; semver schema_version required):
```json
{
  "schema_id": "if.bus.envelope",
  "schema_version": "0.1.0",
  "event_id": "01J...ULID",
  "emitted_utc": "2026-01-31T12:34:56Z",
  "producer": {"kind": "if.api.adapter", "id": "if.api.resolver.dispatch", "version": "0.1.0"},
  "event": {"name": "if.api.resolver.dispatch.resolved"},
  "routing": {"topic": "if.api.resolutions", "partition_key": "tenant:demo"},
  "payload_media_type": "application/json",
  "payload": {"event_type": "resolver.dispatch", "selected_adapter_id": "if.api.resolver.doi", "resolved": {"target_url": "https://..."}},
  "payload_sha256": "sha256-of-canonical-payload-bytes"
}
```

Black/white: `if.bus` is `preview` in the registry; envelope wrapping is a transport contract, not a public deployment claim.

*If adapters can’t compose, you didn’t build a platform — you built a pile.*

---

## 4d) Agent/LLM messages: structured requests (no vibes)

When you ask an LLM (or any agent) to design or invoke an adapter, require a machine-checkable request shape.
This avoids “agent-only language” by using boring, typed fields humans can audit.

Minimal shape (illustrative):
```json
{
  "intent": "resolve_identifier",
  "actions": [
    {
      "tool": "if.api.resolver.doi",
      "args": {"doi": "10.1234/example"},
      "require_receipt": true,
      "dry_run": false
    }
  ],
  "constraints": {"latency_sla_ms": 3000, "payload_max_bytes": 1048576},
  "reason_codes": ["user_request"],
  "evidence": [{"kind": "spec", "url": "https://doi.org/10.1234/example", "retrieved_at": "2026-01-31T12:34:56Z", "sha256": "..."}],
  "risk": {"level": "low"},
  "trace": {"parent_event_id": "01J...ULID"}
}
```

Rules of thumb:
- Prefer structured data over custom DSLs; if you add expressions (policy/scoring), pin test vectors.
- Validate the request schema before any provider call; enforce `adapter_spec` constraints the same way.

*If a tool call can’t be described as data, it will be described as vibes.*

---

## 5) Example 1: GitHub webhook normalization (demo)

Use case:
- Input: GitHub “push” webhook JSON.
- Output: canonical “event envelope” JSON that downstream systems can consume without GitHub-specific branching.

Black/white:
- This is documented as a receipt-backed demo in `docs/32-if-api-integrations-inventory.md`.
- This example shows the *shape*; it is not a claim of live webhook delivery on this host.

Minimal input shape (illustrative):
```json
{
  "ref": "refs/heads/main",
  "after": "e7c1…",
  "repository": {"full_name": "org/repo"},
  "pusher": {"name": "alice"}
}
```

Minimal normalized output shape (illustrative):
```json
{
  "event_type": "github.push",
  "source": {"provider": "github", "repo": "org/repo"},
  "refs": {"ref": "refs/heads/main", "after": "e7c1…"},
  "actor": {"label": "alice"}
}
```

Public demo (HTML-first):
https://infrafabric.io/static/hosted/review/if-api-github-webhook-demo/2026-01-05/index.html

Receipt (if.trace, brand `if.trace`):
https://infrafabric.io/static/trace/Z_kiBtTQHztidcgYYaSRy4UK

*If you can’t normalize a webhook, you’ll normalize your incident response instead.*

---

## 6) Example 2: DOI resolver (demo)

Use case:
- Input: a DOI string (or URL form).
- Output: resolved metadata + canonicalized target URL, returned as normalized JSON.

Why it belongs in `if.api`:
- providers vary in response shapes and redirect behavior
- DOI resolution is “identifier → metadata”, not “integrity”

Illustrative output shape:
```json
{
  "event_type": "resolver.doi",
  "input": {"doi": "10.1234/example"},
  "resolved": {
    "title": "…",
    "publisher": "…",
    "target_url": "https://…"
  }
}
```

Public demo (HTML-first):
https://infrafabric.io/static/hosted/review/if-api-doi-resolver-demo/2026-01-07/index.html

Receipt (if.trace, brand `if.trace`):
https://infrafabric.io/static/trace/EKMtQbLWtQLZEERUqcKX1g

*If you confuse “resolved” with “verified”, you will ship a trust bug dressed as a feature.*

---

## 6b) Example 2b: Resolver dispatch/chaining (`if.api` → `if.api`)

Use case:
- Input: a generic identifier string (DOI, SWHID, OCI digest, etc.).
- Output: a single canonical response shape while delegating to a specific resolver adapter.

Why it matters:
- Each resolver stays separately testable and receipt-backed.
- A dispatcher can compose adapters without leaking provider quirks into consumers.

Illustrative dispatch response shape:
```json
{
  "event_type": "resolver.dispatch",
  "input": {"identifier": "10.1234/example"},
  "selected_adapter_id": "if.api.resolver.doi",
  "resolved_event": {"event_type": "resolver.doi", "resolved": {"target_url": "https://..."}}
}
```

Related demos (HTML-first):
https://infrafabric.io/static/hosted/review/if-api-doi-resolver-demo/2026-01-07/index.html
https://infrafabric.io/static/hosted/review/if-api-swhid-resolver-demo/2026-01-07/index.html

Related receipts (if.trace; integrity only):
https://infrafabric.io/static/trace/EKMtQbLWtQLZEERUqcKX1g
https://infrafabric.io/static/trace/gtwHpclpxuWVxvcRPDecFQ

*If every resolver needs a bespoke client, you didn’t normalize — you multiplied.*

---

## 7) Example 3: Ad decisioning (Revive Adserver) (proposed)

Context:
- Revive Adserver is an open-source ad server (formerly OpenX).
- GGQ needs deterministic, constraint-aware ad selection for fixed page slots (top banner vs right-rail pavés).

Black/white:
- This section is a proposed integration design, not a deployment claim.
- Backward-compatibility constraint: GGQ v9 already consumes an `ads.json` response shape; preserve that shape even if the upstream provider changes.
- Stable consumer schema (repo-local): `schemas/if-api/ggq_ads_json.schema.json`
- Proposed provider adapter_spec schema (repo-local): `schemas/if-api/ggq_revive_ads_adapter_spec.schema.json`
- Draft spec instance (repo-local): `docs/data/if_api_adapter_spec.ggq_revive_ads.v1.json`

Decisioning rule (first principles):
- **Ad selection is synchronous** (the page needs the creative immediately).
- Therefore: decisioning belongs in an `if.api` adapter (request/response), not on an async bus path.
- Telemetry (impressions/clicks), sync jobs, and audit exports can go async later.

Stable consumer contract (GGQ v9, illustrative):
- Input (HTTP GET): `page=<home|destination|itineraires|article|map>`, optional `destination_id=<int>`, optional `locale=<fr|en>`
- Output (JSON):
```json
{
  "locale": "fr",
  "page": "article",
  "destination_id": 234,
  "banner": {"id": 123, "href": "https://…", "image_url": "https://…", "alt": "…", "type": "banniere"},
  "paves": [{"id": 628, "href": "https://…", "image_url": "https://…", "alt": "…", "type": "pave"}],
  "inline": null
}
```

Proposed adapter boundary:
- The `if.api` adapter talks to Revive (provider reality) and emits the stable `ads.json` contract (our contract).
- Decisioning stays synchronous; telemetry (impressions/clicks) can go async later.

Non-negotiable guardrails (slot safety):
- Banner slot must only receive banner-shaped creatives; otherwise return `null` and let the frontend fallback path render.
- Pavé slots can be more flexible, but still must enforce max byte size + mime type + safe URL allowlist.
- `tenant_id` does not need to appear in the public `ads.json` response, but it must partition secret refs, rate limits, caches, and rotation state.

Slot enforcement snippet (illustrative; fail closed on layout safety):
```python
def filter_eligible(slot_spec, creatives):
    eligible = []
    for c in creatives:
        if c.get("type") not in slot_spec["allowed_types"]:
            continue
        if (c.get("width"), c.get("height")) not in slot_spec["allowed_sizes"]:
            continue
        if int(c.get("byte_size") or 0) > slot_spec["max_bytes"]:
            continue
        if not is_https_url(c.get("image_url")):
            continue
        if not is_allowlisted_host(c.get("image_url")):
            continue
        eligible.append(c)
    return eligible

def pick_many_deterministically(eligible, limit):
    # Stable selection: sort by (priority desc, id asc). No randomness => reproducible for debugging/receipts.
    eligible = sorted(eligible, key=lambda c: (-int(c.get("priority") or 0), str(c.get("id") or "")))
    return eligible[:limit]

banner = pick_many_deterministically(filter_eligible(BANNER_SPEC, revive_creatives), limit=1)
banner = banner[0] if banner else None
paves  = pick_many_deterministically(filter_eligible(PAVE_SPEC, revive_creatives), limit=PAVE_SPEC["max_count"])
```

Determinism note:
- Stable sorting (no randomness) keeps outputs reproducible for debugging and receipt-backed demos.
- If you later add weighting, rotation, or frequency capping, treat it as a stateful change that must be documented and test-vectored.

Repo-local POC (non-authoritative, but runnable):
- `scripts/if_api_ggq_ads_revive_adapter_poc.py`

*If you let arbitrary creative sizes into a fixed layout, you’re not selling ads — you’re selling broken pages.*

---

## 8) Guardrails: rate limits, caching, idempotency, secrets, classification

### Rate limits (enforceable, not vibes)

If a provider publishes rate limits, treat them as a contract surface:
- capture them in `adapter_spec.rate_limits`
- pin evidence (URL + retrieved_at + sha256)
- implement enforcement (backoff, caching, retry discipline)

### Caching (explicit)

Caching is not an optimization; it is part of the correctness story when providers are flaky.

Rule:
- cache must be explicit in the adapter spec (what is cached, TTL, invalidation signal)

### Idempotency (repeatable without corruption)

For write-like operations:
- require idempotency keys
- record stable operation IDs
- emit deterministic receipts for “what bytes were used”

### Secrets (references only)

Hard rule:
- adapters never embed raw secrets in specs, logs, or published artifacts
- adapters refer to secrets by opaque references (`secret_ref` / templates)

Illustrative `secret_ref` shape:
```json
{
  "auth": {
    "type": "api_key",
    "secret_ref": "secrets://if.api/<tenant>/revive/api_key"
  }
}
```

Resolution model (design intent):
- `secret_ref` is a pointer string; the value lives in the operator secret store (not in this repo).
- Rotation should change the secret value without changing the `secret_ref`.
- Logging must redact any secret-shaped values (headers, query params) and should prefer stable identifiers (`adapter_id`, request id, and `secret_ref`).
- Receipts and `/llm` artifacts must never contain secrets; if they contain a `secret_ref`, it is still only a reference.

Secret resolution failure (fail closed):
- If `secret_ref` fails to resolve (missing, expired, revoked), fail closed (5xx/503) and log `adapter_id` + `secret_ref` (not the secret).
- Do not fall back to cached credentials; it creates a stale-auth window and hides rotation failures.

### Classification wall (`/llm` vs `/mcp`)

Rule:
- public evidence belongs in `/llm`
- tenant/NDA evidence belongs in `/mcp` (token-gated)

Reference:
https://infrafabric.io/llm/platform/mcp-integration/2026-01-25/index.md.txt

*If your adapter spec doesn’t name its guardrails, your incident report will.*

---

## 8b) Failure modes: drift, 429s, signature failures, spec conflicts

The happy path is the easy part. The adapter boundary exists so failure behavior is explicit and repeatable.

Failure decision tree (conceptual):
```mermaid
flowchart TD
  A["Request arrives"] --> B["Validate input + auth"]
  B -->|invalid| R400["Return 400 or 401"]
  B --> C["Call provider"]
  C -->|200 + expected shape| D["Normalize + enforce"]
  D --> E["Return canonical JSON"]
  C -->|429 or 503| F["Backoff + cache"]
  F -->|cache hit| E
  F -->|cache miss| G["Return safe fallback"]
  C -->|schema drift| H["Fallback + open incident"]
  B -->|webhook sig fail| R403["Reject 403"]

```

Specific failure modes to name in the adapter spec (and tests):
- **Provider API changes without notice**: treat as drift; fail closed where safety matters, otherwise return a minimal fallback and mark capability as `unknown` until re-pinned.
- **Rate limits hit mid-request** (`429`, `Retry-After`): prefer serving cached or degraded responses over retry storms; update observables to match reality.
- **Webhook signature validation fails**: reject and log (without leaking secrets); do not “accept anyway”.
- **Tenant context cannot be resolved**: fail closed (4xx); do not default to a shared tenant bucket.
- **Secret resolution fails** (`secret_ref` missing/expired/revoked): fail closed; do not “keep going” with stale cached auth.
- **Spec conflicts with observed behavior**: record both (spec vs observables), then either update the spec or implement a compensating rule, but don’t pretend it’s fine.

*If failure modes aren’t written down, they will be invented live during the outage.*

---

## 9) Ship checklist: making an integration reviewable

To ship an integration as reviewable (not just “exists”):

1) Register the product/module correctly (do not invent IDs).
2) Publish a minimal adapter spec (schemas + examples + boundaries).
3) Provide a runnable demo (no private infra required).
4) Bind outputs to `if.trace` receipts (brand `if.trace`).
5) Publish an HTML-first review surface with a plain-text fallback.

Registry and scaffolding entrypoints (one URL per line):
https://infrafabric.io/llm/if.registry.json.txt
https://infrafabric.io/llm/entrypoint.json.txt
https://infrafabric.io/llm/sot/index.json.txt

*If a reviewer can’t reproduce the demo, you didn’t ship a demo — you shipped an invitation to trust you.*

---

## 10) Verify checklist (boring on purpose)

Repo-local (no network):
```bash
python3 -m py_compile scripts/if_api_doi_resolver_demo.py
python3 -m py_compile scripts/if_api_swhid_resolver_demo.py
python3 -m py_compile scripts/if_api_ggq_ads_revive_adapter_poc.py
python3 -m py_compile scripts/if_blackboard.py
# expect: exit code 0
```

Registry + schemas (repo-local):
```bash
python3 -c 'import jsonschema, referencing'
# expect: exit code 0

python3 - <<'PY'
import json
reg=json.load(open('if.registry.json'))
products=reg.get('products') or []
entry=next((p for p in products if p.get('product_id')=='if.api'), None)
print((entry or {}).get('status',''))
print((entry or {}).get('path',''))
PY
# expect:
# preview
# /if/api

python3 - <<'PY'
import json
for p in [
  'schemas/if-api/adapter.schema.json',
  'schemas/if-api/adapter_spec.schema.json',
  'schemas/if-api/ggq_ads_json.schema.json',
  'schemas/if-api/ggq_revive_ads_adapter_spec.schema.json',
  'schemas/if-bus/envelope.schema.json',
  'schemas/if-bus/security_signal.schema.json',
]:
  json.load(open(p))
print('ok')
PY
# expect: ok

python3 - <<'PY'
import json
import jsonschema
from referencing import Registry, Resource
from referencing.jsonschema import DRAFT202012

adapter_schema=json.load(open('schemas/if-api/adapter.schema.json'))
spec_schema=json.load(open('schemas/if-api/adapter_spec.schema.json'))
revive_spec_schema=json.load(open('schemas/if-api/ggq_revive_ads_adapter_spec.schema.json'))

registry = Registry()
for s in [adapter_schema, spec_schema, revive_spec_schema]:
  schema_id = s.get("$id")
  if schema_id:
    registry = registry.with_resource(schema_id, Resource.from_contents(s, default_specification=DRAFT202012))

validator = jsonschema.Draft202012Validator(spec_schema, registry=registry, format_checker=jsonschema.FormatChecker())
spec={"spec_id":"if.api.example@0.1.0","adapter":{"adapter_id":"if.api.example","label":"Example adapter","version":"0.1.0"}}
validator.validate(spec)

validator2 = jsonschema.Draft202012Validator(revive_spec_schema, registry=registry, format_checker=jsonschema.FormatChecker())
revive_spec_instance=json.load(open('docs/data/if_api_adapter_spec.ggq_revive_ads.v1.json'))
validator2.validate(revive_spec_instance)

print("schema_ok")
print("ggq_revive_spec_ok")
PY
# expect:
# schema_ok
# ggq_revive_spec_ok
```

Public URLs (optional):
```bash
curl -fsSI https://infrafabric.io/static/hosted/review/if-api-github-webhook-demo/2026-01-05/index.html | head -n 5
# expect: HTTP/2 200

curl -fsSI https://infrafabric.io/static/trace/Z_kiBtTQHztidcgYYaSRy4UK | head -n 5
# expect: HTTP/2 200

curl -fsSI https://infrafabric.io/static/hosted/review/if-api-swhid-resolver-demo/2026-01-07/index.html | head -n 5
# expect: HTTP/2 200

curl -fsSI https://infrafabric.io/static/trace/gtwHpclpxuWVxvcRPDecFQ | head -n 5
# expect: HTTP/2 200

curl -fsSL --retry 5 --retry-delay 1 --connect-timeout 5 --max-time 15 'https://ww1.guidesgq.com/__backend/ggq/_v9/api/ads.json?page=article&cb=0' -o /tmp/if_api_ads.json
# expect: exit code 0

python3 - <<'PY'
import json, jsonschema

j=json.load(open('/tmp/if_api_ads.json'))
print(sorted(j.keys()))

required={'banner','destination_id','inline','locale','page','paves'}
missing=required - set(j.keys())
assert not missing, f"missing keys: {sorted(missing)}"
print('ads_keys_ok')

schema=json.load(open('schemas/if-api/ggq_ads_json.schema.json'))
jsonschema.Draft202012Validator(schema).validate(j)
print('ads_schema_ok')
PY
# expect:
# (prints sorted keys)
# ads_keys_ok
# ads_schema_ok
```

*If the verify steps are hand-wavy, the system will be too.*

---

## 11) Appendix (links, one URL per line)

Further reading (suggested order):

1) Orientation:
- `if.registry.json`
- `docs/32-if-api-integrations-inventory.md`

2) Boundaries + evidence surfaces:
- `docs/31-if-bus-whitepaper.md`
- `docs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md`

3) Public surfaces (no-login):
https://infrafabric.io/llm/products/if-api/
https://infrafabric.io/if/trace/
https://infrafabric.io/llm/blackboard/index.md.txt

if.api inventory (repo-local):
- docs/32-if-api-integrations-inventory.md

if.bus posture (repo-local):
- docs/205-if-bus-full-stack-whitepaper-v1.md
- docs/207-if-bus-2026-01-22-full.md

MCP boundary whitepaper (repo-local):
- docs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md
