Licensing Terms
Overview
Section titled “Overview”FORA introduces the Universal Licensing Core: a structured, cross-domain-portable model for expressing commercial licensing terms on any digital resource. A resource carries zero or more LicenseTerm entries — each term is a complete, self-contained access arrangement. Having multiple terms for a single resource is the normal case, not the exception.
The model was designed to cover a wide range of content types and legal arrangements: news articles, stock photos, academic papers, music recordings, patent design files, datasets, and software. The same proto shapes appear at both ends of the pipeline:
- Ingestion (
ResourceEntry.terms) — publishers declare terms when pushing resources into an Exchange catalog viaCatalogService.PushResources. - Emission (
Offer.terms) — the Exchange projects each term the requester is entitled to onto its own offer. It moves the term’s price intoOffer.pricingand carries the rest of the term on the offer unchanged, with no translation or reformulation. A resource with three eligible terms answersDiscoverResourceswith three offers, one per term.
An offer states its price once, in Offer.pricing. The term inside the offer carries no pricing (rule offer.terms.pricing_unset). Each offer derives from exactly one catalog term, so the offer’s price and the term’s price cannot disagree, and execute charges Offer.pricing.
Why multiple terms per resource?
Section titled “Why multiple terms per resource?”A single resource often has different access arrangements for different use cases. Examples:
- A news article is free for academic RAG but costs $0.05 per access for commercial entities.
- A stock photo can be licensed for editorial use (free with attribution) or commercial use (paid, with a 500,000-impression cap).
- A patent design file can be licensed for viewing ($0) or for manufacturing (per-unit fee with self-reported metering).
One resource entry, multiple terms — each covering a complete, distinct access arrangement.
Term semantics
Section titled “Term semantics”Every LicenseTerm has a TermSemantics value:
Pricing is required on every catalog term regardless of semantics — absent Pricing is a validation error (rule resource_entry.terms.pricing_required), and model = FREE must be stated explicitly (absent Pricing is not free). An agent cannot act on a priceless term, so the machine-readable price is always present even when the rest of the terms are deferred to a document. On an offer that price is Offer.pricing, and the term the offer carries has none.
ENUMERATED — the machine fields (restrictions, quotas, obligations) are the complete, authoritative expression of the term. They must be internally consistent (a term may not contradict itself), are validated and normalized at ingest — the same checks ship in the SDK as a publisher pre-check (ValidateResourceEntry / NormalizeLicenseTerm and their Python and TypeScript twins) — and are surfaced on every offer. Restrictions are not an Exchange-side gate the requester must pass to see a term: discovery returns an offer for every scope-covered term and the agent chooses the offer whose restrictions it can honour, bearing compliance. Restrictions are declared terms the parties agree to when the agent accepts the offer, and the Exchange never enforces them. An Exchange or Broker MAY, as a convenience, pre-filter offers using attributes the requester volunteers (function / geography / user-type), but that is advisory, not enforcement. So if a term declares ai-train prohibited, that prohibition rides on the offer and binds the transaction the agent accepts.
REFERENCE_ONLY — the document at License.uri (which must be present) is the authoritative, complete source for restrictions, quotas, and obligations, and the agent must fetch and read it before using the resource. The machine fields are optional in this mode — the publisher MAY send Pricing alone — but any restrictions/quotas/obligations they do include must be accurate: they MUST NOT contradict the referenced document, and whatever is expressed binds the parties, exactly as under ENUMERATED. Because License.uri is always present for REFERENCE_ONLY, License.uri_digest (a method:hexdigest pin of the document) MUST be present so the agent can verify the fetched bytes match what was offered. The same requirement applies to any ENUMERATED term that carries a uri (mutable or not) — a pinned digest is what makes the referenced document tamper-evident. Pricing is still stated machine-readably. This mode is designed for complex license arrangements (music rights, patent portfolios, MCPS/PRS collective management agreements) whose terms cannot be fully enumerated in a machine-readable form.
Restrictions
Section titled “Restrictions”A Restriction constrains one licensing dimension:
| Kind | What it constrains | Vocabulary source |
|---|---|---|
FUNCTION | What the agent may do with the resource | (fora.v1.vocab_enum) on RESTRICTION_KIND_FUNCTION |
GEOGRAPHY | Where the agent is operating | (fora.v1.vocab_enum) on RESTRICTION_KIND_GEOGRAPHY |
USER_TYPE | What kind of entity the agent represents | (fora.v1.vocab_enum) on RESTRICTION_KIND_USER_TYPE |
The token lists are authored as proto-native buf options on the RestrictionKind enum values (no side-car JSON registry); the functiontokens / geographytokens / usertypes constant packages and their IsRegistered checks are generated from them.
Each restriction carries:
permitted[]— tokens allowed on this axis. Empty = any value permitted.prohibited[]— tokens blocked on this axis. Takes precedence overpermitted[].advisory— whenfalse(the default), the restriction is binding: an agent that cannot evaluate every token in it (including an unknown vendor token) MUST decline the term. Setadvisory: trueto downgrade an unverifiable restriction to non-blocking. (Fail-closed by default — a forgotten flag fails safe.)
A token cannot be both permitted and prohibited on one axis, and that is checked twice, over two readings of the same lists. At the RPC boundary restriction.permitted_prohibited_disjoint compares the tokens as written, because it runs over the request exactly as received. At ingest, after canonicalisation, restriction.canonical_disjoint compares what the fold produced. The second is not a repetition: several tokens have more than one accepted spelling, so permitted: ["scrape"] with prohibited: ["crawl"] — or ["us"] against ["US"] — names one token on both sides while passing the first check. Both refuse the term.
Reading a restriction: a value is in-scope when it matches at least one permitted[] token AND matches none of the prohibited[] tokens. Multiple restrictions on the same term are AND-combined — the agent must satisfy all of them. The agent applies this itself when choosing between the offers a resource yields; the Exchange does not pre-filter terms against requester attributes.
Standard function tokens (rendered from the proto at build, not hand-listed):
all ai-all ai-train ai-input ai-index search crawl text-and-data-mining tts commercial advertising editorial research reproduce distribute modify display sync broadcast stream print manufacture sell
Accepted aliases, canonicalised at ingest: train-ai → ai-train, generative-ai → ai-input, scrape → crawl, tdm → text-and-data-mining, copy → reproduce, adapt → modify, derivative → modify
Standard geography tokens — registered specials (plus structural ISO 3166-1 alpha-2 codes such as US, DE, GB, … and the wildcard * for worldwide):
* EU EEA
Standard user-type tokens:
individual academic non_profit news_publisher broadcaster commercial_entity
Accepted aliases, canonicalised at ingest: personal → individual, business → commercial_entity, enterprise → commercial_entity
Unknown tokens during ingest produce a PushResourcesResponse.warnings[] entry — the term is accepted but flagged. This is forward-compatible: new vocab tokens can be added to the registry and used immediately without breaking existing Exchange deployments. Tokens are canonicalised before the check — RFC 8259 whitespace trimmed, ASCII case folded, and the accepted aliases listed in each table above resolved to their registered token — so a spelling difference is never an unknown token; only a non-ASCII byte is left as it came, because a Unicode fold would turn a homograph into a registered token. Canonicalisation is also what the disjointness check above is measured against at this tier: the fold is what turns two spellings into the one token the term may not carry on both sides.
Quotas
Section titled “Quotas”A Quota caps how much a licensee may consume before a term expires or must be renegotiated. Quotas gate term validity — they are not billing quantities. Billing is handled by Pricing.
Each quota has:
metric— the unit being capped (a registered token from the(fora.v1.vocab)option onQuota.metric, or avendor:namespaced custom).limit— the maximum value in the given window.window—HOURLY,DAILY,MONTHLY, orTOTAL(lifetime cap, never resets).
Standard metric tokens (rendered from the proto at build):
display-words impressions tokens input-tokens units-manufactured accesses copies seats
Example: a stock photo with an impressions cap of 500,000 lifetime uses:
quotas: [{ metric: "impressions", limit: 500000, window: TOTAL }]Obligations
Section titled “Obligations”An Obligation is a post-use behavioral requirement. Attribution and contribution are obligations, not pricing models — they belong here, not in PricingModel.
Each obligation has:
kind— what the agent must do (see table below).trigger— when the obligation activates.scope_license— the license derivatives must be released under, as aLicense(required forSHARE_ALIKE; setidfor an SPDX short-id and/oruri). A referencedurirequires auri_digest, the same swap-protection as any other license reference.detail— human-readable text (recommended for all kinds, e.g., exact attribution wording or notice text; anOTHERobligation without it produces a lint warning).
| Kind | Description |
|---|---|
ATTRIBUTION | Credit the author or publisher whenever the resource is used |
CONTRIBUTION | Good-faith payment — amount suggested, not contractually fixed |
SHARE_ALIKE | Derivatives must be released under the same / compatible license (CC-BY-SA / GPL style); scope_license required |
NETWORK_COPYLEFT | Network service triggers copyleft (AGPL style) |
NOTICE | Include the specified copyright notice |
OTHER | Custom requirement, described in Obligation.detail |
| Trigger | Description |
|---|---|
ON_USE | Triggered on any use |
ON_DISTRIBUTION | Triggered when copies are distributed |
ON_NETWORK_SERVICE | Triggered when served over a network (AGPL) |
ON_DERIVATIVE | Triggered when a derivative work is produced |
Key distinction from the retired pricing model values: attribution is not a payment model. A resource can require attribution and charge per-access. A resource can require contribution and be subscribed. Keeping them separate in ObligationKind allows any combination of pricing and behavioral requirements.
Revenue-share arrangements
Section titled “Revenue-share arrangements”FORA has no revenue-share pricing model, and that is deliberate. PricingModel covers the three charging structures an Exchange can quote, sign, and compare at transaction time — FREE, PER_UNIT, FLAT — each with a concrete unit_cost. A revenue share has no transaction-time price: the rate and its reconciliation are an off-protocol agreement between the agent and the publisher. Encoding it as a pricing model would either be a bare label with no rate (the agent still has to ask “what share?”) or would pull the commercial terms of that agreement into the signed, cross-exchange-comparable Pricing — which the protocol never enforces and would make offers no longer price-comparable.
Instead, a revenue-share arrangement is expressed with primitives already in the core, exactly like a subscription:
- A
Pricing{model: FREE}term — there is no per-access charge. - Gated by an agreement scope, e.g.
scopes: ["revshare:publisher-x"]. The agent that has signed the deal states the matching scope in its signed request (Requester.scopes); an agent that does not state it never sees the term. - A
ReportingObligation(the offer-level reporting requirement) so usage is reported. Revenue is then settled off-protocol from those reports.
A scope gate decides which terms are presented; it is not access control. The Exchange does not verify Requester.scopes, so an agent that states the scope without being party to the deal also sees the term. Its purchase is attributable to its account, and reconciling purchases against the deal’s parties exposes it. A publisher that needs the term withheld from such an agent sells it through an Exchange that requires a verified delegation granting the scope before it presents the term (see Scope-Based Access).
So “pay per crawl or enter the revenue-share deal” is just two LicenseTerms on the same resource: a public PER_UNIT term and a scope-gated FREE term. They reach the agent as two offers, and the agent buys one of them. This is the same shape as the academic dataset subscription example below.
For CoMP interop there is nothing to carry: canonical CoMP V1 has no revshare field, and the fora-comp-v1 profile’s keys are flat dotted keys in ext (comp.package_id, comp.countries, …), like every other profile’s — see CoMP extension.
Metering modes
Section titled “Metering modes”Pricing.metering (field 9, PricingMetering) controls how usage is tracked after a transaction:
| Mode | Description |
|---|---|
ONLINE | Default. Exchange tracks usage events in real time. ReportUsage is required. |
NONE | One-time perpetual sale. No ongoing metering; billing_id is issued at ExecuteTransaction and the ledger entry is closed. No ReportUsage required. |
OFFLINE_SELF_REPORTED | Agent self-reports physical-world consumption (e.g. units manufactured from a licensed design). Exchange audits. |
NONE is appropriate for stock images with perpetual licenses, font licenses, one-time CAD file purchases, and any resource where the entire commercial relationship is settled at the time of the transaction.
OFFLINE_SELF_REPORTED is appropriate for IP licenses where consumption events happen outside the digital domain: a design file licensed for manufacturing, a recipe licensed for commercial production, a musical composition licensed for physical pressing.
Metered pricing: rate, unit and estimate
Section titled “Metered pricing: rate, unit and estimate”A PER_UNIT price is metered: the agent pays per unit. You state the rate and the unit, and you may also state an estimate:
Pricing.estimated_quantity— the estimate, in the price’sunit. It is optional, on the term you push and on the offer. APER_UNITprice may state a rate with or without an estimate; whether to state one is your decision, and nothing requires it. An estimate that an offer states is positive (theoffer.metered.estimate_positiverule), and the SDK verifiers reject a zero one. The estimate counts the price’s own unit: a price per access has an estimate in accesses, a price per token an estimate in tokens. The estimate describes the resource rather than the arrangement, so you can state it once onResourceEntry.estimated_quantity, and the Exchange puts it on the offer’s pricing. An estimate on the term’s own pricing takes precedence. AFREEorFLATprice needs no estimate.
A metered purchase charges estimate × rate, or one unit’s rate without an estimate, and the charge is final. A disagreement about the quantity consumed is a dispute. Charging a metered purchase has the rule and an example.
pricing: { model: PER_UNIT, unit: "tokens", rate: 0.00002, currency: "USD", estimated_quantity: 2500 } # the purchase charges 2500 x 0.00002Cross-domain examples
Section titled “Cross-domain examples”News article — two terms
Section titled “News article — two terms”A tech news publisher offers the same article under two arrangements:
Term 1 — free for academic RAG:
semantics: ENUMERATEDrestrictions: - kind: FUNCTION, permitted: ["ai-input"], prohibited: ["ai-train"] - kind: USER_TYPE, permitted: ["academic"]obligations: - kind: ATTRIBUTION, trigger: ON_USE, detail: "Cite TechCrunch at the original URL"pricing: { model: FREE }Term 2 — paid for commercial entities:
semantics: ENUMERATEDrestrictions: - kind: FUNCTION, permitted: ["ai-input", "ai-index"] - kind: USER_TYPE, permitted: ["commercial_entity"]pricing: { model: PER_UNIT, unit: "accesses", rate: 0.05, currency: "USD" }obligations: - kind: ATTRIBUTION, trigger: ON_USE, detail: "Cite TechCrunch at the original URL"An academic RAG agent matches Term 1. A commercial legal research platform matches Term 2. The Exchange emits one offer per term, each priced from the term it derives from: Term 2’s offer carries pricing: { model: PER_UNIT, unit: "accesses", rate: 0.05, currency: "USD", estimated_quantity: 1 }, one access, and the term on that offer carries no pricing. The agent picks the applicable offer, signs it, and executes it.
Stock photo — impressions quota + perpetual license
Section titled “Stock photo — impressions quota + perpetual license”A stock photo agency licenses a photo for commercial display:
semantics: ENUMERATEDrestrictions: - kind: FUNCTION, permitted: ["display"] - kind: GEOGRAPHY, permitted: ["*"]pricing: { model: FLAT, rate: 1.20, currency: "USD", metering: NONE }quotas: - { metric: "impressions", limit: 500000, window: TOTAL }obligations: - kind: ATTRIBUTION, trigger: ON_USE, detail: "© GettyImages. All rights reserved."metering: NONE means the requester pays once and the ledger closes immediately — no ReportUsage required. The impressions quota is a contractual cap, not an online-enforced limit.
Patent design file — manufacture function + offline metering
Section titled “Patent design file — manufacture function + offline metering”A design licensing firm licenses a 3D-printable part:
semantics: ENUMERATEDrestrictions: - kind: FUNCTION, permitted: ["manufacture"] - kind: GEOGRAPHY, permitted: ["*"]pricing: { model: FLAT, rate: 200.00, currency: "USD", metering: OFFLINE_SELF_REPORTED }quotas: - { metric: "units-manufactured", limit: 1000, window: TOTAL }obligations: - kind: CONTRIBUTION, trigger: ON_USE, detail: "Suggested royalty: $0.50 per unit above 1,000"The $200 fee covers a license to manufacture up to 1,000 units. The agent self-reports manufacturing runs. The Exchange audits. Above 1,000 units, the agent is expected to negotiate a new term or pay the suggested contribution.
Licensed music track — complex rights
Section titled “Licensed music track — complex rights”A music rights holder for a sync license:
Term 1 — machine-enumerable sync rights:
semantics: ENUMERATEDrestrictions: - kind: FUNCTION, permitted: ["sync"] - kind: GEOGRAPHY, permitted: ["*"]pricing: { model: FLAT, rate: 500.00, currency: "USD", metering: NONE }obligations: - kind: NOTICE, trigger: ON_DISTRIBUTION, detail: "℗ 2024 Example Records. Licensed under MCPS license MR-7741."Term 2 — full MCPS/PRS terms by reference:
semantics: REFERENCE_ONLYlicense: uri: "https://www.mcps-prs-alliance.co.uk/license/MR-7741" name: "MCPS/PRS Blanket Sync License MR-7741" uri_digest: "sha256:3b1f8e0c…"pricing: { model: FLAT, rate: 500.00, currency: "USD" }Pricing is required even on a REFERENCE_ONLY catalog term — the human-readable terms live in the referenced document, but the machine-readable price is always stated. On the offer, it is Offer.pricing.
The Exchange emits an offer for each. A sophisticated agent reads the REFERENCE_ONLY offer’s term to verify the machine-enumerable Term 1 matches the authoritative document before committing to either offer.
Academic dataset — seat quota + share-alike
Section titled “Academic dataset — seat quota + share-alike”A data provider licenses a research dataset:
semantics: ENUMERATEDscopes: ["subscription:institution"]restrictions: - kind: FUNCTION, permitted: ["ai-train", "ai-input"] - kind: USER_TYPE, permitted: ["academic", "non_profit"]pricing: { model: FREE }quotas: - { metric: "seats", limit: 10, window: MONTHLY }obligations: - kind: SHARE_ALIKE, trigger: ON_DISTRIBUTION, scope_license: { id: "CC-BY-NC-SA-4.0" }, detail: "Trained models using this dataset must be released under CC-BY-NC-SA 4.0"The institutional subscription is settled off-protocol; on-protocol, the term is presented to an agent whose request states the subscription:institution scope (the term is model: FREE because there is no per-access charge). An Exchange that requires proof presents it only with the subscribing institution’s delegation JWT, which carries the matching scope. Up to 10 researchers in the subscribing institution can use the dataset. Any model trained on this dataset that is released publicly must carry the same license.
Vocabulary registry
Section titled “Vocabulary registry”The open vocabulary axes are proto-native — each registered token list is authored once, as buf custom options directly on the field or enum value that uses it (no side-car JSON registry):
- function / geography / user-type tokens —
(fora.v1.vocab_enum)on theRESTRICTION_KIND_FUNCTION/RESTRICTION_KIND_GEOGRAPHY/RESTRICTION_KIND_USER_TYPEenum values - quota metrics —
(fora.v1.vocab)onQuota.metric - pricing units —
(fora.v1.vocab)onPricing.unit
The protoc-gen-foravocab buf plugin reads these options structurally and generates a typed constant package per axis (functiontokens, geographytokens, usertypes, quotametrics, pricingunits) with an IsRegistered membership check, an Aliases map and a Canonical lookup; the constants, the membership check, the aliases and the validation all derive from the single authored list and cannot drift. Aliases are (fora.v1.vocab_enum_alias) entries beside the tokens (train-ai=ai-train, generative-ai=ai-input, scrape=crawl, tdm=text-and-data-mining, copy=reproduce, adapt=modify, derivative=modify; personal=individual, business=commercial_entity, enterprise=commercial_entity), rendered in the tables above from the same descriptor option. Geography registers only the non-ISO specials (*, EU, EEA); ISO 3166-1 alpha-2 codes are conventional and checked at ingest. On the wire, Restriction.permitted/prohibited tokens carry only a structural bound (1–64 chars, no whitespace/control characters) — code-level membership is an ingest concern, consistent with the rest of the licensing invariants.
Unknown bare restriction tokens at ingestion produce PushResourcesResponse.warnings[] entries — they do not cause rejection. This allows publishers to use new vocabulary tokens (or vendor: namespaced customs) before the registry is updated, at the cost of a non-enforced restriction on older Exchange deployments. The metering axes are not in that bargain: an unregistered bare Pricing.unit or Quota.metric is a hard reject, because the metering basis is what an agent is charged or capped by and no transactable offer can be made on an unknown one.