Status: implemented in PR #6. This issue captures the rationale; the PR is the authoritative description of the final wire shapes. The design evolved during implementation (Pricing redesign, proto-native vocabulary, enum sentinels) — this body has been refreshed to match what shipped.
Problem
Offer.AccessRestrictions modeled access rules as a flat, ad-hoc structure: permitted/prohibited lists for function, geography, and user category. Sufficient for binary allow/deny, but it could not express a complete commercial licensing arrangement in a machine-readable, cross-domain-portable form. Specifically there was no standard way to represent:
- Multi-arrangement resources — a news article free for academic RAG but paid for commercial use: two terms for one resource, not two resources.
- Usage quotas — a stock photo capped at 500,000 impressions, a design file at 1,000 manufactured units. Quotas gate license validity, not billing.
- Post-use obligations — attribution, share-alike, contribution, notice, network-copyleft. Behavioral requirements that exist regardless of whether a fee is charged.
- Reference-only terms — complex licenses (MCPS/PRS, patent portfolios) that can't be enumerated; the governing document is at a URI.
- Offline metering — physical manufacturing events the Exchange can't observe; the agent self-reports.
- Pricing per use case —
FREE for academic, priced for commercial, on the same resource.
Symptom: publishers were abusing PRICING_MODEL_ATTRIBUTION/CONTRIBUTION to carry behavioral obligations, because there was no obligation concept. Attribution is not a payment model.
Model — one resource, many LicenseTerm
Each LicenseTerm is one complete access arrangement, the same shape at ingest (ResourceEntry.terms) and discovery (Offer.terms):
TermSemantics — ENUMERATED (machine fields authoritative; Exchange enforces) or REFERENCE_ONLY (License.uri is authoritative; machine enforcement deferred).
License — uri (canonical identity, never URL-validated), id (SPDX short-id / TollBit cuid), name, immutable.
Restriction — one axis (FUNCTION / GEOGRAPHY / USER_TYPE) with permitted[], prohibited[], critical.
Quota — usage cap gating validity: metric, limit, window (HOURLY/DAILY/MONTHLY/TOTAL).
Obligation — kind (ATTRIBUTION, CONTRIBUTION, SHARE_ALIKE, NETWORK_COPYLEFT, NOTICE, OTHER), trigger, scope_license (required for SHARE_ALIKE), detail.
Pricing — required on every term; model = FREE must be explicit (absent ≠ free).
scopes (Biscuit scope-gating) and part_label (sub-parts) round out LicenseTerm. Every required enum carries _UNSPECIFIED = 0 and is rejected if unset.
Pricing — structure + metering basis
PricingModel is the closed charging structure: FREE, PER_UNIT, FLAT (+ UNSPECIFIED). The open metering basis ("per what") is Pricing.unit, a vocabulary (below). Pricing.metering = ONLINE / NONE / OFFLINE_SELF_REPORTED. There is no revenue-share model — settlement is off-protocol. PRICING_MODEL_ATTRIBUTION/CONTRIBUTION are removed (they are Obligations).
Vocabulary — proto-native, not JSON
The open string axes (Pricing.unit, Quota.metric, and the function/geography/user-type restriction axes) are defined in the proto via custom options (ramp.v1.vocab) / (ramp.v1.vocab_enum) and tooled by buf:
- Validation —
protovalidate CEL: a bare token must be registered, OR vendor:namespaced (unbounded), OR empty. Structural rules + cross-field rules run at the RPC boundary via the Connect validate interceptor — no per-language validation code.
- Constants — a buf plugin emits a typed-constant package per axis (
pricingunits, quotametrics, functiontokens, geographytokens, usertypes) with IsRegistered(), so application code branches on generated constants. Membership is enforced at ingest from this single source.
- Geography registers only the non-ISO specials (
*, EU, EEA); ISO-3166 alpha-2 codes are structural. Growing a registered list is an additive version bump; vendor extensions need no registry change.
This replaces the earlier vocab/*.json registry files (deleted).
Cross-domain examples (final shapes)
- News article — Term 1
{FUNCTION: ai-input (academic), pricing: FREE}; Term 2 {FUNCTION: ai-input+ai-index (commercial_entity), pricing: PER_UNIT unit=accesses $0.05}; both ATTRIBUTION / ON_USE.
- Stock photo —
{FUNCTION: display, GEOGRAPHY: *, pricing: FLAT $1.20, quota: impressions ≤ 500000 / TOTAL, ATTRIBUTION}.
- Patent/CAD —
{FUNCTION: manufacture, pricing: PER_UNIT unit=units-manufactured $0.50 metering=OFFLINE_SELF_REPORTED, quota: units-manufactured ≤ 1000 / TOTAL, CONTRIBUTION}.
- Reference-only —
{semantics: REFERENCE_ONLY, license.uri: <MCPS/PRS terms>, pricing: <stated>}; machine fields deferred to the document.
Problem
Offer.AccessRestrictionsmodeled access rules as a flat, ad-hoc structure: permitted/prohibited lists for function, geography, and user category. Sufficient for binary allow/deny, but it could not express a complete commercial licensing arrangement in a machine-readable, cross-domain-portable form. Specifically there was no standard way to represent:FREEfor academic, priced for commercial, on the same resource.Symptom: publishers were abusing
PRICING_MODEL_ATTRIBUTION/CONTRIBUTIONto carry behavioral obligations, because there was no obligation concept. Attribution is not a payment model.Model — one resource, many
LicenseTermEach
LicenseTermis one complete access arrangement, the same shape at ingest (ResourceEntry.terms) and discovery (Offer.terms):TermSemantics—ENUMERATED(machine fields authoritative; Exchange enforces) orREFERENCE_ONLY(License.uriis authoritative; machine enforcement deferred).License—uri(canonical identity, never URL-validated),id(SPDX short-id / TollBit cuid),name,immutable.Restriction— one axis (FUNCTION/GEOGRAPHY/USER_TYPE) withpermitted[],prohibited[],critical.Quota— usage cap gating validity:metric,limit,window(HOURLY/DAILY/MONTHLY/TOTAL).Obligation—kind(ATTRIBUTION, CONTRIBUTION, SHARE_ALIKE, NETWORK_COPYLEFT, NOTICE, OTHER),trigger,scope_license(required for SHARE_ALIKE),detail.Pricing— required on every term;model = FREEmust be explicit (absent ≠ free).scopes(Biscuit scope-gating) andpart_label(sub-parts) round outLicenseTerm. Every required enum carries_UNSPECIFIED = 0and is rejected if unset.Pricing — structure + metering basis
PricingModelis the closed charging structure:FREE,PER_UNIT,FLAT(+UNSPECIFIED). The open metering basis ("per what") isPricing.unit, a vocabulary (below).Pricing.metering= ONLINE / NONE / OFFLINE_SELF_REPORTED. There is no revenue-share model — settlement is off-protocol.PRICING_MODEL_ATTRIBUTION/CONTRIBUTIONare removed (they areObligations).Vocabulary — proto-native, not JSON
The open string axes (
Pricing.unit,Quota.metric, and thefunction/geography/user-typerestriction axes) are defined in the proto via custom options(ramp.v1.vocab)/(ramp.v1.vocab_enum)and tooled by buf:protovalidateCEL: a bare token must be registered, ORvendor:namespaced(unbounded), OR empty. Structural rules + cross-field rules run at the RPC boundary via the Connectvalidateinterceptor — no per-language validation code.pricingunits,quotametrics,functiontokens,geographytokens,usertypes) withIsRegistered(), so application code branches on generated constants. Membership is enforced at ingest from this single source.*,EU,EEA); ISO-3166 alpha-2 codes are structural. Growing a registered list is an additive version bump; vendor extensions need no registry change.This replaces the earlier
vocab/*.jsonregistry files (deleted).Cross-domain examples (final shapes)
{FUNCTION: ai-input (academic), pricing: FREE}; Term 2{FUNCTION: ai-input+ai-index (commercial_entity), pricing: PER_UNIT unit=accesses $0.05}; bothATTRIBUTION / ON_USE.{FUNCTION: display, GEOGRAPHY: *, pricing: FLAT $1.20, quota: impressions ≤ 500000 / TOTAL, ATTRIBUTION}.{FUNCTION: manufacture, pricing: PER_UNIT unit=units-manufactured $0.50 metering=OFFLINE_SELF_REPORTED, quota: units-manufactured ≤ 1000 / TOTAL, CONTRIBUTION}.{semantics: REFERENCE_ONLY, license.uri: <MCPS/PRS terms>, pricing: <stated>}; machine fields deferred to the document.