Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/ad_seller/interfaces/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ class ProposalResponse(BaseModel):
status: str
counter_terms: Optional[dict[str, Any]] = None
approval_id: Optional[str] = None
pricing_verified: bool = False
pricing_verification_reason: str = ""
errors: list[str] = []


Expand Down Expand Up @@ -533,12 +535,24 @@ async def submit_proposal(
products=setup_flow.state.products,
)

# Verify pricing against quote history (Layer 4 — CPM hallucination defense)
from ...storage.factory import get_storage
from ...storage.quote_history import QuoteHistoryStore

storage = await get_storage()
quote_history = QuoteHistoryStore(storage)
verification = await quote_history.verify_pricing(
buyer_id=context.get_pricing_key(),
product_id=request.product_id,
proposed_cpm=request.price,
)
pricing_verified = verification.pricing_verified
pricing_verification_reason = verification.reason

# If pending approval, create the approval request
if result.get("pending_approval"):
from ...events.approval import ApprovalGate
from ...storage.factory import get_storage

storage = await get_storage()
gate = ApprovalGate(storage)
approval_req = await gate.request_approval(
flow_id=result["flow_id"],
Expand All @@ -549,6 +563,8 @@ async def submit_proposal(
"recommendation": result["recommendation"],
"evaluation": result.get("evaluation"),
"counter_terms": result.get("counter_terms"),
"pricing_verified": pricing_verified,
"pricing_verification_reason": pricing_verification_reason,
},
flow_state_snapshot=result.get("_flow_state_snapshot", {}),
proposal_id=proposal_id,
Expand All @@ -559,6 +575,8 @@ async def submit_proposal(
status="pending_approval",
counter_terms=result.get("counter_terms"),
approval_id=approval_req.approval_id,
pricing_verified=pricing_verified,
pricing_verification_reason=pricing_verification_reason,
errors=result.get("errors", []),
)

Expand All @@ -567,6 +585,8 @@ async def submit_proposal(
recommendation=result["recommendation"],
status=result["status"],
counter_terms=result.get("counter_terms"),
pricing_verified=pricing_verified,
pricing_verification_reason=pricing_verification_reason,
errors=result.get("errors", []),
)

Expand Down Expand Up @@ -1939,6 +1959,18 @@ async def create_quote(
storage = await get_storage()
await storage.set_quote(quote_id, quote.model_dump(mode="json"), ttl=86400)

# Record in quote history for pricing verification (Layer 4)
from ...storage.quote_history import QuoteHistoryStore

quote_history = QuoteHistoryStore(storage)
await quote_history.record_quote(
quote_id=quote_id,
buyer_id=context.get_pricing_key(),
product_id=request.product_id,
quoted_cpm=final_cpm,
expires_at=expires_at,
)

return quote.model_dump(mode="json")


Expand Down
3 changes: 3 additions & 0 deletions src/ad_seller/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
BuyerIdentity,
IdentityLevel,
)
from .pricing_type import PricingType
from .change_request import (
ChangeRequest,
ChangeRequestStatus,
Expand Down Expand Up @@ -170,6 +171,8 @@
)

__all__ = [
# Pricing type enum
"PricingType",
# Core ad tech entities
"Organization",
"OrganizationRole",
Expand Down
3 changes: 3 additions & 0 deletions src/ad_seller/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

from pydantic import BaseModel, ConfigDict, Field

from .pricing_type import PricingType

# =============================================================================
# Enums
# =============================================================================
Expand Down Expand Up @@ -337,6 +339,7 @@ class Pricing(BaseModel):

model_config = ConfigDict(populate_by_name=True)

pricing_type: PricingType = PricingType.FIXED
pricing_model: Optional[PricingModel] = Field(default=None, alias="pricingmodel")
price: Optional[float] = None
currency: Optional[str] = None
Expand Down
16 changes: 14 additions & 2 deletions src/ad_seller/models/flow_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pydantic import BaseModel, Field

from .core import DealType, PricingModel
from .pricing_type import PricingType


class ExecutionStatus(str, Enum):
Expand Down Expand Up @@ -44,8 +45,9 @@ class ProductDefinition(BaseModel):
inventory_segment_ids: list[str] = Field(default_factory=list)
supported_deal_types: list[DealType] = Field(default_factory=list)
supported_pricing_models: list[PricingModel] = Field(default_factory=list)
base_cpm: float
floor_cpm: float
pricing_type: PricingType = PricingType.FIXED
base_cpm: Optional[float] = None
floor_cpm: Optional[float] = None
audience_targeting: Optional[dict[str, Any]] = None
content_targeting: Optional[dict[str, Any]] = None
ad_product_targeting: Optional[dict[str, Any]] = None
Expand Down Expand Up @@ -113,6 +115,16 @@ class ProposalEvaluation(BaseModel):
description="UCP embedding similarity score (0-1)",
)

# Quote-based pricing verification (Layer 4 — CPM hallucination defense)
pricing_verified: bool = Field(
default=False,
description="Whether the proposed CPM matches a quote the seller issued",
)
pricing_verification_reason: str = Field(
default="",
description="Explanation of pricing verification result",
)

# Overall recommendation
recommendation: str # accept, counter, reject
counter_terms: Optional[dict[str, Any]] = None
Expand Down
6 changes: 4 additions & 2 deletions src/ad_seller/models/media_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from pydantic import BaseModel, Field

from .core import PricingModel
from .pricing_type import PricingType


class PackageLayer(str, Enum):
Expand Down Expand Up @@ -88,8 +89,9 @@ class Package(BaseModel):
geo_targets: list[str] = Field(default_factory=list) # ["US", "US-NY", "US-CA"]

# Pricing (blended from constituent products)
base_price: float
floor_price: float
pricing_type: PricingType = PricingType.FIXED
base_price: Optional[float] = None
floor_price: Optional[float] = None
rate_type: PricingModel = PricingModel.CPM
currency: str = "USD" # ISO 4217

Expand Down
26 changes: 26 additions & 0 deletions src/ad_seller/models/pricing_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

"""Pricing type enum for seller inventory pricing signals.

Allows sellers to express whether a price is fixed, a floor for
negotiation, or unavailable (rate on request). Buyer agents must
respect these signals and never fabricate pricing when a seller
has indicated on_request.
"""

from enum import Enum


class PricingType(str, Enum):
"""How pricing should be interpreted for a product or package.

- FIXED: Price is set by the seller, use as-is.
- FLOOR: Minimum price; negotiation expected above this level.
- ON_REQUEST: No price available; buyer must negotiate before
any pricing exists. Pricing fields should be None.
"""

FIXED = "fixed"
FLOOR = "floor"
ON_REQUEST = "on_request"
5 changes: 4 additions & 1 deletion src/ad_seller/models/quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from pydantic import BaseModel, Field

from .pricing_type import PricingType


class QuoteStatus(str, Enum):
"""Status of a price quote."""
Expand Down Expand Up @@ -84,7 +86,8 @@ class QuoteProductInfo(BaseModel):
class QuotePricing(BaseModel):
"""Pricing breakdown in a quote response."""

base_cpm: float
pricing_type: PricingType = PricingType.FIXED
base_cpm: Optional[float] = None
tier_discount_pct: float = 0.0
volume_discount_pct: float = 0.0
final_cpm: float
Expand Down
Loading