Skip to content

Latest commit

 

History

History
618 lines (471 loc) · 36.1 KB

File metadata and controls

618 lines (471 loc) · 36.1 KB

Open Deal Protocol (ODP)

Draft spec, v0.1 Author: Amir Rad Status: Working draft, not for implementation


Problem

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.

Goal

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.


Relationship to existing work

IAB Tech Lab OpenRTB Deal IDs

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.

IAB Tech Lab OpenDirect

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.

Programmatic guaranteed (PG)

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

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.

Open Conversion Pingback (OCP)

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.

Ad Context Protocol (AdCP)

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.


Design principles

  1. Open and royalty-free. Implementable by anyone without licensing or registration.
  2. Deal-type agnostic. Same wire for programmatic guaranteed, preferred deals, sponsorships, and direct/IO deals.
  3. 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.
  4. No required intermediary. Buyer and seller can transact directly. SSPs/DSPs can act as buyer or seller agents but are not load-bearing.
  5. Cryptographic at every stage. Proposal, acceptance, delivery records, settlement attestations are all signed. Reconciliation is by signature, not invoice.
  6. 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.
  7. Wire-defined commercial terms. Pricing, volume guarantees, dates, caps, brand-safety constraints are all in the wire, not in a side-letter PDF.

Architecture overview

Roles

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.

Primitives

ODP defines six wire primitives, one per lifecycle stage:

  1. Inventory descriptor. Published by the seller in their well-known file. Describes what they are willing to sell and on what terms.
  2. Deal proposal. Signed POST from buyer to seller. Specifies inventory, commercial terms, creatives, dates.
  3. Deal response. Signed reply from seller. Accept, counter, or reject.
  4. Active deal record. A signed and bilaterally-acknowledged deal that the serving agent recognizes at impression time.
  5. Delivery record. Signed periodic report from seller to buyer. Impression counts, spend, KPIs.
  6. Settlement attestation. Signed reconciliation between buyer and seller closing out a delivery period.

End-to-end flow

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
Loading

Federated trust model

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.


The well-known file

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"
  }
}

Top-level fields

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.

Seller block

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.

Inventory descriptor

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.

Buyer block

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.

Caching

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 types

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.

Pricing models

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.

The deal proposal

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).

HTTP request

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>

Required headers

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.

Request body

{
  "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": {}
}

Required body fields

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.


The deal response

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.

Synchronous response

{
  "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.

Counter and rejection details

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.

Active deals

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 machine

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.


Delivery records

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).

Per-impression vs batched

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.


Settlement

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.


Verification flow

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]
Loading

Step details

  1. Schema validation. Required fields per request type; deal_type is in the recipient's accepted set.
  2. Timestamp. Replay protection. Default skew window: ±300 seconds.
  3. DKIM (sender authentication). Fetch https://{ODP-Sender}/.well-known/odp.json, find the key by ODP-Key-Id, verify the Ed25519 signature over the raw body.
  4. SPF (agent authorization). If ODP-Sender is not the principal named in the body (buyer.domain, seller.domain, etc.), fetch the principal's well-known and check that ODP-Sender is in authorized_agents. Skip if sender is the principal directly.
  5. DMARC (recipient policy). The recipient's published policy admits this counterparty, deal type, and price range. Reject violations of the recipient's own policy.
  6. Idempotency. Dedupe on the natural key for the request type (proposal_id for proposals, (deal_id, period.start) for delivery records, etc.). Re-applying a duplicate returns 200 with no further effect.

Response codes

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.

Privacy model

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.


Out of scope

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_auction is 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.

Open questions

  1. 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.
  2. 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 keys array. 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.
  3. Composition with AdCP. Should ODP define explicit hooks for AdCP-mediated negotiation (e.g. an adcp_negotiation_id field on the proposal so an ODP contract can reference the conversation it was minted from)? Or does the existing ext field cover it?
  4. OpenRTB Deal ID compatibility. The proposal allows openrtb_deal_id as an optional field. Should it be required when deal_type is one that traditionally rides RTB delivery? Should the protocol mandate a registry pattern for OpenRTB deal IDs to avoid collisions across sellers?
  5. 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?
  6. 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?
  7. Brand-safety tier vocabulary. v0.1 references iab_safe informally. Should the protocol normatively reference an external taxonomy (IAB Tech Lab Brand Safety, GARM, etc.) or define its own?
  8. 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.
  9. Streaming delivery records (SSE/WebSocket). v0.1 specifies HTTP POST per period. Should real-time pacing dashboards drive a streaming variant?

Next steps

  1. Defensive publication of this draft to establish prior art.
  2. Informal circulation to publisher direct-sold ops, programmatic guaranteed teams at premium publishers, DSP product teams, ad-server vendors.
  3. Iterate toward v0.2 incorporating community input, with particular attention to questions 1, 2, and 5 above.
  4. Engage IAB Tech Lab about the relationship to OpenRTB Deal IDs and the legacy OpenDirect spec.

License

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.