Skip to content

feat(examples): multi-platform-per-process proof — mock platforms with PlatformRouter, refactor v3 reference seller along the way #477

Description

@bokelley

Context

The SDK ships DecisioningPlatform + specialism mixins as the platform contract. Adopters subclass once per platform, declare capabilities, implement specialism methods. Today we have one worked example — examples/v3_reference_seller/ — which assumes one platform per process, one tenant per platform (the upstream HTTP client is held on __init__).

Multi-tenant SaaS sales agents (e.g. Prebid's salesagent) need the opposite shape: N platforms per process, dispatched per tenant. That pattern doesn't have a worked example in the SDK today, even though DecisioningPlatform supports it cleanly via composition.

This issue (a) builds the worked example with two pure-Python in-process mock platforms, (b) ships the PlatformRouter recipe, and (c) refactors the v3 reference seller to use the same pattern as a degenerate single-platform case so we have one consistent way to compose platforms.

Goal

Stand up examples/multi_platform_seller/ that:

  1. Boots a single serve() process.
  2. Hosts two tenants, each backed by a different DecisioningPlatform subclass.
  3. Passes the media_buy_seller storyboard for both tenants.
  4. Doubles as the migration target for salesagent-style adapter-registry architectures.

Non-goals

  • Not adding new SDK runtime — the PlatformRouter lands as an example recipe first. If the pattern proves out, we promote it into adcp.decisioning.dispatch in a follow-up.
  • Not pulling in the JS mock-server. These are pure-Python in-process platforms — easier to read, easier to test, easier to fork.
  • Not adding new platform specialisms — both mocks claim sales-non-guaranteed and sales-guaranteed. Multi-protocol bridging is a separate seam.

Scope

1. MockGuaranteedPlatform(DecisioningPlatform, SalesPlatform)

In-process, fixed-allocation. Models pre-bookable inventory: create_media_buy reserves capacity from a finite pool; rejects when sold out. Status lifecycle exercises pending_creatives → pending_start → active → completed. Pricing fixed CPM.

2. MockNonGuaranteedPlatform(DecisioningPlatform, SalesPlatform)

In-process, auction-time allocation. Models programmatic remnant: create_media_buy always succeeds, delivery scales with budget, status starts active. Pricing variable CPM with floor.

3. PlatformRouter

A DecisioningPlatform subclass that holds a dict[tenant_id, DecisioningPlatform] and delegates each method to the resolved platform. Wired via tenant ResolveContext. Minimal — fewer than 200 LOC.

router = PlatformRouter({
    "tenant-a": MockGuaranteedPlatform(...),
    "tenant-b": MockNonGuaranteedPlatform(...),
})
serve(router, transport="both", asgi_middleware=[SubdomainTenantMiddleware(...)])

4. Storyboard validation

media_buy_seller storyboard passes for both tenants. Tested via the existing storyboard runner with separate subdomain → tenant routing.

5. README + migration doc

examples/multi_platform_seller/MIGRATION_FROM_ADAPTER_REGISTRY.md — explicit translation table from salesagent's ADAPTER_REGISTRY / AdServerAdapter model to PlatformRouter + DecisioningPlatform + SalesPlatform.

6. Refactor examples/v3_reference_seller/ (the bundled work)

The reference seller currently has its own ad-hoc tenant routing logic. Refactor to:

  • Use PlatformRouter as the entry point even though it has only one platform — the degenerate case is the migration target for new adopters who'll add platforms later.
  • Move the upstream HTTP client into the platform's __init__ (already there) but wire the router's tenant lookup consistently with the new examples.
  • Update MIGRATION.md to reference the multi-platform doc as the next step.

This refactor is unavoidable once we ship PlatformRouter — the reference seller becomes inconsistent if we don't update it. Bundling keeps the docs coherent.

Acceptance criteria

  • examples/multi_platform_seller/ directory exists with the two mock platforms + router + README
  • Storyboard run (@adcp/client adcp storyboard run ...) passes for both tenant subdomains
  • MIGRATION_FROM_ADAPTER_REGISTRY.md documents the translation from salesagent-shaped adapter code
  • examples/v3_reference_seller/ uses PlatformRouter (degenerate single-platform case)
  • examples/v3_reference_seller/MIGRATION.md updated to point at the multi-platform example
  • CI: new pytest integration test that boots multi_platform_seller and runs at least one tool against each tenant
  • No regressions in existing reference-seller tests

Out of scope (separate issues)

  • Promoting PlatformRouter into adcp.decisioning.dispatch (do this after the example proves out)
  • Capability projection completeness (separate PR — see corresponding issue)
  • Multi-protocol buyer bridge — speculative, hold until needed
  • Performance / scale testing of the router — single-process latency is fine for the example

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions