|
| 1 | +# v3 reference seller |
| 2 | + |
| 3 | +Canonical multi-tenant AdCP seller. **Spec 3.0-compliant on the wire, |
| 4 | +3.1-ready in architecture and storage.** Adopters fork this directory |
| 5 | +and replace the platform impl with their own business logic. |
| 6 | + |
| 7 | +This directory wires every Tier 2 / v3-supporting component the SDK |
| 8 | +ships into one runnable binary: |
| 9 | + |
| 10 | +| Component | Module | Source | |
| 11 | +|---|---|---| |
| 12 | +| Tier 2 commercial-identity gate | `src/buyer_registry.py` | `adcp.decisioning.BuyerAgentRegistry` | |
| 13 | +| Subdomain tenant routing | `src/tenant_router.py` + `src/app.py` | `adcp.server.SubdomainTenantMiddleware` | |
| 14 | +| Account v3 storage (bank-details column) | `src/models.py` | `Account.billing_entity` JSON column | |
| 15 | +| Audit trail | `src/audit.py` | `adcp.audit_sink.AuditSink` | |
| 16 | +| MCP + A2A on one binary | `src/app.py` | `serve(transport="both", asgi_middleware=...)` | |
| 17 | +| Durable HITL tasks (optional) | swap to `PgTaskRegistry` | `adcp.decisioning.pg.PgTaskRegistry` | |
| 18 | +| Durable webhook delivery (optional) | swap to `PgWebhookDeliverySupervisor` | `adcp.webhook_supervisor_pg` | |
| 19 | +| HTTP-Sig verifier → AuthInfo (TODO) | adopter middleware | `adcp.decisioning.AuthInfo.from_verified_signer` | |
| 20 | +| Account v3 projection on read (TODO) | adopter wires in `sync_accounts` | `adcp.types.project_account_for_response` | |
| 21 | + |
| 22 | +## Run it |
| 23 | + |
| 24 | +```bash |
| 25 | +# 1. Start Postgres |
| 26 | +cd examples/v3_reference_seller |
| 27 | +docker compose up -d postgres |
| 28 | + |
| 29 | +# 2. Seed dev fixtures |
| 30 | +DATABASE_URL=postgresql+asyncpg://postgres@localhost/adcp \ |
| 31 | + python -m seed |
| 32 | + |
| 33 | +# 3. Boot the seller |
| 34 | +DATABASE_URL=postgresql+asyncpg://postgres@localhost/adcp \ |
| 35 | + python -m src.app |
| 36 | +``` |
| 37 | + |
| 38 | +The server binds `0.0.0.0:3001` and serves both transports. |
| 39 | + |
| 40 | +> ⚠️ **Local-dev only.** `docker-compose.yml` uses |
| 41 | +> `POSTGRES_HOST_AUTH_METHOD=trust` and exposes 5432 on |
| 42 | +> `0.0.0.0`. Do not run this compose file on a host reachable from |
| 43 | +> an untrusted network. `seed.py` plants a literal dev bearer |
| 44 | +> token (`dev-bearer-token-acme-1`) — do not run `seed.py` against |
| 45 | +> a production `DATABASE_URL`. Production deployments point |
| 46 | +> `DATABASE_URL` at managed Postgres with scram-sha-256 + a real |
| 47 | +> password, and seed via the admin API (not this script). |
| 48 | +
|
| 49 | +## What's wired |
| 50 | + |
| 51 | +### Schema (`src/models.py`) |
| 52 | + |
| 53 | +Four tables — the spine of a multi-tenant v3 seller: |
| 54 | + |
| 55 | +- `tenants` — one row per `<subdomain>.example.com`. |
| 56 | + `SubdomainTenantMiddleware` reads the request `Host` header and |
| 57 | + finds the matching row. |
| 58 | +- `buyer_agents` — Tier 2 commercial-identity rows. The framework's |
| 59 | + dispatch gate reads this BEFORE the platform method runs and |
| 60 | + rejects suspended (transient) and blocked (terminal) agents with |
| 61 | + structured errors. |
| 62 | +- `accounts` — buyer-side accounts under recognized agents. Carries |
| 63 | + the spec 3.1-ready `billing_entity` (write-only bank details on |
| 64 | + responses) and `reporting_bucket` (offline reporting target). The |
| 65 | + reference seller does not implement `sync_accounts`, so the |
| 66 | + bank-details projection is a column-level architectural seam, not |
| 67 | + an enforced runtime guard — adopters who add `sync_accounts` |
| 68 | + MUST project through `adcp.types.project_account_for_response` |
| 69 | + before returning the row. |
| 70 | +- `media_buys` — terminal artifact of `create_media_buy`, |
| 71 | + idempotency-keyed for replay safety. |
| 72 | + |
| 73 | +### Tenant routing (`src/tenant_router.py`) |
| 74 | + |
| 75 | +`SqlSubdomainTenantRouter` implements the framework's |
| 76 | +`SubdomainTenantRouter` Protocol. The middleware sets the |
| 77 | +`current_tenant()` contextvar; downstream stores |
| 78 | +(`buyer_registry.py`, `platform.py`) read it without explicit |
| 79 | +plumbing. |
| 80 | + |
| 81 | +### Commercial-identity gate (`src/buyer_registry.py`) |
| 82 | + |
| 83 | +`TenantScopedBuyerAgentRegistry` extends the framework's |
| 84 | +`PgBuyerAgentRegistry` pattern with tenant scoping — the same |
| 85 | +`agent_url` can have different commercial postures across tenants. |
| 86 | +Implements both `resolve_by_agent_url` (signed traffic) and |
| 87 | +`resolve_by_credential` (bearer / OAuth). |
| 88 | + |
| 89 | +### Audit trail (`src/audit.py`) |
| 90 | + |
| 91 | +`DbAuditSink` writes one `audit_events` row per skill dispatch. |
| 92 | +Failures are swallowed by the framework's audit middleware (the |
| 93 | +sink is fire-and-forget by Protocol contract). Adopters with Slack |
| 94 | +alerting compose with `adcp.audit_sink.SlackAlertSink` via |
| 95 | +`CompositeAuditSink`. |
| 96 | + |
| 97 | +### Platform (`src/platform.py`) |
| 98 | + |
| 99 | +`V3ReferenceSeller` implements `sales-non-guaranteed` — the five |
| 100 | +required Sales methods (`get_products`, `create_media_buy`, |
| 101 | +`update_media_buy`, `sync_creatives`, `get_media_buy_delivery`). |
| 102 | +Every method body reads `ctx.buyer_agent` (the resolved Tier 2 |
| 103 | +record) and `ctx.account` (the resolved account); both are |
| 104 | +populated by the framework's dispatch gate before the method runs. |
| 105 | + |
| 106 | +This file is the bulk of what an adopter customizes. Everything |
| 107 | +else is boilerplate the seller wires once. |
| 108 | + |
| 109 | +## Auth modes |
| 110 | + |
| 111 | +The seller supports both v3 signed-request and pre-trust beta |
| 112 | +bearer auth simultaneously — the `BuyerAgentRegistry` dispatches |
| 113 | +on credential kind. Adopter middleware constructs `AuthInfo` two |
| 114 | +ways: |
| 115 | + |
| 116 | +```python |
| 117 | +# Signed (v3) — produces typed HttpSigCredential |
| 118 | +auth = AuthInfo.from_verified_signer( |
| 119 | + signer, # from adcp.signing.verify_request_signature |
| 120 | + max_verified_age_s=300.0, # reject stale signers |
| 121 | +) |
| 122 | + |
| 123 | +# Bearer (pre-trust beta) — typed ApiKeyCredential |
| 124 | +auth = AuthInfo( |
| 125 | + kind="bearer", |
| 126 | + credential=ApiKeyCredential(kind="api_key", key_id=token_id), |
| 127 | +) |
| 128 | +``` |
| 129 | + |
| 130 | +Both put the typed credential into `ctx.metadata['adcp.auth_info']` |
| 131 | +where the framework picks it up. |
| 132 | + |
| 133 | +## What's NOT wired (yet) |
| 134 | + |
| 135 | +These ship as separable follow-ups — the framework's components |
| 136 | +exist; the reference seller wires the simpler defaults: |
| 137 | + |
| 138 | +- **HTTP-Sig verifier middleware** — adopters add |
| 139 | + `verify_request_signature` in their `context_factory` once |
| 140 | + AAO publishes the brand.json registry. The Tier 1 SDK primitives |
| 141 | + ship in `adcp.signing`; this seller uses bearer auth in the seed. |
| 142 | +- **Brand authorization (Tier 3)** — gated on ADCP spec issue |
| 143 | + #3690. |
| 144 | +- **Postgres `TaskRegistry` / `WebhookDeliverySupervisor`** — |
| 145 | + swap `InMemoryTaskRegistry` → `PgTaskRegistry` and |
| 146 | + `InMemoryWebhookDeliverySupervisor` → `PgWebhookDeliverySupervisor` |
| 147 | + in `src/app.py` for production durability. Both classes ship in |
| 148 | + the SDK; this seller's `app.py` uses the in-memory variants for |
| 149 | + fast iteration. |
| 150 | +- **Alembic migrations** — `Base.metadata.create_all` runs at boot |
| 151 | + (idempotent on table existence — it does NOT detect column |
| 152 | + renames or type changes on existing tables). Adopters who |
| 153 | + prototyped against earlier branches and pulled new column |
| 154 | + changes should drop and recreate the dev database; production |
| 155 | + sellers wire Alembic and version their schema changes. |
| 156 | +- **Admin CRUD API** — separate Starlette app for tenant / agent |
| 157 | + CRUD. Patterns to come; for now use `seed.py` and direct SQL. |
| 158 | + |
| 159 | +## Customization |
| 160 | + |
| 161 | +Adopters typically change: |
| 162 | + |
| 163 | +1. **`src/platform.py`** — the platform method bodies. Replace the |
| 164 | + stub product catalog, add your CMS query for `get_products`, |
| 165 | + route `create_media_buy` into your real DSP / ad-server, etc. |
| 166 | +2. **`src/audit.py`** — extend `details` with adopter-specific |
| 167 | + fields (decision flags, fraud scores, A/B variant ids). |
| 168 | +3. **Auth wiring in `src/app.py`** — wire your verifier middleware |
| 169 | + that constructs `AuthInfo`. |
| 170 | + |
| 171 | +Adopters typically *don't* change: |
| 172 | + |
| 173 | +- Models — the v3 schema is the contract. |
| 174 | +- Tenant router logic — the Protocol shape is fixed. |
| 175 | +- Audit middleware composition — the framework wires it. |
| 176 | +- The unified MCP+A2A binary — `transport="both"` is one knob. |
| 177 | + |
| 178 | +## Spec versioning |
| 179 | + |
| 180 | +This seller is **3.0-compliant on the wire** — every field it sends |
| 181 | +matches the AdCP 3.0 schemas. The schema and architecture is |
| 182 | +**3.1-ready** (`billing_entity` + `reporting_bucket` columns, |
| 183 | +typed `BillingMode`, write-only bank-details projection). Sellers |
| 184 | +running this code today serve 3.0 buyers; the same code serves |
| 185 | +3.1 buyers when the spec lands. |
0 commit comments