Skip to content

Rate card is writable but is not read by the quote, deal-booking or negotiation pricing paths #69

Description

@jaanijuk

Summary

GET/PUT /api/v1/rate-card store and return a rate card document under the Pricing tag. Quotes, deal booking and negotiation all take their base price from the product catalogue instead, so changing the rate card has no effect on what the seller quotes or accepts. Neither the rate-card endpoints nor the quote rationale indicate this.

Observed — a stored rate card does not change a quote

Starting state, six entries:

display 12.00 | video 25.00 | ctv 35.00 | mobile_app 18.00 | native 10.00 | audio 15.00

The product under test. Note that the identifier and name suggest CTV, while the catalogue declares the inventory type as display — so display is the rate-card key that applies:

curl -s localhost:8001/products | python3 -c "
import json,sys
p=json.load(sys.stdin); p=p.get('products',p)
x=[i for i in p if i['product_id']=='inv-ctv-apex-series'][0]
print(x['ext']['inventory_type'], x['base_price']['amount_micros']/1e6, x['ext']['floor_cpm'])
"
# display 45.0 38.25

Set display to 99.00, leaving the other five untouched. The request body is a bare array of entries:

curl -s -X PUT localhost:8001/api/v1/rate-card \
  -H "Authorization: Bearer $OPERATOR_KEY" -H "Content-Type: application/json" \
  -d '[{"inventory_type":"display","base_cpm":99.0,"currency":"USD"},
       {"inventory_type":"video","base_cpm":25.0,"currency":"USD"},
       {"inventory_type":"ctv","base_cpm":35.0,"currency":"USD"},
       {"inventory_type":"mobile_app","base_cpm":18.0,"currency":"USD"},
       {"inventory_type":"native","base_cpm":10.0,"currency":"USD"},
       {"inventory_type":"audio","base_cpm":15.0,"currency":"USD"}]'
# HTTP 200

Read back to confirm it stored:

stored: [('display', 99.0), ('video', 25.0), ('ctv', 35.0), ('mobile_app', 18.0), ('native', 10.0), ('audio', 15.0)]

Quote a product whose inventory_type is display:

curl -s -X POST localhost:8001/api/v1/quotes -H "Content-Type: application/json" \
  -d '{"idempotency_key":"probe-0001","product_id":"inv-ctv-apex-series","deal_type":"PD","impressions":1000000}'
"product": {"product_id": "inv-ctv-apex-series", "name": "Apex Premium Series",
            "inventory_type": "display"},
"pricing": {
  "base_cpm":  {"amount_micros": 45000000, "currency": "USD"},
  "final_cpm": {"amount_micros": 45000000, "currency": "USD"},
  "tier_discount_pct": 0.0,
  "volume_discount_pct": 0.0,
  "rationale": "Base price: $45.00 CPM | Final price: $45.00 CPM"
}

The response echoes inventory_type: display, so the applicable rate-card key is not in doubt. Both discount percentages are zero, so nothing else accounts for the figure. $45.00 is the product's own base_cpm; the stored rate card entry for that inventory type was $99.00. The rationale describes $45.00 as the base price without qualification.

Incidentally, the two artefacts do not even share a representation: /products and /api/v1/quotes report price as a Money object in integer micros, while the rate card stores base_cpm as a plain number.

The rate card was restored to its original six entries afterwards.

Observed — no pricing path reads the stored document

rate_card:current has exactly four references in src/, and all four are CRUD on the two interfaces that expose it:

Location Operation
admin.py:306 read, for GET /api/v1/rate-card
admin.py:345 write, for PUT /api/v1/rate-card
mcp_server.py:511 read, for the get_rate_card MCP tool
mcp_server.py:545 write

No pricing service, engine or flow reference was identified in a search of src/. The three pricing paths each take their base price from the product:

Path Source
Quotes — quote_service.py:160-169 avails["estimated_cpm"], derived from the product
Deal booking — deal_service.py:559 catalog_service.priceable_cpm(product)
Negotiation — negotiation_service.py:244 product_data.get("base_cpm", 0)

Floors follow the same pattern: negotiation uses product_data.get("floor_cpm"), and the observed floor of $38.25 for that product matches its catalogue definition rather than anything in the rate card.

Related inconsistency — two tools named get_rate_card return different artefacts

Not required to resolve the pricing question above, but it bears on the same underlying ambiguity about what the rate card is:

  • mcp_server.py:508 — returns the stored document, falling back to a hardcoded default when absent.
  • crew_tools.py:452-468 — GETs /products and constructs a rate card from each product's base_cpm, grouped by inventory_type.

An agent asking for "the rate card" gets the operator's stored document over MCP, and a catalogue-derived view over the AgentCore crew tool. The second reflects what actually prices; the first does not. If both are retained, their names or descriptions could make that distinction explicit — related to, but not required to resolve, the pricing ambiguity above.

Environment

seller-agent v2.4.2, commit e5b367d2b780aa4d0e03b6363d744ba3adf2b221. Also present on main, which is currently the same commit at time of reporting. Python 3.12.3, WSL2 Ubuntu 24.04, storage backend SQLite.

Inferred

Whether the rate card is meant to price. It may be intended as a reference document for agents to read rather than a pricing input, in which case the behaviour is correct and this is a naming and documentation problem rather than a functional one. Nothing I found states the intent either way. The Pricing tag on both endpoints, the write path, and the absence of any note that the document is advisory all point the other way, but that is inference.

Operator expectation is not something I can evidence from the code. The observation is only that an operator who sets a rate card receives HTTP 200, can read the value back, and sees quotes continue at a different price with no indication that the two are unrelated.

Suggested remedy

Not prescribed, since it turns on the intended role of the document.

If the rate card is meant to inform pricing, the three paths above would need to consult it — with a defined precedence between it and product-level base_cpm, which is a design decision rather than a fix.

If it is meant as a reference document only, then saying so in the endpoint description and the OpenAPI summary would remove the ambiguity at much lower cost, and the two get_rate_card tools returning different artefacts would still be worth reconciling.

Acceptance criteria

  • Either a rate card set through PUT /api/v1/rate-card demonstrably affects the price returned by POST /api/v1/quotes for a product of that inventory type, or the endpoint documentation states that the document does not participate in pricing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions