Skip to content

Commit aaed924

Browse files
Align with web standards (RFC 9110) and future-proof
- 402 responses: add Cache-Control: no-store to all 402 paths (x402 middleware) - Content negotiation: add Vary: Accept on /content/fetch 200 responses - Standardize X-FairFetch-Version header (was X-Fairfetch-Version) in code and docs - OpenAPI: version 0.2.0, document 402/Cache-Control and Vary, reference RFC 9110 - Tests/docs: update version header references to X-FairFetch-Version
1 parent 3895f86 commit aaed924

9 files changed

Lines changed: 26 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ Every successful response includes these headers. Think of them as a receipt and
601601
| `X-PAYMENT-RECEIPT` | Proof that payment was settled. For x402: a transaction hash. For wallets: a ledger transaction ID (`ff_...`). | `0x6d8ce1bf...` or `ff_3a7c9e...` |
602602
| `X-FairFetch-Payment-Method` | How the agent paid: `wallet` (pre-funded account) or `x402` (one-time payment). | `wallet` |
603603
| `X-FairFetch-Wallet-Balance` | Remaining wallet balance after this charge (only present for wallet payments). | `99000` |
604-
| `X-Fairfetch-Version` | Protocol version, so clients know which Fairfetch spec they're talking to. | `0.2` |
604+
| `X-FairFetch-Version` | Protocol version, so clients know which Fairfetch spec they're talking to. | `0.2` |
605605

606606
> [!TIP]
607607
> For a plain-language explanation of all Fairfetch concepts, headers, and terminology, see the [Concepts Guide](docs/CONCEPTS.md).

api/routes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ async def fetch_content(
191191
license_id=grant_header,
192192
)
193193
_attach_preferred_access(md_response, request)
194+
md_response.headers["Vary"] = "Accept"
194195
return md_response
195196

196197
try:
@@ -238,6 +239,7 @@ async def fetch_content(
238239
license_id=grant_header,
239240
)
240241
_attach_preferred_access(json_response, request)
242+
json_response.headers["Vary"] = "Accept"
241243
return json_response
242244

243245

