The Prebid Sales Agent follows a layered architecture that cleanly separates protocol handling, business logic, and ad server integrations. This page describes the system design, protocol comparison, multi-tenancy model, database schema, adapter pattern, and authentication layers.
The system is organized into four distinct layers. Each layer has a single responsibility and communicates only with its immediate neighbors.
┌──────────────────────────────────────────────────────────────────┐
│ Layer 1: Admin UI (Flask) │
│ Web dashboard, SSE activity feed, approval queue │
└──────────────────────────┬───────────────────────────────────────┘
│
┌──────────────────────────┴───────────────────────────────────────┐
│ Layer 2: MCP / A2A Protocol Servers │
│ FastMCP (HTTP/SSE) at /mcp/ │ JSON-RPC 2.0 at /a2a │
│ Tool registration & routing │ Agent card discovery │
└──────────────────────────┬───────────────────────────────────────┘
│
┌──────────────────────────┴───────────────────────────────────────┐
│ Layer 3: Service Layer │
│ ProductService │ MediaBuyService │ CreativeService │
│ PrincipalService │ ReportingService │ AuditService │
│ ToolContext (tenant isolation) │
└──────────────┬───────────────────┬───────────────────────────────┘
│ │
┌──────────────┴──────┐ ┌────────┴───────────────────────────────┐
│ Layer 4: Adapters │ │ Layer 4: Database │
│ GAMAdapter │ │ PostgreSQL + SQLAlchemy ORM │
│ MockAdapter │ │ Alembic auto-migrations │
│ (future adapters) │ │ Composite unique constraints │
└─────────────────────┘ └────────────────────────────────────────┘
| Layer | Component | Responsibility |
|---|---|---|
| 1. Admin UI | Flask web app | Dashboard, approval queue, SSE activity feed, OAuth login |
| 2. Protocol | FastMCP server | MCP tool registration, HTTP/SSE transport, token auth |
| 2. Protocol | A2A server | JSON-RPC 2.0 endpoint, agent card at /.well-known/agent.json |
| 3. Services | Business logic | Validation, orchestration, tenant isolation via ToolContext |
| 4. Adapters | Ad server plugins | Translate service calls into ad server API operations |
| 4. Database | PostgreSQL | Persistent storage with SQLAlchemy ORM and Alembic migrations |
Every AI agent request follows the same path through the system, regardless of whether it arrives via MCP or A2A.
AI Agent (Claude, GPT, custom)
│
│ HTTP POST with auth token
▼
┌─────────────────────────────┐
│ Nginx Reverse Proxy (:8000)│
│ Routes /mcp/ or /a2a │
└──────────┬──────────────────┘
│
▼
┌─────────────────────────────┐
│ MCP Server (:8080) │ ┌─────────────────────────────┐
│ or A2A Server │──▶│ ToolContext │
│ (protocol parsing) │ │ - tenant_id │
└──────────┬──────────────────┘ │ - principal_id │
│ │ - db_session │
▼ │ - adapter instance │
┌─────────────────────────────┐ └─────────────────────────────┘
│ Service Layer │◀─── uses ToolContext for isolation
│ (business logic) │
└──────────┬──────────────────┘
│
┌─────┴─────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ Database │ │ Ad Server │
│ (CRUD) │ │ Adapter │
└──────────┘ │ (GAM, Mock) │
└──────────────┘
x-adcp-auth header or A2A Authorization: Bearer header) and resolves it to a principal and tenant.ToolContext object is constructed containing the tenant_id, principal_id, database session, and the correct ad server adapter for that tenant.The Sales Agent exposes identical business functionality through both protocols. The choice between them depends on your integration pattern.
| Aspect | MCP (Model Context Protocol) | A2A (Agent-to-Agent Protocol) |
|---|---|---|
| Transport | HTTP with SSE (Server-Sent Events) | HTTP with JSON-RPC 2.0 |
| Endpoint | /mcp/ |
/a2a |
| Authentication | x-adcp-auth header token |
Authorization: Bearer header |
| Discovery | Tool listing via list_tools |
Agent card at /.well-known/agent.json |
| Streaming | Native SSE streaming | JSON-RPC response batching |
| Best for | AI assistants (Claude, GPT) that natively support MCP | Multi-agent orchestration systems |
| Agent card | Not applicable | Published at /.well-known/agent.json with capabilities |
| Library | FastMCP Python SDK | Custom JSON-RPC handler |
Use MCP when your AI assistant natively supports the Model Context Protocol. This is the primary interface and provides the most natural integration for single-agent workflows:
# Connect Claude Desktop to the Sales Agent
uvx adcp http://localhost:8000/mcp/ --auth your-token list_tools
Use A2A when building multi-agent systems where agents need to discover and communicate with each other programmatically. A2A provides structured agent discovery via the agent card:
# Discover agent capabilities
curl http://localhost:8000/.well-known/agent.json
# Send a JSON-RPC request
curl -X POST http://localhost:8000/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{"jsonrpc":"2.0","method":"get_products","params":{"brief":"video"},"id":1}'
Both protocols expose the same set of tools and return identical data structures. See the Tool Reference for the complete catalog of available operations. For a deeper comparison of protocol design philosophies, see the AdCP Protocol Comparison.
The Sales Agent is designed for multi-tenant operation, where each publisher operates in complete isolation from others.
Every incoming request is resolved to a specific tenant. The ToolContext object carries this tenant scope through the entire request lifecycle:
class ToolContext:
tenant_id: int # Resolved from auth token
principal_id: int # The authenticated agent/user
db_session: Session # SQLAlchemy session
adapter: AdServerAdapter # Tenant's configured ad server adapter
All database queries are automatically scoped by tenant_id. A principal (AI agent or human user) in Tenant A can never see or modify data belonging to Tenant B.
Principals (agents and users) are identified by a composite unique constraint of (tenant_id, email). This means the same email address can exist as separate principals in different tenants, each with independent permissions and audit histories.
┌──────────────────────────────────────────────────────┐
│ Tenants │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Publisher A │ │ Publisher B │ │ Publisher C │ │
│ │ │ │ │ │ │ │
│ │ Principals │ │ Principals │ │ Principals │ │
│ │ Products │ │ Products │ │ Products │ │
│ │ Media Buys │ │ Media Buys │ │ Media Buys │ │
│ │ Creatives │ │ Creatives │ │ Creatives │ │
│ │ Audit Logs │ │ Audit Logs │ │ Audit Logs │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Complete data isolation per tenant │
└──────────────────────────────────────────────────────┘
The Sales Agent uses PostgreSQL as its primary data store, with SQLAlchemy ORM for data access and Alembic for schema migrations.
| Component | Technology | Purpose |
|---|---|---|
| Database | PostgreSQL | ACID-compliant relational storage |
| ORM | SQLAlchemy 2.x | Python object-relational mapping |
| Migrations | Alembic | Auto-generated schema migrations |
| Connection | Connection pooling | Managed by SQLAlchemy engine |
Alembic is configured in auto-generate mode. When SQLAlchemy models change, Alembic detects the differences and creates migration scripts automatically:
# Generate a new migration after model changes
alembic revision --autogenerate -m "add new column"
# Apply pending migrations
alembic upgrade head
Migrations run automatically on container startup in Docker deployments.
| Table | Purpose | Key Fields |
|---|---|---|
| tenants | Publisher accounts | id, name, slug, ad_server_type, config |
| principals | AI agents and human users | id, tenant_id, email, name, token_hash, role |
| products | Advertising inventory items | id, tenant_id, name, description, pricing, targeting |
| media_buys | Campaign orders from buyers | id, tenant_id, principal_id, product_id, budget, status, flight_dates |
| creatives | Ad creative assets | id, tenant_id, media_buy_id, format, content, approval_status |
| audit_logs | Complete operational history | id, tenant_id, principal_id, action, entity_type, entity_id, details, timestamp |
tenants
│
├── principals (many) ── composite unique: (tenant_id, email)
│ │
│ └── media_buys (many)
│ │
│ └── creatives (many)
│
├── products (many)
│ │
│ └── media_buys (many) ── references product_id
│
└── audit_logs (many) ── references principal_id, entity_type, entity_id
All tables include tenant_id as a foreign key to enforce data isolation at the database level.
The Sales Agent uses the adapter pattern to support multiple ad server backends. Each adapter implements the AdServerAdapter abstract base class, providing a uniform interface regardless of the underlying ad server.
class AdServerAdapter(ABC):
"""Abstract base class for ad server integrations."""
@abstractmethod
async def create_order(self, media_buy: MediaBuy) -> str:
"""Create an order in the ad server. Returns external order ID."""
@abstractmethod
async def create_line_item(self, media_buy: MediaBuy, order_id: str) -> str:
"""Create a line item under an order. Returns external line item ID."""
@abstractmethod
async def upload_creative(self, creative: Creative) -> str:
"""Upload a creative asset. Returns external creative ID."""
@abstractmethod
async def get_delivery_report(self, order_id: str) -> DeliveryReport:
"""Fetch delivery metrics for an order."""
@abstractmethod
async def sync_creative(self, creative: Creative) -> str:
"""Sync creative status from the ad server."""
| Adapter | Class | Purpose |
|---|---|---|
| Google Ad Manager | GAMAdapter |
Production integration with Google Ad Manager API |
| Mock | MockAdapter |
Development and testing with simulated responses |
To support a new ad server, create a class that implements AdServerAdapter:
xandr_adapter.py).AdServerAdapter.ad_server_type field.For detailed source code organization and module-level documentation, see Source Architecture.
The Docker deployment exposes services on the following ports:
| Port | Service | Description |
|---|---|---|
| 8000 | Nginx reverse proxy | Public entry point; routes to internal services |
| 8080 | MCP Server (FastMCP) | Primary AI agent interface via HTTP/SSE |
| 8001 | Admin UI (Flask) | Web dashboard and approval workflows |
| 8091 | A2A Server (alternate) | Direct A2A access when not using the proxy |
In production, all traffic enters through the Nginx proxy on port 8000, which routes requests based on path:
http://localhost:8000
│
├── /mcp/* ──▶ MCP Server (:8080)
├── /a2a ──▶ A2A Server (:8080)
├── /admin/* ──▶ Admin UI (:8001)
├── /health ──▶ Health endpoint
└── /.well-known/agent.json ──▶ A2A agent card
For local development without Docker, services can be started individually on their native ports. See the Quick Start guide for details.
The Sales Agent implements multiple authentication mechanisms depending on the access context.
| Layer | Method | Scope | Use Case |
|---|---|---|---|
| Super Admin | SUPER_ADMIN_TOKEN env var |
System-wide | Tenant provisioning, system configuration |
| OAuth | Google OAuth 2.0 | Admin UI | Human publishers accessing the dashboard |
| Tenant Admin | Token-based | Per-tenant | Publisher admin operations via API |
| Principal Token | x-adcp-auth or Bearer header |
Per-tenant | AI agent authentication for MCP/A2A |
Incoming Request
│
├── Has x-adcp-auth header?
│ └── Yes ──▶ Look up principal by token hash
│ └── Resolve tenant_id, principal_id
│
├── Has Authorization: Bearer header?
│ └── Yes ──▶ Check if super admin token
│ ├── Yes ──▶ Super admin context
│ └── No ──▶ Look up principal by token hash
│ └── Resolve tenant_id, principal_id
│
└── Has OAuth session cookie?
└── Yes ──▶ Resolve from session
└── Admin UI context
Principal tokens are stored as hashed values in the database. The raw token is only shown once at creation time. Each principal can have multiple tokens for rotation without downtime.
For complete security documentation, including credential management, network isolation, and production hardening, see Security & Operations.
The Sales Agent integrates Google Gemini for intelligent product discovery, enabling AI agents to search advertising inventory using natural language.
When an AI agent calls get_products with a natural language query, the service uses Gemini to match the query against the tenant’s product catalog:
AI Agent: "Find video advertising products for sports content"
│
▼
┌─────────────────────────────┐
│ ProductService.get_products│
│ │
│ 1. Load tenant's products │
│ 2. Send to Gemini with │
│ query context │
│ 3. Gemini ranks and │
│ filters matches │
│ 4. Return scored results │
└─────────────────────────────┘
│
▼
Matched products with relevance scores
Gemini integration requires a Google AI API key set in the environment:
# docker-compose.yml excerpt
environment:
- GEMINI_API_KEY=your-api-key
When no API key is configured, product discovery falls back to keyword-based matching against product names and descriptions.
Product discovery is one of several tools available to AI agents. See the Tool Reference for the complete list of operations including media buy management, creative handling, and reporting.