Clear ExchangeDocs Live demo

Publisher portal and API

Publishers get authenticated, self-serve access to three things, in the API and in the web UI (/app#portal):

  1. Rate cards. View your card and edit the price of each usage tier.
  2. A hard floor per tier. The lowest price you will ever receive for a retrieval. The exchange enforces it in pricing and in clearing.
  3. 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, else 422 price_below_floor. To cut a price below the standing floor, lower the floor in the same request.
  • expected_revision (optional) is the revision you last read. If the card has changed since, the edit is refused with 409 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's price_breakdown shows discounted and 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 parameterMeaning
from, toISO date or date-time. from is inclusive, to exclusive.
intervalday (default), week (Monday start) or month, in UTC. Buckets the time series.
usageOnly this tier.
agent_idOnly this agent.
limit, offsetPage 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_paid is what the agent paid all-in, clearing_fee is the exchange's fee on top, and net is your price, received in full: price_paid = net + clearing_fee on every row. The fee shown is the one each sale was made at (fee_bps per row); current_fee_bps is today's.
  • One block per currency; amounts in different currencies are never added together.
  • by_agent lists the top 50 agents by net and by_content the top 20 items.
  • Agent identity. Agents are identified by the agent_id they declared. The exchange does not yet authenticate agents, so every agent is verified: false and agent_verification.available is false. When agent authentication (Web Bot Auth, see the roadmap) lands, verified agents will carry verified: true and 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

StatusCodeWhen
401publisher_auth_requiredMissing, unknown or revoked publisher key.
404rate_card_not_foundThe card does not exist or belongs to another publisher.
400invalid_requestBad body, amount or query parameter. message names the field.
409revision_conflictexpected_revision is stale. details.revision is current.
422price_below_floorThe edit would leave a tier priced below its floor.
409offer_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.