compliance/headers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
X-FairFetch-Origin-Signature — Ed25519 sig of the content body
55
X-FairFetch-License-ID — Usage Grant compact identifier
66
X-FairFetch-Usage-Category — Permitted usage tier (search_engine_indexing, summary, rag, etc.)
7+
X-FairFetch-Version — Protocol version (e.g. 0.2)
78
X-Data-Origin-Verified — EU AI Act origin attestation
89
X-AI-License-Type — Content license terms
910
"""
@@ -40,7 +41,7 @@ def to_dict(self) -> dict[str, str]:
4041
"X-AI-License-Type": self.license_type,
4142
"X-FairFetch-Usage-Category": self.usage_category,
4243
"X-FairFetch-Compliance-Level": compliance.value,
43-
"X-Fairfetch-Version": "0.2",
44+
"X-FairFetch-Version": "0.2",
4445
}
4546

4647
if self.signature:

docs/CONCEPTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ suggestion and get clean, legal content through the API instead.
198198
| `X-AI-License-Type` | "These are the publisher's terms for AI usage." | `publisher-terms` |
199199
| `X-FairFetch-Origin-Signature` | "The publisher digitally signed this content. You can verify it hasn't been tampered with." | `GllQLb/V4Vd+Su...` |
200200
| `X-Content-Hash` | "Here's a fingerprint of the content. Use it to verify integrity." | `sha256:2c449548...` |
201-
| `X-Fairfetch-Version` | "This server speaks Fairfetch protocol version 0.2." | `0.2` |
201+
| `X-FairFetch-Version` | "This server speaks Fairfetch protocol version 0.2." | `0.2` |
202202

203203
### When Payment Is Involved
204204

docs/PUBLISHER_GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ X-FairFetch-Origin-Signature: ...
452452
X-FairFetch-License-ID: ...
453453
X-Content-Hash: sha256:...
454454
X-PAYMENT-RECEIPT: ...
455-
X-Fairfetch-Version: 0.2
455+
X-FairFetch-Version: 0.2
456456
```
457457

458458
If all of these are present, your pipeline is working end-to-end.

openapi.yaml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ info:
66
MCP protocol support, and EU AI Act 2026 compliance.
77
88
## Payment Flow (x402)
9+
Uses HTTP status 402 Payment Required per RFC 9110 (HTTP Semantics).
910
1. Request content without `X-PAYMENT` header → receive `402 Payment Required`
10-
2. Response body contains `accepts` object with price, asset, network, and wallet (price may vary by content URL path when publisher uses route-based pricing)
11+
2. Response body contains `accepts` object with price, asset, network, and wallet (price may vary by content URL path when publisher uses route-based pricing). Responses include `Cache-Control: no-store` and must not be cached.
1112
3. Obtain payment proof via the facilitator
1213
4. Retry with `X-PAYMENT: <proof>` header → receive content
1314
@@ -17,7 +18,7 @@ info:
1718
- `application/ld+json` — JSON-LD article representation
1819
- `text/markdown` — Clean markdown extraction
1920
- `application/json` — Standard JSON response
20-
version: 0.1.0
21+
version: 0.2.0
2122
license:
2223
name: Apache 2.0
2324
url: https://www.apache.org/licenses/LICENSE-2.0
@@ -52,8 +53,13 @@ paths:
5253
- $ref: '#/components/parameters/PaymentHeader'
5354
responses:
5455
'200':
55-
description: Content successfully retrieved and processed
56+
description: Content successfully retrieved and processed. Response varies by Accept (Vary: Accept).
5657
headers:
58+
Vary:
59+
description: Indicates content negotiation by Accept header (RFC 9110)
60+
schema:
61+
type: string
62+
example: Accept
5763
X-Data-Origin-Verified:
5864
$ref: '#/components/headers/X-Data-Origin-Verified'
5965
X-AI-License-Type:
@@ -84,7 +90,7 @@ paths:
8490
schema:
8591
$ref: '#/components/schemas/UrlBlocked'
8692
'402':
87-
description: Payment required — include X-PAYMENT or X-WALLET-TOKEN header
93+
description: Payment required (RFC 9110). Include X-PAYMENT or X-WALLET-TOKEN. Response includes Cache-Control no-store.
8894
content:
8995
application/json:
9096
schema:
@@ -187,7 +193,7 @@ paths:
187193
example: fairfetch
188194
version:
189195
type: string
190-
example: 0.1.0
196+
example: 0.2.0
191197

192198
/compliance/optout:
193199
get:

payments/x402.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
RECEIPT_HEADER = "X-PAYMENT-RECEIPT"
3737
LICENSE_ID_HEADER = "X-FairFetch-License-ID"
3838

39+
# RFC 9110: 402 responses must not be stored by caches (payment state is per-request).
40+
HTTP_402_HEADERS = {"Cache-Control": "no-store", "X-Payment-Required": "true"}
41+
3942

4043
class X402Middleware(BaseHTTPMiddleware):
4144
"""Intercepts requests to paid routes and enforces payment.
@@ -202,6 +205,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
202205
amount_required=effective_price,
203206
shortfall=effective_price - balance,
204207
),
208+
headers=dict(HTTP_402_HEADERS),
205209
)
206210

207211
# --- Standard path: x402 one-time payment ---
@@ -216,7 +220,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
216220
return JSONResponse(
217221
status_code=402,
218222
content=body,
219-
headers={"X-Payment-Required": "true"},
223+
headers=dict(HTTP_402_HEADERS),
220224
)
221225

222226
# When search_engine_indexing but not free, settle at base price (1x)
@@ -237,6 +241,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
237241
effective_price,
238242
verification_error=result.error,
239243
),
244+
headers=dict(HTTP_402_HEADERS),
240245
)
241246

242247
logger.info(

tests/test_compliance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_basic_headers(self):
4040

4141
assert d["X-Data-Origin-Verified"] == "true"
4242
assert d["X-AI-License-Type"] == "publisher-terms"
43-
assert d["X-Fairfetch-Version"] == "0.2"
43+
assert d["X-FairFetch-Version"] == "0.2"
4444

4545
def test_headers_with_signature(self, signer: Ed25519Signer):
4646
sig = signer.sign(b"test content")

tests/test_end_to_end.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def test_paid_response_includes_all_three_pillars(self, client: AsyncClien
6262
assert "X-PAYMENT-RECEIPT" in resp.headers
6363
assert resp.headers["X-PAYMENT-RECEIPT"].startswith("0x")
6464

65-
assert "X-Fairfetch-Version" in resp.headers
65+
assert "X-FairFetch-Version" in resp.headers
6666

6767
async def test_scraper_interception_count_increments(self, client: AsyncClient):
6868
"""Publisher metric: scraper interception count goes up when bots are steered."""

0 commit comments

Comments
 (0)