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.mddocs/231-if-api-deep-dive-whitepaper-2026-02-01.md(v1.0)
Errata (v1.0 → v1.0.1):
- Correct
if.busposture:if.busispreview(notroadmap) 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.apiruntime service on this host;if.apiisstatus=previewinif.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
-
- Executive summary (one screen)
- 0b) Ecosystem primer (for first-time readers)
-
- What
if.apiis (and is not)
- What
-
- The adapter boundary (why it exists)
- 2b) Normalization example (before/after)
-
- Contracts:
if.api.adapterandif.api.adapter_spec
- Contracts:
- 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.apisits relative toif.bus,if.trace,/llm,/mcp - 4b) Cross-cutting:
if.securityposture - 4c) Composition:
if.api→if.api+ (optional)if.busenvelopes - 4d) Agent/LLM messages: structured requests (no vibes)
-
- Example 1: GitHub webhook normalization (demo)
-
- Example 2: DOI resolver (demo)
- 6b) Example 2b: Resolver dispatch/chaining (
if.api→if.api) -
- Example 3: Ad decisioning (Revive Adserver) (proposed)
-
- Guardrails: rate limits, caching, idempotency, secrets, classification
- 8b) Failure modes: drift, 429s, signature failures, spec conflicts
-
- Ship checklist: making an integration reviewable
-
- Verify checklist (boring on purpose)
-
- 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.tracereceipts (brandif.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.apiis 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, theirstatus, and preferredpathshapes.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.apiregistry entry:if.registry.json(status=preview,path=/if/api)if.apicontract schemas:schemas/if-api/adapter.schema.jsonschemas/if-api/adapter_spec.schema.json
if.apipublic 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:
- Normalize at the edge: convert provider-specific shapes into canonical shapes immediately.
- Make claims testable: every capability claim should be true/false/unknown, with evidence pins where possible.
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):
{
"id": 123,
"createdAt": "2026-01-31T12:34:56Z",
"url": "https://example.com/x?y=1",
"extra": {"weird": true}
}
Illustrative “after” (canonical contract):
{
"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”:
if.api.adapter(metadata)
- identity (
adapter_id,label,version) - provider metadata (
provider,base_url) - auth type (never raw secrets)
- high-level capabilities
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.jsonschemas/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
-
Define scope (and the explicit “not” list).
-
Choose a versioning strategy:
- Recommend SemVer for
adapter.version(MAJOR.MINOR.PATCH). - Breaking change policy: bump
MAJORwhen the output contract changes shape/meaning; keep the previous major runnable until consumers migrate.
- 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.
- 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.
- Plan for evolution:
- Prefer additive changes (new fields; never re-meaning old ones).
- If you must break shape/type, introduce a new
event_typeor a new adapter major version; do not silently mutate existing outputs.
- Multi-tenant discipline (when
tenant_idexists):
- Treat
tenant_idas a first-class partition key for rate limits, caches, and secrets. - Never let one tenant’s spikes or failures consume another tenant’s budgets.
- Make it reviewable:
- Provide a runnable demo +
if.tracereceipts + 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):
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.apiispreview) - 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_idand contract major in cache keys) - secret resolution (
secret_refincludes a tenant segment) - receipts and published artifacts (public
/llmvs 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_idcannot be resolved before the provider call, stop and return a 4xx; do not fall back to a shared/default tenant bucket. - Derive
tenant_idfrom 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_typefor 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
/llminstead of/mcp.
Evidence anti-patterns:
- Treating
if.tracereceipts 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.apiispreview(registry); contracts + demos exist.if.busispreview(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.tracereceipts, and publish on/llm(public) or/mcp(tenant/NDA) based on classification.
Public docs (black/white posture):
docs/30-if-bus-and-api-integrations.mddocs/31-if-bus-whitepaper.mddocs/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.securityisroadmapinif.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
/llmpublic-safe: any tenant payloads or NDA examples go to/mcp.
Security signal flow (conceptual):
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):
{
"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.busenvelope for async delivery and retries.
Envelopes (important distinction):
if.apiproduces canonical JSON payloads (your contract).if.buswraps 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):
{
"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):
{
"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_specconstraints 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):
{
"ref": "refs/heads/main",
"after": "e7c1…",
"repository": {"full_name": "org/repo"},
"pusher": {"name": "alice"}
}
Minimal normalized output shape (illustrative):
{
"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:
{
"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:
{
"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.jsonresponse 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.apiadapter (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>, optionaldestination_id=<int>, optionallocale=<fr|en> - Output (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.apiadapter talks to Revive (provider reality) and emits the stableads.jsoncontract (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
nulland 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_iddoes not need to appear in the publicads.jsonresponse, but it must partition secret refs, rate limits, caches, and rotation state.
Slot enforcement snippet (illustrative; fail closed on layout safety):
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:
{
"auth": {
"type": "api_key",
"secret_ref": "secrets://if.api/<tenant>/revive/api_key"
}
}
Resolution model (design intent):
secret_refis 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, andsecret_ref). - Receipts and
/llmartifacts must never contain secrets; if they contain asecret_ref, it is still only a reference.
Secret resolution failure (fail closed):
- If
secret_reffails to resolve (missing, expired, revoked), fail closed (5xx/503) and logadapter_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):
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
unknownuntil 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_refmissing/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”):
- Register the product/module correctly (do not invent IDs).
- Publish a minimal adapter spec (schemas + examples + boundaries).
- Provide a runnable demo (no private infra required).
- Bind outputs to
if.tracereceipts (brandif.trace). - 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):
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):
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):
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):
- Orientation:
if.registry.jsondocs/32-if-api-integrations-inventory.md
- Boundaries + evidence surfaces:
docs/31-if-bus-whitepaper.mddocs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md
- 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