Draft spec, v0.1 Author: Amir Rad Status: Working draft, not for implementation
Direct deals between advertisers and publishers (programmatic guaranteed, preferred deals, sponsorships, IO-based direct sold, content integrations) move through a patchwork of proprietary ad-server interfaces, manual insertion-order PDFs, per-platform deal IDs, and reconciliation spreadsheets. There is no open, vendor-neutral protocol for the deal lifecycle itself: discovery, proposal, acceptance, activation, delivery, reporting, settlement.
The result: every advertiser-publisher pairing requires a per-platform integration. Premium publishers run multiple ad servers because their direct-sold workflow is locked to GAM, their PG workflow runs through SSPs, and their sponsorships are trafficked manually. Buyers maintain N integrations against N seller stacks. Reconciliation is a quarterly accounting exercise. Disputes are settled by email.
Real-time bidding has OpenRTB, but RTB is the auction layer. The deal layer above it has no equivalent open standard.
Define an open, vendor-neutral protocol for the end-to-end deal lifecycle between any buyer (advertiser, agency, DSP, buying co-op) and any seller (publisher, SSP, ad network, affiliate network). Any buyer can propose. Any seller can accept. Negotiation, activation, delivery reporting, and settlement are wire-defined and cryptographically signed. No central registry. No required intermediary. The protocol composes with OpenRTB for the auction layer and with conventional ad-server stacks for serving, but neither is required.
The bet: if the wire format is good enough, ad-tech vendors can build serving infrastructure that operates entirely against ODP, and the publisher and advertiser stacks converge from "proprietary configuration in N tools" to "a signed deal on file."
This spec covers a new deal type, Open Deals (OD), which generalizes programmatic guaranteed, preferred deals, and direct/IO-style deals into one protocol shape.
OpenRTB carries deal.id on bid requests/responses to let buyers signal participation in a pre-negotiated deal during the auction. ODP is complementary: an Open Deal MAY carry an OpenRTB-compatible deal.id so that legacy bid streams recognize it. ODP defines the lifecycle around that ID (how the deal got proposed, accepted, activated, reported, settled), which OpenRTB does not.
OpenDirect (2017) attempted to standardize an API for automating direct-sold orders. It defined RFP, proposal, and IO workflows but never gained traction: it predated PG normalization, didn't include cryptographic authentication, used OAuth bearer tokens for auth, and required all major SSPs and DSPs to implement before any single publisher saw value. ODP is a redo with a federated authentication model, broader deal-type coverage, and cryptographic settlement primitives.
PG today runs on top of OpenRTB and SSPs: deals are negotiated bilaterally (often by humans), then trafficked into the seller's SSP and the buyer's DSP, then delivered through normal RTB with the deal ID matching. ODP can replace the SSP and DSP middleware for PG entirely (the deal lives as a signed record on both sides; serving is direct), or sit alongside it (deal lifecycle on ODP, delivery still through OpenRTB). Either composition is permitted.
Prebid is publisher-side auction orchestration. ODP is deal-layer orchestration. They do not overlap. A Prebid-using publisher can also be an ODP seller; ODP delivery does not pass through Prebid.
OCP is the sister-spec for post-click conversion attribution. The two compose well (deal delivers a click, click drives a conversion, OCP fires back to the seller), but neither requires the other. ODP and OCP share design DNA: federated trust via well-known files, signed requests, no central registry. See Open questions for the question of whether they should share a single well-known file.
AdCP is an open agent-orchestration protocol for advertising workflows: AI agents discover inventory, refine briefs in natural language, generate creative, execute media buys, and report performance. It defines around twenty task verbs (get_products, create_media_buy, sync_creatives, provide_performance_feedback, etc.) over MCP and Agent-to-Agent transports, with RFC 9421 HTTP Message Signatures and JWKS-based key publication.
ODP is complementary to AdCP, not competing. AdCP defines how agents collaborate to make a buy; ODP defines the formal cryptographic contract that the negotiation produces. Specifically:
| AdCP surface | ODP surface | Relationship |
|---|---|---|
get_products |
Inventory descriptors in the seller's well-known file | AdCP queries dynamically with natural language; ODP publishes statically and structurally. Both can coexist. |
create_media_buy |
Deal proposal + signed acceptance | AdCP's media buy execution materializes as an ODP-signed contract record, giving both sides a cryptographic artifact of agreed terms. |
update_media_buy |
State transitions (paused, terminated, counter-amendment) |
AdCP triggers; ODP records. |
get_media_buy_delivery |
Delivery records | AdCP polls aggregate metrics; ODP delivers signed periodic batches buyers can verify and store. |
log_event |
(delegates to OCP) | Both protocols delegate conversion attribution to OCP or equivalent. |
| (none) | Settlement attestation | ODP fills the gap. |
adagents.json |
authorized_agents in the well-known file |
Same concept, expressed differently. See Open questions. |
| RFC 9421 + JWKS + JWS | Ed25519 over raw body + custom keys array |
v0.1 ODP uses simpler primitives. Alignment with AdCP's choices is a candidate for v1.0. See Open questions. |
The intended composition: an AdCP-using buyer agent and seller agent negotiate in refine mode, converge on terms, then mint a signed ODP deal proposal/acceptance pair as the binding contract. Delivery and settlement run on ODP. Creative generation, audience activation, governance, and agent capability declaration stay in AdCP. Neither protocol requires the other; pairing them is the canonical "fully agentic, fully contractual" stack.
- Open and royalty-free. Implementable by anyone without licensing or registration.
- Deal-type agnostic. Same wire for programmatic guaranteed, preferred deals, sponsorships, and direct/IO deals.
- Federated trust. No central registry. Every entity hosts its own well-known file declaring its role and policies; verification is direct between buyer and seller.
- No required intermediary. Buyer and seller can transact directly. SSPs/DSPs can act as buyer or seller agents but are not load-bearing.
- Cryptographic at every stage. Proposal, acceptance, delivery records, settlement attestations are all signed. Reconciliation is by signature, not invoice.
- Composes with the existing stack. Open Deals can carry OpenRTB-compatible deal IDs so RTB delivery infrastructure recognizes them. No required migration from existing ad-server integrations.
- Wire-defined commercial terms. Pricing, volume guarantees, dates, caps, brand-safety constraints are all in the wire, not in a side-letter PDF.
Five roles participate, though a single entity can play more than one:
| Role | What it does | Who typically plays it |
|---|---|---|
| Buyer | Originates deal proposals, commits budget, sends creatives, receives delivery records. | Advertiser, agency, in-house performance team |
| Buyer agent | Acts on the buyer's behalf to propose, sign, and reconcile. Authorized via the buyer's well-known file. | DSP, ad-tech vendor, agency trading desk |
| Seller | Receives deal proposals, accepts/counters/rejects, owns inventory, serves delivery, issues delivery records. | Publisher, ad network, affiliate network |
| Seller agent | Acts on the seller's behalf to negotiate and report. Authorized via the seller's well-known file. | SSP, ad server, ad-tech vendor, affiliate platform |
| Serving agent | The runtime that actually serves impressions against an active deal. May be the seller, the seller agent, or a separate ad-server vendor. | GAM, Kevel, Equativ, custom ad server, the seller's own infrastructure |
In the simplest case (a publisher selling direct to an advertiser with no agencies or SSPs), one entity is buyer + buyer agent and another is seller + seller agent + serving agent. The protocol does not require any of the agent roles to be filled by separate parties.
ODP defines six wire primitives, one per lifecycle stage:
- Inventory descriptor. Published by the seller in their well-known file. Describes what they are willing to sell and on what terms.
- Deal proposal. Signed POST from buyer to seller. Specifies inventory, commercial terms, creatives, dates.
- Deal response. Signed reply from seller. Accept, counter, or reject.
- Active deal record. A signed and bilaterally-acknowledged deal that the serving agent recognizes at impression time.
- Delivery record. Signed periodic report from seller to buyer. Impression counts, spend, KPIs.
- Settlement attestation. Signed reconciliation between buyer and seller closing out a delivery period.
sequenceDiagram
autonumber
participant Buyer
participant Seller
participant Server as Serving agent
Note over Buyer,Seller: Discovery (optional, async)
Seller->>Seller: publish inventory descriptors in well-known file
Buyer->>Seller: GET /.well-known/odp.json (cached)
Note over Buyer,Seller: Negotiation
Buyer->>Seller: POST signed deal proposal
Seller-->>Buyer: signed accept / counter / reject
Note over Buyer,Seller: Activation
Seller->>Server: register active deal record (internal)
Buyer->>Buyer: store active deal record (internal)
Note over Buyer,Server: Delivery (over the deal's flight dates)
Server->>Server: at impression time, check active deal, serve creative
Note over Buyer,Seller: Reporting
Seller->>Buyer: POST signed delivery record (periodic)
Buyer-->>Seller: 200 ack
Note over Buyer,Seller: Settlement
Buyer->>Seller: POST signed settlement attestation
Seller-->>Buyer: signed counter-attestation
Authentication uses three independent checks, modeled on the same SPF + DKIM + DMARC pattern OCP uses, applied to the bidirectional deal lifecycle.
| Email auth | ODP equivalent | What it proves | Where the data lives |
|---|---|---|---|
| DKIM | Sender of any wire request signs with a key from their published set; the recipient verifies | The sender is who they claim to be | sender_keys (or role-specific keys) in https://{sender}/.well-known/odp.json |
| SPF | A buyer agent claiming to act for a buyer must be in that buyer's authorized_agents; same for seller agents |
The agent is authorized by the principal | authorized_agents in https://{principal}/.well-known/odp.json |
| DMARC | Each side publishes its acceptance policy: which counterparties it deals with, which deal types it supports, minimum/maximum CPMs, etc. | The recipient's deal-acceptance posture is discoverable up front | seller.policy and buyer.policy in the respective well-known files |
The same federation pattern applies to all six primitives. Every signed wire request can be verified by any party with the sender's domain.
Every participating entity publishes a single file at:
https://{entity-domain}/.well-known/odp.json
The file contains optional seller and buyer blocks, plus role-agnostic keys and authorized_agents arrays. An entity that only sells publishes seller; one that only buys publishes buyer; an entity that does both (a publisher buying ads on its own competitors, for example) publishes both.
{
"spec_version": "0.1",
"keys": [
{"kid": "2026-04", "alg": "Ed25519", "pub": "MCowBQYDK2VwAyEA..."}
],
"authorized_agents": ["dsp.example", "ssp.example"],
"seller": {
"inventory": [
{
"inventory_id": "homepage-billboard",
"ad_units": ["/4567/homepage/billboard"],
"formats": ["display"],
"sizes": ["970x250", "728x90"],
"audience_constraints": {
"geo_allowlist": ["US", "CA", "GB"],
"device_types": ["desktop", "mobile_web"]
},
"pricing": {
"model": "cpm",
"floor": 12.50,
"currency": "USD"
},
"deal_types_accepted": ["programmatic_guaranteed", "preferred_deal", "direct"]
}
],
"policy": {
"min_flight_days": 1,
"max_flight_days": 365,
"accepted_creative_formats": ["display", "video"],
"brand_safety_tier_minimum": "iab_safe",
"settlement_terms_days": 60,
"dispute_window_days": 30
},
"delivery_endpoint": "https://publisher.example/odp/delivery",
"settlement_endpoint": "https://publisher.example/odp/settlement"
},
"buyer": {
"policy": {
"supported_deal_types": ["programmatic_guaranteed", "preferred_deal", "direct"],
"max_proposal_validity_seconds": 86400
},
"proposal_endpoint": "https://advertiser.example/odp/proposal",
"delivery_ack_endpoint": "https://advertiser.example/odp/delivery-ack"
}
}| Field | Type | Description |
|---|---|---|
spec_version |
string | ODP spec version. Currently "0.1". |
keys |
array | Public keys this entity uses to sign wire requests. Each entry has kid, alg (Ed25519 required, others optional), pub (base64-encoded public key). At least one key required if the entity acts as buyer or seller. |
authorized_agents |
array | Domains of buyer agents and seller agents authorized to sign on this entity's behalf. SPF-equivalent. Publishing keys implicitly authorizes the entity itself; this field only governs delegated signers. |
| Field | Type | Description |
|---|---|---|
inventory |
array | One or more inventory descriptors (see below). What the seller is willing to sell. |
policy |
object | Acceptance policy: minimum/maximum flight, accepted formats, brand-safety tier, settlement window, dispute window. |
delivery_endpoint |
string | URL where the seller POSTs signed delivery records to buyers. |
settlement_endpoint |
string | URL accepting signed settlement attestations. |
| Field | Type | Description |
|---|---|---|
inventory_id |
string | Stable identifier the seller uses to refer to this inventory bundle. Buyer references it in proposals. |
ad_units |
array | Seller's internal ad unit identifiers (GAM ad unit paths, equivalent in other systems). |
formats |
array | display, video, audio, native, dooh, etc. |
sizes |
array | IAB standard sizes for display, durations for video. |
audience_constraints |
object | Geo allowlist/blocklist, device types, content categories. |
pricing |
object | Pricing model and floor. See Pricing models. |
deal_types_accepted |
array | Subset of ["programmatic_guaranteed", "preferred_deal", "private_auction", "direct"]. See Deal types. |
| Field | Type | Description |
|---|---|---|
policy |
object | Buyer's policy: supported deal types, maximum proposal validity, etc. |
proposal_endpoint |
string | URL where the buyer accepts counters and asynchronous deal-status callbacks from sellers. |
delivery_ack_endpoint |
string | URL accepting delivery-record acknowledgements. |
Well-known files are cached aggressively. Recommended client behavior: respect HTTP cache headers, with a minimum cache duration of 1 hour and maximum of 24 hours. For key rotation faster than 1 hour, publish new keys alongside old, retire old after cache expiry.
| Deal type | Pricing | Volume | Auction | Priority |
|---|---|---|---|---|
programmatic_guaranteed |
Fixed CPM (or CPC/CPA per pricing model) | Guaranteed minimum impressions | None. Deal serves before open auction. | Highest |
preferred_deal |
Fixed floor, no guaranteed volume | None | None. First look at inventory before open auction. | High |
private_auction |
Floor only, real-time auction within an invited bidder set | None | Yes, between invited bidders only. | Medium |
direct |
Negotiated fixed fee, CPM, or sponsorship terms | Often guaranteed (sponsorships, takeovers) | None | Highest |
The wire format for proposal and acceptance is the same across deal types; the deal_type field selects the semantics. v0.1 specifies all four; receivers MAY decline to support any subset by omitting from their policy.deal_types_accepted.
| Model | Identifier | Notes |
|---|---|---|
| Cost per mille | cpm |
Most common for PG, preferred deals, private auctions. |
| Cost per click | cpc |
Common for affiliate-style direct deals. |
| Cost per action | cpa |
Composes with OCP for action attribution. |
| Flat fee | flat |
Sponsorships, takeovers, bundled placements. |
A deal proposal is an HTTPS POST from buyer (or buyer agent) to the seller's well-known-declared inventory contact. The seller's contact URL is implicit: deal proposals POST to a path under the seller's domain that is derived from inventory; v0.1 specifies https://{seller-domain}/odp/proposal as the canonical proposal endpoint, overridable via the seller's well-known file (see Open questions for the question of whether to make this explicit).
POST /odp/proposal HTTP/1.1
Host: publisher.example
Content-Type: application/json
ODP-Spec-Version: 0.1
ODP-Sender: advertiser.example
ODP-Key-Id: 2026-04
ODP-Signature: <base64url Ed25519 signature over the raw request body>
<JSON body, see below>The four headers serve the same role as their OCP counterparts: name the signing entity, select the key, sign the body, declare the spec version.
| Header | Description |
|---|---|
ODP-Spec-Version |
Version of ODP this request conforms to. |
ODP-Sender |
Domain of the entity sending the request. May be the principal (buyer) or an authorized agent (DSP). |
ODP-Key-Id |
Identifier of the public key used to sign. Must match a kid in https://{ODP-Sender}/.well-known/odp.json. |
ODP-Signature |
Base64url-encoded Ed25519 signature over the raw request body bytes. |
{
"odp_spec_version": "0.1",
"proposal_id": "prop-abc123",
"timestamp": 1746558464,
"buyer": {
"domain": "advertiser.example",
"id": "buyer-account-42"
},
"seller": {
"domain": "publisher.example"
},
"deal": {
"deal_type": "programmatic_guaranteed",
"inventory_id": "homepage-billboard",
"openrtb_deal_id": "PUB-PG-2026-Q2-AUDI",
"flight": {
"start": "2026-06-01T00:00:00Z",
"end": "2026-06-30T23:59:59Z",
"timezone": "America/New_York"
},
"pricing": {
"model": "cpm",
"price": 25.50,
"currency": "USD"
},
"volume": {
"guaranteed_impressions": 1000000,
"budget_cap": 25500
},
"frequency_cap": {
"per_user_per_day": 3,
"per_user_per_flight": 15
},
"creative": {
"creative_ids": ["crv-001", "crv-002"],
"sizes": ["970x250", "728x90"],
"click_through_url_pattern": "https://advertiser.example/landing/*"
},
"audience": {
"geo_allowlist": ["US", "CA"],
"device_types": ["desktop", "mobile_web"],
"first_party_segments": ["seg-auto-intender"],
"content_categories_excluded": ["IAB7-39", "IAB14-3"]
},
"brand_safety": {
"tier_required": "iab_safe",
"domain_blocklist_url": "https://advertiser.example/blocklist.txt"
}
},
"validity": {
"expires_at": 1746644864
},
"ext": {}
}odp_spec_version, proposal_id, timestamp, buyer.domain, seller.domain, deal.deal_type, deal.inventory_id, deal.flight, deal.pricing, and deal.creative.creative_ids. Other fields are deal-type-conditional or optional.
proposal_id is buyer-minted, opaque, used by both sides for idempotency and reconciliation. Sellers MUST dedupe on proposal_id.
Sellers respond synchronously with HTTP 200 and a signed JSON body, OR asynchronously by POSTing the response to the buyer's proposal_endpoint later. Synchronous is recommended for programmatic_guaranteed and preferred_deal; asynchronous is permitted for direct deals that need human review.
{
"odp_spec_version": "0.1",
"proposal_id": "prop-abc123",
"timestamp": 1746558464,
"status": "accepted",
"deal_id": "publisher-issued-deal-xyz789",
"openrtb_deal_id": "PUB-PG-2026-Q2-AUDI",
"active_until": "2026-07-30T23:59:59Z",
"ext": {}
}status is one of:
| Status | Meaning |
|---|---|
accepted |
Seller has activated the deal. deal_id is the seller's canonical reference. The deal is live from flight.start through flight.end, with a settlement window extending to active_until (typically 30 days after flight.end for reconciliation). |
countered |
Seller proposes modified terms. Body includes a full counter-proposal in the same shape as the proposal body. Buyer may accept, counter again, or reject. |
rejected |
Seller declines. Body includes rejection_reason (free-form string) and optionally rejection_code from a defined enum. |
The seller signs the response with ODP-* headers identical in shape to the proposal request. Asynchronous responses POST to the buyer's proposal_endpoint and follow the same auth model.
A counter response copies the original proposal's body and modifies the disputed fields. The buyer's signed acceptance of a counter MUST reference the counter's response_id (a seller-minted opaque identifier) to disambiguate from acceptance of the original.
Rejection codes (v0.1, non-exhaustive):
| Code | Meaning |
|---|---|
inventory_unavailable |
Requested inventory_id is sold out or not currently sellable for the requested flight. |
floor_violation |
Proposed price is below the seller's floor for this inventory. |
audience_unsupported |
Seller cannot fulfill the requested audience constraints. |
creative_rejected |
One or more creative IDs failed seller review. |
flight_unavailable |
Requested dates are not bookable. |
policy_violation |
Proposal violates seller's published policy (deal type not accepted, flight too short/long, etc.). |
buyer_not_authorized |
Seller does not transact with this buyer. |
other |
See rejection_reason field. |
Once a proposal is accepted, both sides hold a signed record of the active deal: the seller's signed accepted response, plus the buyer's signed proposal. Together these constitute the active deal record.
The serving agent (publisher's ad server, SSP, custom serving stack) MUST be able to look up an active deal by deal_id at impression time and apply the deal's terms (audience filters, frequency caps, pacing toward volume.guaranteed_impressions). v0.1 does not specify the serving-agent-internal lookup mechanism. Most existing ad servers already support deal lookup via an internal deal table; ODP just standardizes the protocol that populates and updates that table.
| State | Transitions to | Trigger |
|---|---|---|
negotiating |
active, rejected |
Initial proposal lands. Counters keep the deal here with a last_response_from field tracking whose turn it is. Either side's signed accept moves to active; either side's signed reject moves to rejected. |
active |
paused, terminated, settled |
Bilateral signed pause/termination, or natural settlement at end of dispute window. |
paused |
active, terminated |
Bilateral signed resume or unilateral signed termination. |
terminated |
settled |
Termination triggers settlement against delivered-to-date metrics. |
rejected |
(terminal) | Negotiation failed; deal never activated. |
settled |
(terminal) | Both parties have signed settlement attestations; deal is closed. |
Counter-proposals do not change state; they update last_response_from and append to the negotiation_history of the active negotiation record. The state machine collapses what could be modeled as separate proposed and countered states because they have identical semantics ("waiting for the other side to respond").
State transitions are wire events: each transition is an authenticated HTTP POST with the same ODP-* header model as the proposal, addressed to the counterparty's relevant endpoint.
The seller (or seller agent acting on its behalf) periodically POSTs signed delivery records to the buyer's delivery_ack_endpoint. Recommended cadence: hourly during active flight, daily after flight end during the settlement window.
{
"odp_spec_version": "0.1",
"deal_id": "publisher-issued-deal-xyz789",
"timestamp": 1746558464,
"period": {
"start": "2026-06-15T00:00:00Z",
"end": "2026-06-15T23:59:59Z"
},
"metrics": {
"impressions": 34521,
"viewable_impressions": 27893,
"clicks": 412,
"video_completes": 0,
"spend": 880.29,
"currency": "USD"
},
"pacing": {
"delivered_to_date_impressions": 487123,
"guaranteed_impressions": 1000000,
"expected_at_period_end_impressions": 488000
},
"ext": {}
}The buyer's delivery_ack_endpoint returns a signed acknowledgement with HTTP 200. Mismatches between buyer-side counted impressions (from creative-served pixels or third-party measurement) and seller-side reported impressions become disputes (handled out-of-band per the seller's policy.dispute_window_days).
v0.1 specifies batched delivery records (per-period, not per-impression). Per-impression signed records are out of scope for v0.1; cardinality is too high for cryptographic per-event reporting at most deal volumes. Per-period batches with seller attestation are sufficient for reconciliation.
After the flight ends and the settlement window opens, either side initiates settlement by POSTing a signed settlement attestation to the counterparty's settlement_endpoint.
{
"odp_spec_version": "0.1",
"deal_id": "publisher-issued-deal-xyz789",
"timestamp": 1746558464,
"period": {
"start": "2026-06-01T00:00:00Z",
"end": "2026-06-30T23:59:59Z"
},
"final_metrics": {
"impressions": 1023412,
"viewable_impressions": 821094,
"clicks": 8412,
"spend": 26097.01,
"currency": "USD"
},
"settlement": {
"owed": 26097.01,
"currency": "USD",
"payable_terms_days": 60,
"external_invoice_ref": "INV-2026-Q2-1138"
},
"ext": {}
}The counterparty signs an acknowledgement (or counter-attestation if the metrics or amounts differ from their records). Two matching signed attestations close the deal: state transitions to settled. Disputes that cannot be reconciled within the dispute window remain terminated until resolved out-of-band.
ODP does not specify the actual money movement (ACH, wire, blockchain, NET-60 invoicing, etc.). The settlement attestation is a cryptographic record of what was owed; the rails for moving the money are commercial infrastructure outside the protocol.
When any participant receives any signed ODP wire request (proposal, response, delivery record, settlement attestation, state-transition event), it performs the same six-step verification flow:
flowchart TD
Start([Inbound signed request]) --> S1["Step 1: Schema validation<br/>required fields + supported deal_type"]
S1 -->|invalid| R400[400 Bad Request]
S1 -->|ok| S2["Step 2: Timestamp window<br/>abs(now − ts) ≤ max_skew"]
S2 -->|stale or future| R401[401 Unauthorized]
S2 -->|ok| S3["Step 3: DKIM<br/>fetch sender well-known<br/>verify Ed25519 over body bytes"]
S3 -->|fetch fail / unknown kid / bad sig| R401
S3 -->|ok| Q1{ODP-Sender = principal<br/>or authorized agent?}
Q1 -->|true| S5
Q1 -->|false| S4["Step 4: SPF<br/>fetch principal well-known<br/>sender in authorized_agents?"]
S4 -->|fail| R401
S4 -->|ok| S5["Step 5: DMARC<br/>recipient policy admits<br/>this sender / deal type / counterparty?"]
S5 -->|fail| R401
S5 -->|ok| S6["Step 6: Idempotency<br/>(proposal_id, deal_id, period) already seen?"]
S6 -->|duplicate| Ok200[200 OK<br/>idempotent re-ack]
S6 -->|new| Ingest[200 OK<br/>process and update state]
- Schema validation. Required fields per request type;
deal_typeis in the recipient's accepted set. - Timestamp. Replay protection. Default skew window: ±300 seconds.
- DKIM (sender authentication). Fetch
https://{ODP-Sender}/.well-known/odp.json, find the key byODP-Key-Id, verify the Ed25519 signature over the raw body. - SPF (agent authorization). If
ODP-Senderis not the principal named in the body (buyer.domain,seller.domain, etc.), fetch the principal's well-known and check thatODP-Senderis inauthorized_agents. Skip if sender is the principal directly. - DMARC (recipient policy). The recipient's published policy admits this counterparty, deal type, and price range. Reject violations of the recipient's own policy.
- Idempotency. Dedupe on the natural key for the request type (
proposal_idfor proposals,(deal_id, period.start)for delivery records, etc.). Re-applying a duplicate returns 200 with no further effect.
| Status | Meaning |
|---|---|
200 |
Accepted (synchronous) or queued (asynchronous). |
202 |
Accepted, processed asynchronously. |
400 |
Schema or content failure. |
401 |
Authentication or authorization failure. |
404 |
Endpoint not configured. |
409 |
Conflict (e.g. proposal expired, state transition not permitted). |
429 |
Rate limited. |
500 |
Internal error. |
Deal-level data, not user-level data. The wire carries audience constraints (segment IDs, geo, device types) but not user-level PII. Hashed identifiers and other user-level data flow through OpenRTB at impression time (for delivery decisions) or through OCP at conversion time (for attribution), not through the deal-lifecycle wire.
Audience segment opacity. First-party segment IDs (first_party_segments in the proposal) are buyer-internal and meaningful only to the seller if the buyer has shared the segment definition out-of-band. The protocol carries the ID; meaning is bilateral.
Settlement is non-public. Settlement attestations are signed but not broadcast. Each side holds its own signed copy; reconciliation is bilateral.
Right to be forgotten. ODP delivery records reference deals and aggregate metrics, not user identifiers. Right-to-erasure for user-level data flows through OCP's deletion endpoint; ODP does not carry user data subject to erasure.
What the protocol deliberately does not define:
- Money movement. Settlement attestations record what is owed; the rails for paying it (ACH, wire, NET-60 invoice, blockchain, etc.) are commercial infrastructure. The protocol's settlement primitive is the cryptographic record, not the payment rail.
- Creative storage and serving. The proposal references creative IDs; how creatives are stored, fetched, viewability-tracked, and served is the serving agent's concern. Vendors compete on this.
- Real-time auction within a deal.
private_auctionis recognized as a deal type but the auction mechanics (bid format, response time, decisioning) defer to OpenRTB or any other auction protocol the parties agree on. - Dispute resolution mechanics. The protocol exposes a dispute window and a dispute count; resolution itself is bilateral, contractual, and out of scope.
- Identity resolution. No cookie sync, no UID 2.0 negotiation, no contextual classifier. Audience targeting is by reference; identity infrastructure lives elsewhere.
- Frequency-cap enforcement at impression time. The serving agent enforces; the protocol just specifies the cap in the deal record.
- Pacing algorithms. The serving agent decides how to pace; the protocol just reports delivered-to-date.
- Should ODP, OCP, and AdCP share a single well-known file? Today there are three candidates: OCP's
/.well-known/ocp.json, ODP's/.well-known/odp.json, and AdCP's/.well-known/adagents.json. A shared/.well-known/openads.json(or similar) with role-specific blocks would simplify operator configuration and avoid drift across the three specs. Tradeoff: bigger single schema, slower iteration, version coupling across three protocols. Tracking this question for v0.2 once all three specs have community feedback. The question is being tracked in parallel in the OCP repo: see amirhr/ocp. - Alignment of cryptographic primitives with AdCP. AdCP uses RFC 9421 HTTP Message Signatures, JWKS for key publication, and JWS for tamper-evident tokens. ODP v0.1 uses Ed25519 signatures over raw body bytes and a custom
keysarray. Aligning to AdCP's choices would simplify multi-protocol agent stacks (one signature library, one key format) at the cost of more implementation surface for receivers. Recommended for v1.0; v0.1 keeps the simpler primitives to lower the prototyping bar. The OCP repo has the same question. - Composition with AdCP. Should ODP define explicit hooks for AdCP-mediated negotiation (e.g. an
adcp_negotiation_idfield on the proposal so an ODP contract can reference the conversation it was minted from)? Or does the existingextfield cover it? - OpenRTB Deal ID compatibility. The proposal allows
openrtb_deal_idas an optional field. Should it be required whendeal_typeis one that traditionally rides RTB delivery? Should the protocol mandate a registry pattern for OpenRTB deal IDs to avoid collisions across sellers? - Per-impression signed records. v0.1 specifies batched delivery records only. Is there a credible use case (high-CPM premium video, IP licensing, etc.) where per-impression cryptographic attestation is worth the cardinality cost?
- Settlement currency mismatches. Cross-border deals where buyer pays USD and seller is paid in local currency. Does the protocol need to express FX terms or stay silent and let commercial agreements handle it?
- Brand-safety tier vocabulary. v0.1 references
iab_safeinformally. Should the protocol normatively reference an external taxonomy (IAB Tech Lab Brand Safety, GARM, etc.) or define its own? - Reserved-budget cryptographic attestations. Should the buyer's well-known publish signed budget attestations (proof of reserved funds for a deal) or leave that to commercial credit relationships? The latter is simpler; the former enables permissionless market entry.
- Streaming delivery records (SSE/WebSocket). v0.1 specifies HTTP POST per period. Should real-time pacing dashboards drive a streaming variant?
- Defensive publication of this draft to establish prior art.
- Informal circulation to publisher direct-sold ops, programmatic guaranteed teams at premium publishers, DSP product teams, ad-server vendors.
- Iterate toward v0.2 incorporating community input, with particular attention to questions 1, 2, and 5 above.
- Engage IAB Tech Lab about the relationship to OpenRTB Deal IDs and the legacy OpenDirect spec.
This specification is licensed under Creative Commons Attribution 4.0 International (CC-BY-4.0). Patent terms in PATENTS.md. Reference code (when added) under Apache License 2.0.