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 routingsupported_profiles on the query tells a Broker which domains you work in

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. The Broker uses this to route queries to capable Exchanges and filter offers to relevant domains.

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 routes your query only to Exchanges that also declare fora-academic-v1, and returned offers 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, role=ROLE_AGENT), 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 declare what your agent is entitled to access. The Exchange filters its catalog by your scopes — resources outside your scopes are not returned.

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, _ := client.Execute(ctx, offer)
for _, q := range resp.SubscriptionQuota {
fmt.Printf("Scope %s: %d/%d remaining (resets %s)\n",
q.Scope, q.Remaining, q.Limit, q.ResetsAt)
}

When a request is denied, the Exchange returns a specific DenialReason so your agent knows what went wrong and can respond appropriately:

Denial ReasonMeaningWhat to Do
DENIAL_REASON_QUOTA_EXCEEDEDSubscription access count exhausted for this periodWait for quota reset or upgrade subscription
DENIAL_REASON_DELEGATION_INVALIDDelegation missing, unverifiable (broken chain linkage or holder-binding mismatch), expired, or it widens scope beyond its parentObtain a fresh delegation from the principal
DENIAL_REASON_SCOPE_INSUFFICIENTYour scopes do not cover this resourceRequest broader scopes or a different resource
DENIAL_REASON_INSUFFICIENT_BALANCERequester’s balance too low for this transactionAdd funds or reduce request scope
DENIAL_REASON_SIGNATURE_INVALIDEd25519 signature verification failedCheck your signing key configuration

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 access FORA via MCP — add the FORA MCP server to your agent’s tool configuration and discover/license content without a custom SDK. The MCP server exposes six tools (discover_supply, execute_transaction, report_usage, dispute_transaction, check_health, get_rate_limit_status) and handles protocol details (signing, field population, response simplification) automatically.

For Claude Desktop, add to your config:

{
"mcpServers": {
"fora": {
"command": "fora-mcp-server",
"args": ["--transport", "stdio"],
"env": {
"FORA_EXCHANGE_URL": "https://exchange.example.com",
"FORA_PROXY_KEY_PATH": "/path/to/ed25519-private.pem"
}
}
}
}

See MCP Server Overview for full details on deployment, authentication, and tool schemas.

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 your verified request signature, never on anything you send. 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. An empty account handle means you hold no account there yet; 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.