RSL 1.0 licensing cookbook
July 9, 2026 ยท View on GitHub
Last modified: 2026-07-09
This is the cookbook for expressing a specific license stance via SBproxy YAML and seeing the result in the /licenses.xml document the proxy serves. The reader is a publisher author or counsel who wants the right RSL terms on the wire without writing XML by hand.
If you have not yet wired ai_crawl_control on the origin, read ai-crawl-control.md first. If you want the broader picture (content negotiation, JSON envelope, the projections, transforms), read content-for-agents.md.
What RSL 1.0 expresses
The Really Simple Licensing 1.0 specification (RSL Collective, https://rslstandard.org/rsl) is a machine-readable XML document that asserts the license terms a publisher offers for AI ingestion of their content. Its usage vocabulary covers three tokens:
ai-train. Whether the content may be used as training data for a model. Pay-per-crawl pricing typically attaches here.ai-input. Whether the content may be used as model input at inference time, e.g. as RAG context or as a tool-use payload.search. Whether the content may be indexed for a non-LLM search engine. Often free or nominally priced.
Permitted tokens appear inside a <permits type="usage"> element; prohibited tokens appear inside a <prohibits type="usage"> element. A token that is neither permitted nor prohibited is simply absent, which the spec treats as silence: the document takes no position on that use. SBproxy maps operator YAML onto this vocabulary directly, so the config is the source of truth for the served /licenses.xml.
RSL is cooperative, not enforceable on its own. A motivated agent that ignores the document still gets a 402 challenge from the proxy if it tries to access a priced route. RSL exists so cooperative agents (the ones that pay) and licensing counterparties have a stable, machine-readable artifact to reference.
The mapping
The operator declares the editorial signal via content_signal: at the origin level, as a sibling of action: under origins.<hostname>. Do not put content_signal: inside the ai_crawl_control policy block: the policy deserializer ignores unknown keys, so a signal placed there is silently dropped. (The policy block does accept a different, plural content_signals: field, covered in Recipe 4.)
The proxy translates the origin-level signal into the matching <permits> assertion when it renders /licenses.xml:
content_signal value | RSL element |
|---|---|
ai-train | <permits type="usage">ai-train</permits> |
ai-input | <permits type="usage">ai-input</permits> |
search | <permits type="usage">search</permits> |
| absent | no <permits> or <prohibits> element (the document is silent on usage) |
The "absent" row follows the spec: when no usage token is declared, the document simply says nothing about usage. SBproxy does not synthesize a default-deny element (RSL 1.0 has no element for that; an earlier <ai-use licensed="false"> emission was non-conformant and has been removed). To reserve rights when no signal is declared, the proxy stamps a TDM-Reservation: 1 response header instead, and the /.well-known/tdmrep.json projection serves an empty array.
To express an explicit prohibition in the document body (a <prohibits> element), use the plural content_signals: block on the ai_crawl_control policy; see Recipe 4.
The set of origin-level content_signal values is closed. The config compiler rejects any other value at load time with an error naming the allowed set (ai-train, search, ai-input).
Worked recipes
Each recipe shows the operator's config, the resulting /licenses.xml body, and a short explanation. Every XML body below was generated by running the config through the real renderer:
sbproxy projections render --kind licenses --config ./sb.yml
Run the same command against your config to confirm the output matches before pushing to production. The CLI renders with config version 0; a running proxy substitutes its current reload counter (see "URN format" below).
Recipe 1: Allow training, require attribution
The operator licenses training but wants every downstream model output that uses the content to cite the source. Pricing is per-crawl, with a higher price on /articles/*.
proxy:
http_bind_port: 8080
origins:
"blog.example.com":
content_signal: ai-train
action:
type: proxy
url: https://test.sbproxy.dev
policies:
- type: ai_crawl_control
price: 0.001
currency: USD
tiers:
- route_pattern: /articles/*
citation_required: true
price:
amount_micros: 1000
currency: USD
/licenses.xml:
<?xml version="1.0" encoding="UTF-8"?>
<rsl xmlns="https://rslstandard.org/rsl" version="1.0">
<content url="https://blog.example.com/*">
<license urn="urn:rsl:1.0:blog.example.com:0">
<origin hostname="blog.example.com" />
<payment type="crawl" amount="0.001" currency="USD" />
<permits type="usage">ai-train</permits>
<content-signal>ai-train</content-signal>
</license>
</content>
</rsl>
The <payment> element reflects the policy-level price: / currency: (a positive price maps to type="crawl"; no price maps to type="free"). The <content-signal> element is an SBproxy extension that echoes the operator's declared signal verbatim for the audit trail; spec-aware consumers ignore unknown elements.
The citation_required: true flag does not appear in the RSL document. It propagates to the JSON envelope (citation_required: true) and to the citation_block transform, which prepends a one-line > Citation required ... Source: ... License: ... blockquote to Markdown bodies. RSL captures the licensing posture; the citation requirement rides on the response body and the per-tier citation_required field.
Recipe 2: Allow inference, block training
The operator wants their reference content available to RAG pipelines but not to training jobs. Pricing attaches to /api-reference/*.
proxy:
http_bind_port: 8080
origins:
"docs.example.com":
content_signal: ai-input
action:
type: proxy
url: https://test.sbproxy.dev
policies:
- type: ai_crawl_control
price: 0.0005
currency: USD
tiers:
- route_pattern: /api-reference/*
price:
amount_micros: 500
currency: USD
/licenses.xml:
<?xml version="1.0" encoding="UTF-8"?>
<rsl xmlns="https://rslstandard.org/rsl" version="1.0">
<content url="https://docs.example.com/*">
<license urn="urn:rsl:1.0:docs.example.com:0">
<origin hostname="docs.example.com" />
<payment type="crawl" amount="0.0005" currency="USD" />
<permits type="usage">ai-input</permits>
<content-signal>ai-input</content-signal>
</license>
</content>
</rsl>
The document permits ai-input and says nothing about ai-train or search. Under the spec, silence is not a grant: a cooperative training-job operator that reads the document sees no permission for training and should not include this origin's content in a training set. A RAG pipeline that pays the price and presents the content as model input is operating inside the permitted set. To make the training prohibition explicit in the document body, use the content_signals: block from Recipe 4 instead.
Recipe 3: No signal, default-reserve
The operator does not want any AI use of the origin's content. Pricing is intentionally prohibitive on /*; the policy is effectively a paywall.
proxy:
http_bind_port: 8080
origins:
"private.example.com":
# No content_signal: declared. The document stays silent on usage.
action:
type: proxy
url: https://test.sbproxy.dev
policies:
- type: ai_crawl_control
price: 999999
currency: USD
crawler_user_agents:
- GPTBot
- ClaudeBot
- PerplexityBot
- CCBot
tiers:
- route_pattern: /*
price:
amount_micros: 999999999
currency: USD
/licenses.xml:
<?xml version="1.0" encoding="UTF-8"?>
<rsl xmlns="https://rslstandard.org/rsl" version="1.0">
<content url="https://private.example.com/*">
<license urn="urn:rsl:1.0:private.example.com:0">
<origin hostname="private.example.com" />
<payment type="crawl" amount="999999" currency="USD" />
</license>
</content>
</rsl>
With no signal declared, the license body carries no <permits> or <prohibits> element and no <content-signal> echo. The rights reservation moves to two other layers: every response gets a TDM-Reservation: 1 header, and /.well-known/tdmrep.json serves an empty array. The policy is also defensive on the wire: the tier price ensures that any AI-class user agent that ignores the licensing surface still hits a 402 with an unbuyable price.
Recipe 4: Permit search, prohibit training
To split the usage tier into explicit grants and explicit prohibitions, declare the plural content_signals: block on the ai_crawl_control policy. Each of the three axes (search, ai_input, ai_train) takes true (permit), false (prohibit), or stays absent (silent).
proxy:
http_bind_port: 8080
origins:
"blog.example.com":
action:
type: proxy
url: https://test.sbproxy.dev
policies:
- type: ai_crawl_control
price: 0.005
currency: USD
content_signals:
search: true
ai_train: false
tiers:
- route_pattern: /articles/*
price:
amount_micros: 5000
currency: USD
/licenses.xml:
<?xml version="1.0" encoding="UTF-8"?>
<rsl xmlns="https://rslstandard.org/rsl" version="1.0">
<content url="https://blog.example.com/*">
<license urn="urn:rsl:1.0:blog.example.com:0">
<origin hostname="blog.example.com" />
<payment type="crawl" amount="0.005" currency="USD" />
<permits type="usage">search</permits>
<prohibits type="usage">ai-train</prohibits>
<content-signal>search=yes, ai-train=no</content-signal>
</license>
</content>
</rsl>
search=yes lands in <permits>, ai_train=false lands in <prohibits>, and the undeclared ai_input axis is silent. The same set drives the managed robots.txt Content-Signal: directive, so the two surfaces can never contradict each other.
Note that the license scope is per-origin: the document carries a single <content url="https://<hostname>/*"> element wrapping one <license> body, and the URN is per-origin per config version, not per-route. Per-route <content> grouping is a future extension. For per-route rights observation today, lean on /.well-known/tdmrep.json (which emits one entry per priced route) or split routes onto separate hostnames, one origin per license posture.
URN format
The RSL URN format SBproxy emits is:
urn:rsl:1.0:<origin_hostname>:<config_version>
- The
1.0segment is the RSL spec major version. The proxy re-emits the URN unchanged on every config reload until the spec major-bumps. <origin_hostname>is the bare hostname from the origin'sorigins:key. No port, no scheme, no path.<config_version>is a monotonically increasing in-process reload counter, rendered as a plain decimal integer (0,1,2, ...). It increments on every successful config reload, even when the config content is identical at the byte level, so independent reloads produce distinct URNs. Thesbproxy projections renderCLI always renders version0because no reload has happened in that process.
The URN is the value the JSON envelope's license field carries on every response. It is also the value the operator references in external licensing artifacts (a licensing contract may pin a specific URN as the binding artifact for the agreement period). Counterparties can dereference the URN against the served /licenses.xml to prove that the operator's published terms match the contracted terms at a given point in time.
The URN does not need to be publicly resolvable as an HTTP URL. It is a stable identifier; the proxy stamps it on response bodies and headers but does not register it with any external resolver. Counterparties resolve it indirectly by fetching /licenses.xml from the same hostname and reading the <license urn="..."> attribute.
Validation
The RSL 1.0 specification at https://rslstandard.org/rsl is prose-only; the RSL Collective does not publish a canonical XSD for the document shape. Operators can still well-formedness-check the served /licenses.xml with any XML-aware tool:
# Fetch the document via curl.
curl -s -H 'Host: blog.example.com' http://localhost:8080/licenses.xml > licenses.xml
# Well-formedness check.
xmllint --noout licenses.xml
# Expected: no output (means it parsed cleanly)
The wire format follows the prose spec at https://rslstandard.org/rsl. The emitter tests in crates/sbproxy-modules/src/projections/licenses.rs pin the document shape: the root namespace, the nested <rsl><content url="..."><license>...</license></content></rsl> envelope, the <permits type="usage"> / <prohibits type="usage"> mapping, and a regression guard that no legacy <ai-use> element is ever emitted. Any emitter change that drifts from this shape fails the CI gate.
If the served document does not match what is documented here, open an issue against the SBproxy repo with the served body attached.
Companion documents
- content-for-agents.md: the broader user guide. Covers content negotiation, transforms, the JSON envelope, the other projections (robots.txt, llms.txt, tdmrep.json), and aipref signals.
- ai-crawl-control.md: the
ai_crawl_controlpolicy reference (tiers, pricing, paywall behaviour).
External references:
- RSL 1.0 specification: https://rslstandard.org/rsl
- RSL Collective: https://rslstandard.org/