Skip to content

FORA for Exchange Operators

Resource providers need a transaction layer but don’t want to build one. Courts don’t want to run billing APIs. Publishers don’t want to negotiate with every AI company. Hospitals don’t want to handle metered access to imaging data. Patent offices don’t want to build per-query pricing.

You solve this by running an Exchange: aggregate their catalogs, handle discovery, pricing, and transactions, and earn a margin on every access. This is the aggregator pattern — the same model that drives SSPs in ad-tech, clearinghouses in finance, and distributors in publishing.

FORA was validated across 18 real-world scenarios spanning 10 industries. The same protocol works for all of them:

Exchange TypeWhat You AggregateProvidersExample Operators
Media exchangeNews articles, podcasts, videoPublishersSSPs, media companies
Academic exchangeJournal articles, preprintsPublishers, repositoriesElsevier, OpenAlex
Legal exchangeCourt filings, legislation, patentsCourts, patent officesPACER aggregator, EUR-Lex
Credit/financial exchangeCredit reports, market dataData providersD&B resellers, MarketData
Medical data exchangeClinical data, imagingHospitals, research institutionsTCIA, imaging exchanges
Government data exchangeProperty records, business filingsCounty/state agenciesCoreLogic, ATTOM

The value proposition is the same regardless of domain: providers get revenue from AI agents without building anything, agents get standardized access without bespoke integrations, and you keep a margin on every transaction.

You deploy an Exchange Node — a Go service that handles three API calls:

  1. DiscoverResources — AI agent asks “what’s available at this URL and how much?” Your Exchange looks up the provider’s catalog and returns offers with pricing.
  2. ExecuteTransaction — AI agent says “I’ll take it.” Your Exchange checks the requester’s balance, logs the transaction, and returns a signed URL.
  3. ReportUsage — AI agent reports what it did with the content. Mandatory. You track compliance.

That’s the whole protocol. Three RPCs. Your billing adapter plugs in where Authorize(requester, amount) and Record(transaction) are called.

A media exchange meters in tokens. A legal exchange meters in pages. A medical exchange meters in studies. A geospatial exchange meters in sq_km. Same protocol, different units.

FORA pricing states what is metered and at what price, with no assumption about the unit:

  • unit: what is metered.
  • rate: the price of one unit.
  • estimated_quantity: optional, the publisher’s own estimate, in unit. A metered purchase charges the estimate × rate, or one unit’s rate when the offer states no estimate, and that charge is final (see Charging a metered purchase).
  • unit_cost: the comparison metric, the price of one purchase divided by the resource’s size in the comparison unit of its content type. It never enters the charge.
Exchange Type Metering Unit Example rate
──────────────────────────────────────────────────────────────
Media tokens $0.00003/token
Legal pages $0.50/page
Academic accesses $0.05/access
Medical imaging studies $2.00/study
Credit/financial records $5.00/record
Geospatial sq_km $0.10/sq_km
Streaming (audio) minutes $0.02/minute
Pharma interaction_pairs $0.001/pair

Usage reporting follows the same pattern: consumed_quantity + consumed_unit, which matches the offer’s unit. A report records the quantity consumed and is never refused for it. Your Exchange MUST support these fields on all offers and MUST NOT assume an offer is metered in tokens; only a usage report that omits consumed_unit is read as tokens.

Your Exchange declares which domain profiles it supports in its manifest. This tells Brokers and agents what kind of resources you handle:

// Media exchange
{ "supported_profiles": ["fora-news-v1"] }
// Academic exchange
{ "supported_profiles": ["fora-academic-v1"] }
// Legal exchange
{ "supported_profiles": ["fora-legal-v1"] }
// Multi-domain exchange
{ "supported_profiles": ["fora-news-v1", "fora-academic-v1"] }

Agents declare which profiles they understand, and a Broker forwards that list to your Exchange unchanged; it does not route by profile. When an academic research agent declares fora-academic-v1 and you declare that profile, your offers carry its fields.

Profile conformance means your Exchange sets the required ext fields defined in the profile spec and validates them against the profile’s JSON Schema:

ProfileScopeWhat It Means for Your Exchange
fora-news-v1News, podcasts, broadcastingSet IPTC subject codes, correction status, podcast metadata in offer ext fields
fora-academic-v1Scholarly articles, preprintsSet DOI, OA status, version lifecycle in offer ext fields
fora-legal-v1Legislation, case law, patentsSet jurisdiction, amendment chains, ELI/ECLI in offer ext fields

A Exchange MAY implement zero, one, or multiple profiles. You can start with one domain and expand.

If You’re an SSP: How FORA Maps to Your Infrastructure

Section titled “If You’re an SSP: How FORA Maps to Your Infrastructure”

If you operate an SSP or ad exchange, FORA maps directly to your existing concepts:

Ad-Tech Concept → FORA Equivalent
─────────────────────────────────────────────
SSP / Exchange → Exchange Node
Publisher → Resource Provider
ads.txt → fora.json (authorized providers)
Bid Request → ResourceQuery
Win Notification → TransactionRequest
eCPM → unit_cost (effective cost per unit)
Impression → Content access (signed URL)
Viewability Report → Usage Report

You already have:

  • Provider relationships and contracts
  • Billing infrastructure (invoicing, prepaid, credit)
  • CDN access for provider domains
  • Revenue share agreements

FORA adds:

  • A Exchange service (Go binary, stateless, horizontally scalable)
  • An edge function deployed to provider CDNs (bot blocking + signed URL verification)
  • A resource catalog built from provider rsl.txt files and CMS integrations

For each provider, you:

  1. Generate their fora.json and have them publish it on their origin: the file they serve proves they control the domain and names your Exchange
  2. Ingest their rsl.txt to build a resource catalog with pricing
  3. Configure their pricing tier (the rsl.txt rate is a ceiling; your private rate can be lower)
  4. Authorize catalog_contributors — third-party verification vendors the provider permits to push attestations for their content
  5. If applicable, deploy the FORA edge function to their delivery endpoint (CDN, API gateway, PACS server) for signed URL verification
  6. Done. Their content is now available to AI requesters through your Exchange

Providers publish that one file and deploy nothing else in the managed model. You manage the rest. This is the Exchange-Managed deployment model. Some providers (large publishers, data vendors) may prefer to manage their own delivery endpoint — the protocol supports both.

AI companies register via your existing account management or a self-signup API:

  • Enterprise: Out-of-band key exchange during contract signing. You register their Ed25519 public key.
  • Self-signup: AI agent registers via API with their public key manifest. Per-request billing only (no subscription).

You don’t need bilateral deals with every AI company. Any agent that speaks the FORA protocol can buy from your Exchange.

The Requester message carries no signature: it states who is asking, and the RFC 9421 HTTP Message Signature on the request proves who sent it. Your Exchange MUST verify every request before acting on it. The rules are on the Authentication page; apply them in this order, and reject a request that fails any of them, with no fallback and no degraded mode:

  1. Count the signatures first. Before you verify any, refuse a request carrying more signatures than your WellKnownManifest.max_intermediary_hops with resource_exhausted (HTTP 429) and no typed reason. Every signature counts, whether or not it covers another. See Verification at the Exchange.
  2. Verify each signature under the Web Bot Auth profile. Resolve its keyid, the key’s RFC 7638 thumbprint, in the key directory its own covered Signature-Agent member names ({origin}/.well-known/http-message-signatures-directory), never from Requester.domain. Check the profile parameters, the four components a FORA RPC covers (@method, @target-uri, content-digest, authorization) and the Content-Digest of the body. Refuse a failure as unauthenticated with request_auth_failure: SIGNATURE_MISSING when no signature parses, SIGNATURE_INVALID when one does not verify, SIGNATURE_STALE outside its created/expires window. When a required component is missing, also answer with an Accept-Signature field listing the components you require, so the client can fix its request. See Signature Verification and The Web Bot Auth Profile.
  3. Refuse a replay. A signature you have already accepted is refused as SIGNATURE_STALE. See Replay protection.
  4. Check revocation against the signer’s own directory only. A key is revoked when the revocation list its own directory names lists it. No other party’s list can revoke it, even one that names its thumbprint. See Whose list revokes whose key.
  5. Tell a direct request from a relayed one. On ResourceQuery and TransactionRequest, compare Requester.domain with the host of the directory the covered Signature-Agent member names, by the request-recipient identity rule. Equal is a direct request from the requester itself. Different is a request a Broker authored for that requester, and you act on it only when you admit the signer as a Broker, by your own policy (for example, the Brokers registered with you); otherwise refuse it as unauthenticated with request_auth_failure SIGNATURE_INVALID. A Broker never forwards an agent’s request unchanged: it signs the requests it authors alone. See Direct and relayed requests.
  6. On a purchase, verify the agent’s acceptances. Verify each item’s AgentAcceptance, and the request’s AgentRequestAcceptance, against the Ed25519 keys currently valid in the directory Requester.domain names, never against a Broker’s key. On a relayed purchase these are the agent’s identity and consent, the acceptance key is what the delivery URL is bound to, and a delegation’s cnf.jkt is checked against it. A sub-request that is not the exact in-order projection of the set the agent signed is denied in the body, every item with DENIAL_REASON_SIGNATURE_INVALID. See Projected execute requests.
  7. On Broker-led discovery, Requester.domain is the Broker’s statement. A ResourceQuery a Broker originated carries no agent signature, so nothing authenticates the field there: it says whom the Broker queries for, under the Broker’s own signature.

Cache directories and keys with a TTL and re-fetch them to pick up rotation, without a fetch per request (see Caching Contract).

When a request includes a Delegation message (nested inside Requester), run the chain through the profile’s verifier (helpers.VerifyDelegation in the Go SDK). The procedure, the limits and the checks are stated once, in Verification. This page adds only what your Exchange supplies to the verifier and what it does with the result:

  1. Trust comes from the subscription record. Give the verifier the trusted root keys of the subscription record that owns the gated term, each with the issuer identity it belongs to. Trust is configured on that record, never fetched: not from principal_domain, not from issuer, not from a WBA directory, OIDC or JWKS. A key trusted on one record grants nothing on another. Withdraw trust by removing a key from the record’s list; revocation_uri is not part of the profile.
  2. The holder is the key the request proved. Give the verifier the thumbprint of that key: on a direct request the key that signed it (RFC 9421), on a purchase a Broker relays the key that signed each item’s AgentAcceptance. The verifier refuses a mismatch. This is mandatory: it is what makes a stolen token unusable, so a token possessed without its signing key grants nothing. On a query a Broker originated, which proves possession of no agent key, do not verify or honor the delegation: answer as if none were present.
  3. One outcome per failure. A token over 32768 bytes fails wire validation: the whole request is refused as invalid, before any item is read. At discovery, a chain the verifier refuses is treated as if no delegation were present: the gated terms stay hidden and no denial reason reports why; you MAY journal the refusal. At purchase, a chain the verifier refuses denies that item with DENIAL_REASON_DELEGATION_INVALID.
  4. Use what the verifier returns: the root issuer, the holder thumbprint, the effective scopes and the effective expiry. A gated offer you issue MUST NOT expire later than that effective expiry. A chain that carries a cap claim (fora_max_spend_cents, fora_max_accesses, fora_quota_period) is refused by this release’s verifier; meter subscriptions on the record, as Subscription Quota Signaling describes.

This verification is entirely offline — no callback to the principal is required.

After verifying identity (and any delegation, as above), present the non-gated terms the requester’s stated scopes cover; only those reach the DiscoverResources response. The scopes are the requester’s own statement and you do not verify them. A gated term, a subscription term in the usual case, is different: present it only when the verified delegation’s effective scopes cover it, ignore requester.scopes for that term, and answer a requester without a verified delegation as if the term did not exist. The matching rule is stated once, in Scope Matching.

For a resource that exists but falls outside the requester’s scopes, answer exactly as for a resource you have nothing to offer for: an OfferGroup with no offers and absence_reason unset. Do not reveal that the resource exists, and do not send a reason that tells the two cases apart. This applies to enterprise RBAC and public marketplace subscriptions alike (see Existence hiding).

An offer you presented stays purchasable until it expires, so a purchase is never refused because the requester’s scopes do not cover the resource.

When the requester holds a subscription (identifiable via the requester’s delegation and/or subscription scopes), populate SubscriptionQuotaInfo entries on offers and transaction responses. This proactive quota signaling lets agents track remaining allowance and avoid committing to transactions they cannot complete. Track max_accesses and quota_period from the Delegation message when present.

FORA defines identity- and entitlement-related denial reasons your Exchange MUST return when appropriate. The delegation reasons:

Denial ReasonWhen to Return
DENIAL_REASON_QUOTA_EXCEEDEDSubscription access count exhausted for the current quota_period
DENIAL_REASON_DELEGATION_INVALIDOn a purchase item only: the delegation is missing, or the verifier refuses the chain (malformed, chain linkage, issuer or signature, expired or not yet valid by the signed chain, another recipient, holder binding, a child that widens its parent, or scopes or caps that do not cover the request). Never at discovery, where the gated terms are hidden instead, and never for an oversized token, which wire validation refuses. See Verification

And the entitlement/subscription family, for subscription-gated offers:

Denial ReasonWhen to Return
DENIAL_REASON_ENTITLEMENT_MISSINGNo entitlement token presented for a subscription-only offer
DENIAL_REASON_ENTITLEMENT_MALFORMEDEntitlement token failed to decode
DENIAL_REASON_ENTITLEMENT_EXPIREDEntitlement token’s validity window has passed
DENIAL_REASON_ENTITLEMENT_WRONG_BUYERToken’s subscriber_org does not match the asserted requester
DENIAL_REASON_SUBSCRIPTION_LAPSEDThe covering subscription contract has lapsed
DENIAL_REASON_ENTITLEMENT_NOT_GRANTEDSubscription exists but no buyer-side grant ties this caller to it

And the relay reason, for a provider that does not accept purchases a Broker relays:

Denial ReasonWhen to Return
DENIAL_REASON_RELAY_NOT_ACCEPTEDThe provider that sells this item’s resource does not accept purchases relayed by a Broker. Decided per item by each offer’s provider and answered in the body, never as a refusal of the whole sub-request; the agent can buy the offer directly

This is the identity/entitlement subset with operator guidance; the complete, authoritative DenialReason enum is rendered from the proto on the proto reference. For the full identity model specification — Requester fields, the JWT chain, scope format, and key management — see Authentication.

Your Exchange verifies ResourceAttestation signatures at catalog push time. When a provider or verification vendor pushes resource metadata via CatalogService.PushResources, your Exchange:

  1. Fetches the signer’s public key from their WBA directory (the JWK Set at https://{verifier}/.well-known/http-message-signatures-directory), identified by its RFC 7638 thumbprint (the RFC 9421 keyid)
  2. Verifies the Ed25519 signature over JCS-canonicalized claims
  3. Checks that the verifier is either the provider domain (Level 1 self-attestation) or a vendor listed in the provider’s catalog_contributors (Level 2 third-party attestation)
  4. Rejects attestations from unauthorized contributors

This prevents unauthorized parties from injecting false claims into your resource catalog.

Your Exchange handles DisputeTransaction RPCs from agents. The dispute system operates in three tiers:

TierScopeVolumeResponse Time
Tier 1 — AutomatedDelivery failures, content hash mismatches, signed URL expiry~99% of disputes< 1 second
Tier 2 — Rule-BasedAmbiguous evidence, delivery says 200 but content is suspect~0.9% of disputes< 24 hours
Tier 3 — InvestigationAggregate fraud patterns, systematic abuse~0.1% of disputesCase-by-case

report_id generation: When an agent calls ReportUsage, your Exchange returns a report_id in the UsageReportResponse. This report_id is required on every DisputeRequest — it proves the agent fulfilled its reporting obligation before disputing. Your Exchange rejects disputes without a valid report_id.

Delivery logs are the authoritative evidence source. They cannot be fabricated by the agent and provide independent verification of delivery status, response size, and timing.

As an Exchange operator, you are responsible for two additional capabilities:

Exchange Manifest — Your Exchange serves a machine-readable self-description at /.well-known/fora.json (role=ROLE_EXCHANGE). This tells Brokers and agents what your Exchange supports (pricing models, delivery methods, accepted verifiers, endpoints) without trial-and-error. Your offer-signing keys are published separately in your WBA directory (the JWK Set at /.well-known/http-message-signatures-directory), not in fora.json. The manifest includes:

  • base_currency (ISO 4217 code) — the currency for all unit_cost values. Enables cross-exchange price comparison by the Broker. A legal exchange quoting in EUR and a media exchange quoting in USD can be compared when both declare their currency.
  • supported_profiles — domain extension profiles your Exchange conforms to.

The manifest is generated from your configuration at startup and served as a static JSON document. See Exchange Manifest.

Health Checks — Your Exchange exposes three health layers: gRPC health (grpc.health.v1.Health/Check), HTTP liveness (/healthz), and HTTP readiness (/readyz). Brokers use gRPC health probing to make structured routing decisions — they can distinguish an Exchange with a stale catalog from one with billing down. Kubernetes uses the HTTP endpoints for pod lifecycle management. See Health Checks.

Revenue depends on your exchange domain and the value of the resources you aggregate:

Media exchange:
Provider sets price: $0.05/article (tokens)
AI Agent pays: $0.05/article
You keep: Your standard rev-share margin
Legal exchange:
Provider sets price: $0.50/page (court filing)
AI Agent pays: $0.50/page
You keep: Your standard rev-share margin
Credit exchange:
Provider sets price: $5.00/record (credit report)
AI Agent pays: $5.00/record
You keep: Your standard rev-share margin
Medical imaging exchange:
Provider sets price: $2.00/study (MRI scan)
AI Agent pays: $2.00/study
You keep: Your standard rev-share margin

Higher-value resources mean higher absolute margins per transaction. A credit exchange processing 2K reports/day at $5.00 average = $10K/day gross = $3.6M/year. A media exchange processing 10K articles/day at $0.05 average = $500/day gross = $182K/year. Infrastructure costs scale with RPS, not transaction value — approximately $40/day at 10K RPS.

AI agents can use a Broker to query multiple Exchanges and compare prices — like header bidding but for content. This means:

  • Competitive pressure keeps your margins honest
  • Premium content commands premium pricing (quality signals in offers)
  • Subscription deals give your providers an advantage (zero marginal cost for the requester)
  • base_currency in your manifest enables cross-exchange price comparison even across currencies

NFR targets: 10K RPS, under 100ms p99 latency, under $0.00005/transaction infrastructure cost.

Billing adapter interface (your integration point):

type BillingAdapter interface {
Authorize(ctx, billingRef, amount) → (ok, error)
Record(ctx, transaction) → error
CheckSubscription(ctx, billingRef, providerGroup) → (sub, error)
}
// billingRef is the account handle the Exchange minted at Register and
// resolves on every call from what authenticates the agent: the request
// signature on a direct request, the agent's acceptances on a purchase a
// Broker relays. The account is the agent's, never the Broker's.

Implement these three methods against your existing billing system. The reference implementation uses DynamoDB; production deployments wire it to their real ledger.

Unit-Agnostic Pricing for Non-Text Resources

Section titled “Unit-Agnostic Pricing for Non-Text Resources”

FORA uses a single unit-agnostic pricing model, PRICING_MODEL_PER_UNIT, for all metered resources. Pricing.unit names what is metered, so non-text resources are priced through the same model:

ModelUnitUse Case
PRICING_MODEL_PER_UNITpagesCourt filings, legal documents, PDFs
PRICING_MODEL_PER_UNITminutesVideo, audio, streaming sessions
PRICING_MODEL_PER_UNITrecordsDatabase records, credit reports, medical studies

Pricing is unit-agnostic: unit_cost replaces eCPT, estimated_quantity replaces estimated_tokens, and Pricing.unit specifies the metering unit. Your Exchange MUST support these fields on all offers.

FORA supports DELIVERY_METHOD_STREAMING for real-time connections (WebSocket, SSE, HLS, Icecast). Your Exchange generates a signed URL pointing to a streaming endpoint. The agent connects and receives continuous data. Stream mechanics (heartbeat, reconnection, concurrent connection limits) are managed by the provider, not the protocol.

Streaming transactions follow the same FORA flow, but the signed URL has a longer TTL (streaming sessions last minutes/hours) and usage is reported in time-based units (minutes, hours).

  1. Review the Exchange design for architecture details
  2. See the Billing Adapter for the integration interface
  3. Walk through the Scenario Walkthrough for a complete transaction trace
  4. Review the Threat Model — it was built from 20 years of ad-tech fraud patterns