Publisher portal and API
Publishers get authenticated, self-serve access to three things, in the API and in the web UI (/app#portal):
- Rate cards. View your card and edit the price of each usage tier.
- A hard floor per tier. The lowest price you will ever receive for a retrieval. The exchange enforces it in pricing and in clearing.
- Earnings. What sold, to which agents, at what price, the clearing fee and your net, over time.
A publisher only ever sees and changes its own data. Everything the portal shows comes from the endpoints below, so anything you can click you can script.
Getting a key
Publishers sign in with an API key. An operator issues it (it needs the operator token, so there is no way for a publisher to mint a key for someone else):
curl -s -X POST https://app.clearexchange.ai/v1/admin/publishers/pub_harbour_ledger/keys \
-H "Authorization: Bearer $ADMIN_TOKEN" -d '{"label":"finance team"}'
{
"key_id": "pkid_3f9a1c2e",
"publisher_id": "pub_harbour_ledger",
"label": "finance team",
"created_at": "2026-10-28T17:00:00.000Z",
"key": "pk_Qm3...",
"note": "Store this key now; it is not shown again."
}
The key is shown once. The exchange stores only its SHA-256 hash (publisher-keys.jsonl in VAR_DIR), so a leaked data volume does not leak keys. Use it as a bearer token:
curl -s https://app.clearexchange.ai/v1/publisher/me -H "Authorization: Bearer pk_Qm3..."
Operators can list a publisher's keys (GET /v1/admin/publishers, ids and labels only) and revoke one (DELETE /v1/admin/publishers/{publisher_id}/keys/{key_id}); a revoked key stops working at once. A key opens only /v1/publisher/*, and the operator token does not open it either. Any other request gets 401 publisher_auth_required.
Rate cards
GET /v1/publisher/rate-cards
Your cards as the exchange prices with them now (your edits applied).
{
"rate_cards": [
{
"rate_card_id": "rc_harbour_ledger_std",
"rate_card_ref": "rc_harbour_ledger_std@v4",
"revision": 1,
"last_edited_at": "2026-10-28T17:05:00.000Z",
"currency": "USD",
"supply_id": "vertical-finance",
"tiers": [
{ "usage": "grounding_only", "price": "0.004", "price_micros": 4000, "floor": null, "floor_micros": null },
{ "usage": "display_snippet", "price": "0.008", "price_micros": 8000, "floor": "0.005", "floor_micros": 5000 },
{ "usage": "full_text", "price": "0.02", "price_micros": 20000, "floor": null, "floor_micros": null }
],
"default_terms": { "citation_required": true, "retention_seconds": 86400, "ai_training": "prohibited" },
"rules": [ { "id": "markets-premium", "match": { "sections": ["markets"] }, "multiplier_bps": 15000 } ],
"note": "..."
}
]
}
GET /v1/publisher/rate-cards/{rate_card_id} returns one card. A card that belongs to another publisher is 404 rate_card_not_found, the same as one that does not exist.
PATCH /v1/publisher/rate-cards/{rate_card_id}
Change the price and/or floor of any tiers. Prices are decimal strings with at most six decimals, in the card's currency. "floor": null removes a floor. Tiers you do not mention are untouched.
curl -s -X PATCH https://app.clearexchange.ai/v1/publisher/rate-cards/rc_harbour_ledger_std \
-H "Authorization: Bearer pk_Qm3..." \
-d '{"tiers":{"display_snippet":{"price":"0.009","floor":"0.006"}},"expected_revision":1}'
The response is the updated card, as above. Rules:
- The edit is atomic: all of it applies, or none.
- After the edit every tier must have
price >= floor, else422 price_below_floor. To cut a price below the standing floor, lower the floor in the same request. expected_revision(optional) is therevisionyou last read. If the card has changed since, the edit is refused with409 revision_conflict, so two people editing do not overwrite each other.- You may add a tier the card does not have (give it a price). You cannot remove one.
- Unknown fields and unknown tiers are
400 invalid_request.
An edit takes effect on the next query. Each edit bumps the card's version (rate_card_ref becomes rc_harbour_ledger_std@v5), and offers and entitlements pin to the version they were priced with. Prices are for the publisher: you receive them in full, and the clearing fee is added on top for the buyer.
What you edit is the tier price and floor. The licence terms and pricing rules are read-only here: saving a price or floor never changes them, and a request that tries to is refused with 400. They are set by the card's source for now; the rules are shown, read-only, so you can see what the floor applies after.
GET /v1/publisher/rate-cards/{rate_card_id}/revisions
The audit trail: every edit with its time, the key that made it (by, a key id) and the tiers it set.
Hard floors
A floor is a promise that the exchange will never sell your content for less. It is enforced in two places:
- Pricing. For every tier,
price = max(floor, min(card price after rules, offered price)). A pricing rule (an archive discount, say) or a discount your own source offers on an item cannot take the price below the floor; the offer'sprice_breakdownshowsdiscountedand the price that results. Offers are priced at the floor, not dropped. - Clearing. Accepting an offer re-checks the publisher price against the floor in force now. If you raised your floor after an agent was offered a price below it, accepting fails with
409 offer_below_floor; the agent queries again and gets the new price. A sale that was agreed at or above the floor is honoured at the price agreed.
The exchange's own behaviour never discounts: the only ways a price falls below your card are a rule you wrote or a discount your source offered, and both stop at the floor.
A floor is also a field of the rate card schema (floor_micros on a tier), so a card supplied by a source can declare one.
Earnings
GET /v1/publisher/earnings
What sold, newest first, with totals and breakdowns. All filters are optional.
| Query parameter | Meaning |
|---|---|
from, to | ISO date or date-time. from is inclusive, to exclusive. |
interval | day (default), week (Monday start) or month, in UTC. Buckets the time series. |
usage | Only this tier. |
agent_id | Only this agent. |
limit, offset | Page through sales (default 50, at most 500). |
{
"publisher_id": "pub_harbour_ledger",
"interval": "day",
"from": null,
"to": null,
"fee_basis": "on_top",
"current_fee_bps": 1000,
"agent_verification": { "available": false, "note": "Agent identities are as claimed ..." },
"currencies": [
{
"currency": "USD",
"totals": { "sales": 42, "gross": "0.3696", "gross_micros": 369600, "fee": "0.0336", "fee_micros": 33600, "net": "0.336", "net_micros": 336000 },
"series": [ { "period": "2026-10-28", "period_start": "2026-10-28T00:00:00.000Z", "sales": 42, "gross": "0.3696", "...": "..." } ],
"by_usage": [ { "usage": "display_snippet", "sales": 40, "net": "0.32", "...": "..." } ],
"by_agent": [ { "agent_id": "research-assistant-7", "verified": false, "last_sale_at": "2026-10-28T16:49:00.000Z", "sales": 12, "net": "0.1", "...": "..." } ],
"by_content": [ { "content_id": "hl-2026-0388", "sales": 9, "net": "0.07", "...": "..." } ]
}
],
"sales": {
"total": 42, "limit": 50, "offset": 0,
"items": [
{
"sold_at": "2026-10-28T16:49:00.000Z",
"entry_id": "led_...",
"content_id": "hl-2026-0388",
"usage": "display_snippet",
"agent_id": "research-assistant-7",
"agent_verified": false,
"rate_card_ref": "rc_harbour_ledger_std@v4",
"currency": "USD",
"price_paid": { "amount": "0.0132", "amount_micros": 13200 },
"clearing_fee": { "amount": "0.0012", "amount_micros": 1200 },
"fee_bps": 1000,
"net": { "amount": "0.012", "amount_micros": 12000 }
}
]
}
}
price_paidis what the agent paid all-in,clearing_feeis the exchange's fee on top, andnetis your price, received in full:price_paid = net + clearing_feeon every row. The fee shown is the one each sale was made at (fee_bpsper row);current_fee_bpsis today's.- One block per currency; amounts in different currencies are never added together.
by_agentlists the top 50 agents by net andby_contentthe top 20 items.- Agent identity. Agents are identified by the
agent_idthey declared. The exchange does not yet authenticate agents, so every agent isverified: falseandagent_verification.availableisfalse. When agent authentication (Web Bot Auth, see the roadmap) lands, verified agents will carryverified: trueand their verified identity here, with no change to the shape. - Like the ledger it is built from, this records paid retrievals; no money moves in the MVP.
Errors
| Status | Code | When |
|---|---|---|
| 401 | publisher_auth_required | Missing, unknown or revoked publisher key. |
| 404 | rate_card_not_found | The card does not exist or belongs to another publisher. |
| 400 | invalid_request | Bad body, amount or query parameter. message names the field. |
| 409 | revision_conflict | expected_revision is stale. details.revision is current. |
| 422 | price_below_floor | The edit would leave a tier priced below its floor. |
| 409 | offer_below_floor | (Agents, on accept.) The offer is priced below a floor raised since it was made. |
Operator endpoints for keys: GET /v1/admin/publishers, POST /v1/admin/publishers/{publisher_id}/keys (404 publisher_not_found for an unknown publisher), DELETE /v1/admin/publishers/{publisher_id}/keys/{key_id}.
Storage
Edits are an append-only log (rate-card-revisions.jsonl in VAR_DIR) laid over the cards the sources supply, so they survive restarts and deploys and are the audit trail. They apply to the card with that rate_card_id; removing the log reverts to the supplied cards. See Configuration and deploy.