Skip to content

fix: keep the seller's floor off buyer-facing catalog surfaces - #125

Open
Sirajmx wants to merge 3 commits into
mainfrom
fix/seller-private-floors
Open

Sirajmx wants to merge 3 commits into
mainfrom
fix/seller-private-floors

Conversation

@Sirajmx

@Sirajmx Sirajmx commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The floor is the seller's negotiation reserve. The shared Negotiation primitive keeps it off the wire, and the negotiation routes already strip it (#95). The catalog surfaces still published it. Since #117 serves real ad-server floors, each of these told a buyer the lowest price the seller would accept:

Surface Auth What leaked
GET /products, GET /products/{id} none ext.floor_cpm on every product
MCP list_products ungated floor_cpm
MCP get_config, get_setup_status ungated default floor, pg_floor_multiplier, pd_discount_max
AuthenticatedPackageView (media kit) any non-public effective tier, including a tier claimed in the body on /media-kit/search floor_price
AgentCore create_deal_direct — the floor-derived minimum in the rejection message, and floor_cpm on the booked deal
POST /discovery pricing answers any tier that sees exact prices each product's floor (bare key name)

Change

  • Drop floor_cpm from the shared Product ext (REST, and AgentCore's product tools, which call REST) and from MCP list_products.
  • MCP get_config / get_setup_status: the pricing policy fields are returned only to an operator (operator key over HTTP, or local stdio, the same model as the CLI). Everyone else gets currency.
  • Remove floor_price from AuthenticatedPackageView. exact_price, which is what the buyer pays, stays.
  • AgentCore create_deal_direct: the below-floor rejection no longer states the threshold, and the booked deal no longer carries floor_cpm.
  • DiscoveryInquiryFlow: pricing answers no longer include floor (the buyer's price stays). Found by @Kishore-2572 in review.
  • docs/guides/media-kit.md: removed floor_price from the authenticated package example.
  • CHANGELOG entry under Fixed.

Unchanged: quotes, the floor clamp (#92) and negotiation still read floors internally, and deal-level auction bid floors (bidfloor) are still published, since a PA buyer needs them. The seller_minimum_cpm on the REST below-floor 422 stays: there it is the buyer's own tier price, not the floor.

Open question

AuthenticatedPackageView.floor_price is removed by default. If key-holding buyers should keep seeing the package floor, that's a one-line revert plus one test. Note that this view is chosen by effective tier, not only by key.

Testing

  • New tests/unit/test_catalog_floor_disclosure.py (15 tests): each surface is swept for floor field names at any depth, including the bare floor. The 12 leak tests fail on main and pass here; the other 3 pin what stays (the rest of ext, operator access to the pricing policy, and the /discovery catalog answer).
  • Unit: 1812 passed. Integration: 39 passed, 28 skipped. ruff check src/ and ruff format --check src/ tests/ clean.
  • Live sweep of anonymous and claimed-tier responses (products, product detail, pricing, quotes, media kit list/detail/search, packages, discovery with pricing/catalog/availability/targeting queries, rate card, agent card): on main, ext.floor_cpm on both product routes, floor_price on claimed-tier media-kit search, and floor on /discovery pricing answers; on this branch, none.
  • Cross-repo: buyer-agent main (26718ae) clients against this branch: OpenDirectClient.list_products, MediaKitClient.get_media_kit and search_packages all parse. The buyer only reads floor_price with data.get() into an optional field.

The floor is the seller's negotiation reserve, which the shared Negotiation primitive keeps off the wire, but the catalog still published it: ext.floor_cpm on GET /products, floor_cpm on the ungated MCP list_products, the default floor and PG/PD knobs on the ungated MCP get_config/get_setup_status, floor_price on AuthenticatedPackageView (served to any non-public effective tier, including a body-claimed one on /media-kit/search), and the floor-derived minimum and floor_cpm on AgentCore create_deal_direct. With real ad-server floors served (AI-6), each told a buyer the lowest price the seller would accept.

Drop the floor from those responses; the MCP pricing policy is returned only to an operator. Pricing, quotes and negotiation still read floors internally, and deal-level bidfloor is unchanged.
@Kishore-2572

Copy link
Copy Markdown
Contributor

Pulled 11d2bbe and checked it against a real server, base (bcc22d4) vs PR. Each leak this closes is there on main and gone here: /products ext.floor_cpm, MCP list_products, and MCP get_config/get_setup_status for callers with no key or a buyer key. An operator key over MCP HTTP still gets the full pricing policy, and local stdio still counts as operator. I also reintroduced each leak one at a time (8 cases, including the threshold in the AgentCore message and an operator gate that never opens), and the new tests fail every time.

One surface is still leaking the floor: DiscoveryInquiryFlow (flows/discovery_inquiry_flow.py:167) sets price_info["floor"] = product.floor_cpm for tiers that see exact prices. To reproduce, call POST /discovery with an agency or advertiser buyer key and a pricing query (e.g. "What is the pricing for video?"). The response includes every product's floor, e.g. price: 13.5, floor: 10.0. The field is named "floor", not floor_cpm, so it's easy to miss with a grep. It already leaks on main, but it's the same floor this PR removes from AuthenticatedPackageView, so key holders can still read it. Could you drop that line and add a /discovery case to test_catalog_floor_disclosure.py?

On the open question: I'd keep the floor hidden from key-holding buyers too. A key proves who the buyer is, not that they should see the seller's reserve, and exact_price already tells them what they pay. /discovery needs the same treatment for that to hold.

Minor: docs/guides/media-kit.md:266 still shows floor_price in the authenticated package response.

Everything else held up: buyer-agent doesn't read ext.floor_cpm, and its media-kit client treats floor_price as optional. The full unit suite passes on 3.12.

DiscoveryInquiryFlow returned each product's floor under the bare key "floor" to any tier that sees exact prices. Drop it (the buyer's price stays), add the bare name to the floor-disclosure sweep, and cover /discovery pricing and catalog answers. Also remove floor_price from the authenticated package example in docs/guides/media-kit.md.
@Sirajmx

Sirajmx commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks Kishore, confirmed and fixed in 5f320a8. DiscoveryInquiryFlow returned the bare floor for any tier that sees exact prices; that line is gone (the buyer's price stays). The test sweep now treats floor as a forbidden name too, and there are /discovery cases for pricing and catalog answers. The two pricing ones fail without the fix. Removed floor_price from the authenticated example in docs/guides/media-kit.md. For anyone reproducing: the flow only gives a pricing answer when the query contains "price", "cost" or "cpm", so "What is the pricing for video?" alone gets the catalog answer. Thanks for weighing in on AuthenticatedPackageView too.

@Kishore-2572 Kishore-2572 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked 2bec746. /discovery pricing answers (price and cost queries) carry no floor for agency/advertiser keys or a body-claimed tier, live on a real server, and the rest of the sweep from last round is still clean (operator still gets the pricing policy over MCP). Putting the floor line back fails both new /discovery pricing tests. 1849 unit passed, integration green. And thanks for the correction on the query wording — "pricing" alone does fall to the catalog answer. Approving from my side.

@Sirajmx
Sirajmx requested a review from atc964 October 28, 2026 16:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants