Skip to content

FORA for AI Agents

Your agent is building a due-diligence report. It needs a D&B credit report (one API, one auth scheme, one pricing model). It needs a PACER court filing (completely different API). It needs drug interaction data from DrugBank (yet another). Academic literature from Elsevier (yet another). A satellite image of a property from Planet (yet another).

Every integration is bespoke. Every provider has its own discovery mechanism, pricing model, authentication scheme, delivery format, and terms of use. You can’t compare prices across providers. You can’t enforce a unified budget. You can’t verify what you received in a consistent way. And if something goes wrong, every dispute process is different too.

The result: your agent is locked into whichever providers your developers happened to integrate. Adding a new resource type means weeks of custom work. Neither side scales.

One protocol to discover, price, transact, deliver, verify, and dispute any metered resource from any participating Exchange.

// News article
article, _ := client.Fetch(ctx, "https://cdn.fora-protocol.org/premium/article.html")
// Credit report
report, _ := client.Fetch(ctx, "duns:123456789", fora.WithProfile("fora-credit-v1"))
// Drug interaction check
interactions, _ := client.Fetch(ctx, "drugbank://ddi?drugs=DB00682,DB00331")
// Court filing
filing, _ := client.Fetch(ctx, "pacer://case/1:24-cv-01234/doc/42")
// Academic paper
paper, _ := client.Fetch(ctx, "doi:10.1038/s41586-024-07487-w")
// Satellite imagery
imagery, _ := client.Fetch(ctx, "planet://PSScene/20240315_091234_01_2345?aoi=POLYGON((-73.9 40.7,-73.8 40.7,-73.8 40.8,-73.9 40.8,-73.9 40.7))")
// Brain MRI scan
scan, _ := client.Fetch(ctx, "dicom://study/1.2.840.113619.2.5.1762583153.215519.978957063.78")

Same SDK. Same flow. Same budget controls. Same verification. Same dispute process. The protocol handles the differences — your agent doesn’t have to.

1. Your agent wants a resource (article, credit report, court filing -- anything)
2. SDK resolves the resource identifier → finds the Exchange endpoint
3. SDK calls DiscoverResources → gets Offers with pricing, freshness, attestations
4. SDK calls ExecuteTransaction → gets delivery instructions + billing_id
5. SDK fetches resource via signed URL → returns it to your agent
6. SDK calls ReportUsage → reports actual consumption (tokens, pages, minutes, records)

Steps 2-6 happen behind a single Fetch() call. Your agent sees one function. The protocol runs underneath.

The FORA Agent SDK (Go, TypeScript, or Python):

Build a client with your identity and the keys it signs with. The base URL comes from the Exchange’s own /.well-known/fora.json rather than from configuration:

// The endpoint an Exchange serves comes from its own /.well-known/fora.json,
// never from configuration; the same resolver routes usage reports back to
// whichever Exchange issued the offer.
endpoints := resolvers.NewWellKnownEndpointResolver(resolvers.WellKnownOptions{})
client := connect.NewClient(baseURL,
connect.WithSigner(signer), // RFC 9421 request signing; custody stays yours
connect.WithAgentKey(agentPublic), // the public half a bound delivery fetch presents
connect.WithRequester(requester), // who this agent says it is
connect.WithOfferKey(exchangePublic), // the key this Exchange signs offers with
connect.WithEndpointResolver(endpoints),
// The WBA directory where THIS agent publishes its own signing key, as a JWK
// Set at {origin}/.well-known/http-message-signatures-directory. The Exchange
// reads it off the covered Signature-Agent header and looks there for the key
// whose RFC 7638 thumbprint equals the keyid. Signature-Agent is covered
// whether or not it is set, so leaving this out signs an EMPTY value that no
// Exchange can resolve a key from, and the call is refused with a 401 after it
// was routed, signed and sent. Publish the directory before you call.
connect.WithSignatureAgent("https://agent.example"),
)

Which domains you work in is a property of each query rather than of the client, so it goes on the ResourceQuery:

// 1. Discover. Offers arrive already sorted into verified and rejected, and a
// rejected one keeps the reason it was refused.
found, err := client.Discover(ctx, &forav1.ResourceQuery{
Exchange: "exchange.example",
Uris: []string{"https://publisher.example/article"},
// Which domains you work in is a property of the query rather than of the
// client, so it goes here.
SupportedProfiles: []string{
"fora-news-v1", // articles, podcasts, broadcasting
"fora-academic-v1", // journal papers, preprints, datasets
"fora-legal-v1", // legislation, case law, patents
},
})
if err != nil {
return err
}
offers := found.Verified()
if len(offers) == 0 {
return fmt.Errorf("no verifiable offer: %v", found.Rejected())
}

The SDK handles:

  • Endpoint discovery — an Exchange’s API address read from its own fora.json, and usage reports routed back to whichever Exchange issued the offer
  • Offer verification — every offer’s signature checked against the issuing Exchange’s published key before you can buy it, and a rejected offer keeps its reason
  • Ed25519 signing — every request signed with your key under RFC 9421
  • Bound delivery — the delivery URL is tied to your key and the client presents the matching proof of possession, so a copied link fetches nothing
  • Profile fields — supported_profiles on the query tells each Exchange which domain fields you understand

Two things the SDK does not do, both part of a deferred layer: there is no spend cap of any kind, and usage reports are not submitted automatically — you call ReportUsage yourself. See Budget and Usage Reporting.

Your agent declares which domains it understands via supported_profiles on the DiscoveryRequest. A Broker forwards it to every Exchange it queries and does not route by it; each Exchange answers for its own profiles.

Three v1 profiles exist:

ProfileDomainsStandards
fora-news-v1Articles, podcasts, live broadcastsIPTC NewsML-G2, Podcasting 2.0, W3C TDMRep
fora-academic-v1Journal papers, preprints, datasetsCrossRef, OpenAlex, JATS XML, COUNTER 5.1
fora-legal-v1Legislation, case law, patentsELI, ECLI, Akoma Ntoso, WIPO ST.3/ST.16

Exchanges declare conformance in their manifest. When your agent declares supported_profiles: ["fora-academic-v1"], the Broker forwards that to every Exchange it queries, and offers from Exchanges that also declare fora-academic-v1 include profile-specific metadata (e.g., academic.doi, academic.oa_status, academic.version).

If you omit supported_profiles, you get offers from all Exchanges — useful for broad discovery, less useful for domain-specific queries.

Every FORA request carries a Requester message that tells the Exchange who you are and what you can access.

The Requester message is the identity envelope on every ResourceQuery and TransactionRequest. Your SDK builds it automatically from your configuration, but here is what it contains:

FieldWhat You SetExample
idYour agent’s unique identifier"research-bot-42"
domainDomain where your public key is published"agents.acme.com"
typeWhat kind of entity you are (see below)REQUESTER_TYPE_AGENT
scopesWhat you can access["academic:*", "credit:read"]

The Requester carries no signature field. The SDK signs every request with your Ed25519 private key as an RFC 9421 HTTP Message Signature (in the HTTP headers, covering the whole request). The Exchange verifies it against the public key published in your WBA directory (the JWK Set at {domain}/.well-known/http-message-signatures-directory), identified by its RFC 7638 thumbprint (the RFC 9421 keyid).

Choose the type that matches your agent’s operating mode:

TypeWhen to Use
REQUESTER_TYPE_AGENTAutonomous AI agents — LLMs, RAG systems, research bots
REQUESTER_TYPE_HUMAN_TOOLHuman using an AI-powered copilot or assistant
REQUESTER_TYPE_SERVICEEnterprise service accounts — automated pipelines, cron jobs
REQUESTER_TYPE_DELEGATEDAgent acting on behalf of another agent or user (sub-agent)
REQUESTER_TYPE_RESEARCHResearch pipelines — batch data collection, model training

Scopes state what your agent is entitled to. The Exchange presents the terms your stated scopes cover, and terms outside them are not returned. The scopes are your own statement, signed and attributable to your account, and the Exchange does not verify them, so state only what you hold. An Exchange may also require a verified delegation before it presents a scoped term.

Scope PatternMeaning
"subscription:marketdata-2026"Active MarketData subscription
"credit:read"Can access credit reports (read-only)
"academic:*"Full access to academic resources
"*"Unrestricted (public Exchange default)
[] (empty)Exchange applies its default access policy

If your agent delegates to sub-agents, delegation JWTs enable safe permission narrowing without round-tripping to the original issuer. Your agent issues a child delegation JWT (narrowing) from its own token — restricting scopes, lowering spend caps, shortening expiry — and hands the narrowed token to the sub-agent. The sub-agent cannot widen permissions; this is cryptographically enforced.

The child delegation JWT is carried in the Requester.delegation field. Set type to REQUESTER_TYPE_DELEGATED on the sub-agent’s requests.

For the full delegation model — the JWT chain, child delegation JWT examples, and enterprise IAM integration — see Authentication.

When your agent holds a subscription, offers and transaction responses include SubscriptionQuotaInfo entries showing remaining quota for the current period. This lets you check remaining allowance proactively — before committing to a transaction — and track burn-down without a separate API call.

// After a transaction, check remaining quota
resp, err := client.Execute(ctx, offer)
if err != nil {
return err
}
for _, q := range resp.GetSubscriptionQuota() {
resets := "not stated"
if t := q.GetResetsAt(); t != nil {
resets = t.AsTime().Format(time.RFC3339)
}
fmt.Printf("Subscription %s: %d of %d %s remaining (resets %s)\n",
q.GetSubscriptionId(), q.GetQuotaRemaining(), q.GetQuotaLimit(), q.GetUnit(), resets)
}

When an item is denied, the Exchange returns a specific DenialReason so your agent knows what went wrong and can respond appropriately. A denial of an item rides in the response body, on that item’s denial_reason, also when you bought a single offer. Only a refusal of the whole request — for example no account at that Exchange (DENIAL_REASON_ACCOUNT_NOT_REGISTERED) — is an error carrying ErrorDetail.transaction_denial.

Denial ReasonMeaningWhat to Do
DENIAL_REASON_QUOTA_EXCEEDEDSubscription access count exhausted for this periodWait for quota reset or upgrade subscription
DENIAL_REASON_DELEGATION_INVALIDOn a purchase item: your delegation is missing, or the Exchange refused the chain (malformed, chain linkage, issuer or signature, expired or not yet valid by the signed chain, addressed to another Exchange, holder binding, a child that widens its parent, or scopes or caps that do not cover the request). A token over 32768 bytes is refused as an invalid request instead, and at discovery a refused delegation hides the gated terms with no reason. See VerificationObtain a fresh delegation from the principal, bound to the key you sign with
DENIAL_REASON_INSUFFICIENT_BALANCERequester’s balance too low for this transactionAdd funds or reduce request scope
DENIAL_REASON_SIGNATURE_INVALIDA signature the item depends on did not verify: the offer’s, or your request acceptance, including when the items that arrived are not the set you signedCheck your signing key configuration, and that the request carries exactly the offers you signed, in order
DENIAL_REASON_RELAY_NOT_ACCEPTEDThe provider of this offer does not accept purchases relayed by a BrokerBuy this offer directly at its Exchange

Providers set their own prices. FORA normalizes everything to unit_cost (effective cost per unit) so you can compare across providers regardless of their pricing model.

ModelUnitExampleIndustry
Per tokentokens$0.0001/token for news articleMedia
Per articlearticles$1.50 for journal paperAcademic
Per pagepages$0.10/page for court filingLegal
Per minuteminutes$0.05/min for podcast episodeMedia
Per recordrecords$61.99 for credit reportFinancial
Per studystudies$25 for brain MRIMedical
Per sq_kmsq_km$3/sq_km for satellite imageryGeospatial
Per queryqueries$0.50 for drug interaction checkPharma
Subscription—$0/request under enterprise dealAny
Free—$0 with attribution (CC-BY)Academic, Government

You control spend at three levels:

  1. Per-request: “Never pay more than $100 for one credit report”
  2. Per-session: “Cap total spend at $500 for this due-diligence job”
  3. Per-period: “Spend no more than $10,000/month on content across all agents”

If a request would exceed any limit, the SDK returns a BudgetExceededError before making any network call.

Not all resources are static files. FORA classifies resources by mutability, which determines how your agent verifies delivered content:

MutabilityExampleHash BehaviorAgent Action
STATICJournal article, patent filing, archived news storyHash is stable — verify itCompare SHA-256 of received bytes to attested content_hash. Mismatch is disputable.
DYNAMICD&B credit report, DrugBank interaction database, satellite catalogHash changes between offer and fetchDo NOT auto-dispute hash mismatch. Check data_as_of for freshness instead.
LIVEMarketData quote feed, NPR live broadcast, news monitoring streamNo content exists at offer timeNo hash verification. Metering is time-based (minutes, hours).

For dynamic resources, the data_as_of field on the Offer tells you when the data was current. Your agent can set max_data_age to filter out stale offers:

// "I need a credit report updated within the last 7 days"
report, _ := client.Fetch(ctx, "duns:123456789",
fora.WithProfile("fora-credit-v1"),
fora.WithMaxDataAge(7 * 24 * time.Hour),
)

The Broker drops offers where now() - data_as_of exceeds your threshold.

Need multiple resources at once? FetchBatch groups requests by Exchange and executes them in parallel.

A literature review pulling 50 papers across publishers:

dois := []string{
"doi:10.1038/s41586-024-07487-w", // Nature
"doi:10.1126/science.adq3858", // Science
"doi:10.1016/j.cell.2024.03.012", // Cell (Elsevier)
"doi:10.1001/jama.2024.5678", // JAMA
// ... 46 more DOIs
}
papers, _ := client.FetchBatch(ctx, dois, fora.WithProfile("fora-academic-v1"))

A due-diligence package pulling credit, legal, and property records:

results, _ := client.FetchBatch(ctx, []string{
"duns:123456789", // D&B credit report
"pacer://case/1:24-cv-01234/doc/42", // Federal court filing
"pacer://case/2:23-cv-05678/doc/15", // Related proceeding
"county://cook-il/property/12-34-567-890", // Property record
})

The SDK groups identifiers by domain, sends one query per Exchange, and executes batch transactions. Network round trips go from N*M to M (where M is the number of distinct Exchanges).

FORA supports streaming resources (WebSocket, SSE, HLS, Icecast) via DELIVERY_METHOD_STREAMING. The flow is the same — ExecuteTransaction returns a signed URL, but that URL points to a streaming endpoint.

// Connect to a live MarketData quote feed
stream, _ := client.Fetch(ctx, "marketdata://stream/equities/AAPL,MSFT,GOOGL",
fora.WithDeliveryMethod(fora.Streaming),
)
// stream.URL points to a WebSocket endpoint
// stream.Expiry is 2 hours from now
// Usage is metered per minute of connection time

Your agent connects and receives continuous data for the session duration. When the signed URL expires, request a new one through the same FORA flow. The protocol handles session lifecycle — heartbeat, reconnection, and concurrent connection limits are managed by the provider.

Direct mode (default): Your agent talks to each provider’s Exchange directly. Simple. Works for known providers.

Brokered mode: A FORA Broker queries multiple Exchanges in parallel, deduplicates offers (same article on different sites), filters by freshness and profile compatibility, and picks the cheapest or highest-quality source. Better for cost optimization at scale and cross-domain queries.

Every Offer returned by DiscoverResources can carry ResourceAttestation entries — signed claims about the content from a trusted party (provider or verification vendor). Three verification levels determine what your agent can verify:

LevelAttesterWhat You Can Verify
0 — NoneNobodyCDN delivery only (did you get a 200?)
1 — Self-attestedProviderContent hash: compute SHA-256 of received bytes and compare to the attested content_hash
2 — Third-partyVerification vendor (e.g., DoubleVerify)Trust the attestation without re-verifying — an independent party measured the content

At Level 1, your agent can re-compute the content hash from delivered bytes and compare it to the provider’s signed claim. A mismatch is cryptographic proof that the delivered content differs from what was promised.

At Level 2, an independent verification vendor (whose keys are published in its WBA directory at https://{verifier}/.well-known/http-message-signatures-directory) has crawled and measured the content. Your agent trusts the vendor’s attestation without re-verifying.

If something goes wrong — delivery failure, content hash mismatch, token count discrepancy — your agent can file a dispute. The flow requires a usage report first:

1. ReportUsage -> get report_id
2. DisputeTransaction -> reference transaction_id + report_id + evidence
3. Exchange resolves automatically (< 1 second for routine disputes)

What’s auto-disputable at each attestation level:

EvidenceLevel 0Level 1Level 2
CDN delivery failure (4xx/5xx)Auto-creditAuto-creditAuto-credit
Signed URL expired before fetchAuto-creditAuto-creditAuto-credit
Content hash mismatchN/A (no hash)Auto-creditN/A (vendor algorithm)
Token discrepancy + CDN size corroborationFlaggedAuto-creditAuto-credit
Token discrepancy alone (agent-reported)Not auto-creditableNot auto-creditableNot auto-creditable

The report_id is required on every DisputeRequest — the Exchange rejects disputes without a prior usage report on file.

You can also reach FORA over MCP, with no SDK and no key of your own. The Identity Service hosts an MCP endpoint, mints and custodies an Ed25519 key for you, publishes your key directory on a subdomain it gives you, and signs every protocol request on your behalf.

It is a hosted HTTP endpoint rather than a local command, so there is nothing to install and no key file to configure. Point an MCP client at it and it discovers how to authenticate:

https://mcp.demo.fora-protocol.org/mcp

The endpoint answers an unauthenticated request with 401 and the OAuth metadata that says where to send the user. The client registers itself dynamically, the developer signs in, and five tools appear:

ToolVerb
fora_registeropen an account at an Exchange and accept its terms
fora_statuslist this agent’s accounts
fora_discoverfind offers by URL, by query, or by filter
fora_executelicense offers and get the content back
fora_reportfile the usage report an offer’s terms require

When a call is refused, the tool’s error result carries {"refusal": UpstreamRefusal} as its structured content: the party that refused (the Broker, an Exchange or the Identity Service), the Connect code it answered with, and its FORA ErrorDetail unchanged. You branch on the same typed reason an SDK client reads.

Custody is the trade. The service holds your private key and, by default, fetches the licensed bytes for you — which is what makes the delivery URL’s key binding satisfiable at all when you hold no key. An agent that holds its own key and publishes its own directory is the same participant to an Exchange and avoids that trade entirely; the rest of this page is that path.

See Identity Service for the custody model, the key lifecycle, and what the service costs you in trust.

What providers are on FORA? Any provider whose Exchange operator runs a FORA Exchange. The protocol is open — no central registry required.

What if the provider doesn’t use FORA? The SDK returns NoExchangeError. Fall back to your existing content access strategy.

Is this legal? Yes. Every transaction is explicitly authorized by the provider (via their Exchange) and cryptographically signed. You have a receipt for every piece of content.

What about robots.txt? FORA handles inference/RAG access (real-time, user-facing). robots.txt handles training crawlers. Different traffic types.

How does FORA handle resources outside the three v1 profiles? The core protocol is domain-agnostic. Extension profiles add domain-specific metadata but are not required. A Exchange can serve credit reports or satellite imagery without conforming to any profile — the core fields (unit_cost, unit, resource_mutability, data_as_of) handle metering and verification for any resource type. New profiles are added by domain communities as adoption grows.

  1. Register your agent with a participating Exchange — Register in Go, register in TypeScript and Python. The Exchange creates your account and bills it based on what authenticates you, never on anything you send: your request signature on a direct purchase, your acceptances on a purchase a Broker relays, which is yours too (see Accounts). Address the call by naming the Exchange’s bare domain on the request: the SDK resolves the endpoint from that Exchange’s own manifest, reads the terms revision it publishes from a freshly fetched copy, and echoes it so the signature records what you accepted.
  2. Check the account with GetAccountStatus / getAccountStatus before you transact. If you hold no account there yet, the Exchange answers with the Connect code not_found (an empty account handle from an older Exchange means the same); an account that exists but is not active is waiting for the operator to switch it on, and the remedy is to wait or contact them rather than to register again. An account is per-Exchange, so a client can hold several — the same client registers at each.
  3. Install the SDK, declare your supported_profiles, and configure your budget
  4. Replace your per-provider integrations with client.Fetch()

For the full API, see Agent SDK Overview and Fetch Flow.