Repository navigation
Description
Checkout: seller-agent v2.4.2 (e5b367d2b780aa4d0e03b6363d744ba3adf2b221)
Sample catalog: data/csv/samples/aws_workshop — 15 products — loaded by the default CSV adapter. No other catalog was selected.
Environment: Install with the repository lock so FastAPI stays 0.139.0 (pip install -e . -c constraints.txt, with constraints.txt holding the lock's pins, or uv sync --locked).
These routes are mounted with no prefix. They are /packages, not /api/v1/packages.
At e5b367d2b780aa4d0e03b6363d744ba3adf2b221 (v2.4.2), an unauthenticated GET /packages?buyer_tier=agency returns the authenticated package view, including exact_price and floor_price. GET /packages/{package_id}?buyer_tier=agency does the same. Without the query parameter, the same anonymous calls return only price_range, which is the public view.
We reproduced this in two separate environments on October 3, 2026: a clean Linux container (Python 3.12.11) and WSL2 Ubuntu 24.04 (Python 3.12.3). Each was a fresh clone at the SHA above, with dependencies held to the repository's uv.lock versions, in CSV ad-server mode with the bundled aws_workshop sample. The server was bound to port 8001. The v2.4.2 README uses port 8000.
Reproduction
- From the repo root, start the seller and wait until
GET /healthreturns 200. Port 8001 is the port we bound. The v2.4.2 README uses 8000.
./.venv/bin/uvicorn ad_seller.interfaces.api.main:app --port 8001- Bootstrap an operator key. The label and expiry are not part of the defect. The token is printed on stdout with an
ask_live_prefix.
./.venv/bin/ad-seller create-operator-key --label "aamp-lab-2026-10-03" --expires-in-days 90- Curate one package that has a floor price, using that key.
inv-ctv-apex-seriesis a product in theaws_workshopsample.
curl -s -X POST localhost:8001/packages \
-H 'Content-Type: application/json' -H "X-Api-Key: $OPERATOR_KEY" \
-d '{"name":"Lab package 1","product_ids":["inv-ctv-apex-series"],"base_price":45.00,"floor_price":38.25}'The response is the package id. Ours was HTTP 200. The id differs by run.
- Read it anonymously, with and without the parameter. Use the id from step 3.
curl -s localhost:8001/packages
curl -s "localhost:8001/packages?buyer_tier=agency"
curl -s localhost:8001/packages/$PACKAGE_ID
curl -s "localhost:8001/packages/$PACKAGE_ID?buyer_tier=agency"- Mint a buyer key and repeat the two reads with
X-Api-Key. The token is theask_live_value in the mint response.
curl -s -X POST localhost:8001/auth/api-keys \
-H 'Content-Type: application/json' -H "X-Api-Key: $OPERATOR_KEY" \
-d '{"label":"lab agency-tier buyer","agency_name":"Lab Agency","seat_name":"Lab Seat"}'curl -s -H "X-Api-Key: $BUYER_KEY" localhost:8001/packages
curl -s -H "X-Api-Key: $BUYER_KEY" localhost:8001/packages/$PACKAGE_IDObserved
Every call returned HTTP 200.
| Caller | Request | Price fields in the package |
|---|---|---|
| Anonymous | GET /packages | price_range: "$36-$54 CPM" only |
| Anonymous | GET /packages?buyer_tier=agency | exact_price: 45.0, floor_price: 38.25, price_range |
| Anonymous | GET /packages/{package_id}?buyer_tier=agency | exact_price: 45.0, floor_price: 38.25, price_range |
| Buyer API key | GET /packages and GET /packages/{package_id} | exact_price, floor_price, price_range |
In both environments on October 3, the anonymous ?buyer_tier=agency response bodies were byte-identical to that run's buyer-key bodies, and to that run's operator-key bodies. The package id differs by run, so the match is within a run. A further run on October 9, against the same checkout and the aws_workshop sample, produced the same price fields. The formatted anonymous tiered bodies matched the formatted buyer-key bodies on both routes, and the formatted single-package body also matched the operator-key body.
On that October 9 run, GET /packages/{package_id} with buyer_tier=seat, buyer_tier=advertiser, and buyer_tier=nope also returned exact_price 45.0 and floor_price 38.25. nope is not a recognized tier. Those three were compared on the price fields only. The formatted agency body on that route matched the formatted buyer-key body and the formatted operator-key body. The same call with only agency_id=lab-agency, and buyer_tier left at its default, returned price_range only. GET /media-kit/packages/{package_id}?buyer_tier=agency also returned price_range only. That route does not take buyer_tier.
Read in source at e5b367d2. list_packages and get_package take buyer_tier as a query parameter and use the public view only when api_key_record is None and buyer_tier == "public". Any other value takes the authenticated view:
src/ad_seller/interfaces/api/routers/media_kit.pyL270–L283src/ad_seller/interfaces/api/routers/media_kit.pyL299–L308
With no key, deps._build_buyer_context maps the caller-supplied buyer_tier to an AccessTier and sets is_authenticated true when that tier is not public. Its comment describes this as "existing behavior, backward compatible":
src/ad_seller/interfaces/api/deps.pyL87–L94
POST /media-kit/search applies the same condition to buyer_tier in its request body (L145–L147). In our runs its response was the same for anonymous, buyer-key and operator callers, so we did not observe a pricing difference there.
These line ranges are unchanged on main at 81a2e730e534614575777d66427a0a19a13eb3fc. We read that in source and did not run main.
Scope
We tried buyer_tier=agency on GET /packages and GET /packages/{package_id}. On the October 9 run we also tried seat, advertiser, and an unrecognized value, nope, on GET /packages/{package_id} only. All four values returned exact_price and floor_price. The route uses the public view only when there is no key and buyer_tier is the literal public. Any other query value takes the authenticated view, before the tier map is consulted.
We did not try the advertiser_id parameter, or the MCP and A2A surfaces. agency_id alone did not change the public view. In the October 3 runs, /media-kit/packages and /media-kit/packages/{package_id} returned the public payload to every caller. On October 9, adding ?buyer_tier=agency to the media-kit package route still returned the public payload.
Question
Is the unauthenticated tier fallback on these routes intended to remain in v2.x? As it stands, tier gating on the package routes holds only for callers who do not pass a buyer_tier other than public, including a value the tier map does not recognize.
Related but separate:
- fix: stop leaking the seller's floor price and concession budget on negotiation reads #95 and fix: stop disclosing the seller's floor price in chat and on every counter-offer #97 concern floor-price disclosure on the negotiation, chat and counter-offer surfaces.
- fix: six mutating routes accept unauthenticated callers #86 concerns unauthenticated mutating routes.
This report concerns how the read routes select the tier view for a caller without a key.
Activity
[-]# GET /packages and GET /packages/{package_id} return exact_price and floor_price to an anonymous caller who adds ?buyer_tier=agency[/-][+]GET /packages and GET /packages/{package_id} return exact_price and floor_price to an anonymous caller who adds ?buyer_tier=agency[/+]