For websites · free · CC0 snippets
Rightshold Gate — free agent-traffic gate for websites
Decide which AI agents may use your API, forms or upload endpoints, and require a valid Rights Receipt for anything they submit. Free, no key, CC0 snippets you paste into your stack.
How it works
Agents send an RS-KYA token in RS-KYA-Token (or a claimed handle in RS-Agent), and uploads carry a Rights Receipt. Your site asks POST https://www.rightshold.com/v1/gate/check with your policy, or verifies offline against https://www.rightshold.com/.well-known/jwks.json. Allow decisions are cacheable for 5 minutes.
curl -s https://www.rightshold.com/v1/gate/check -H 'content-type: application/json' \
-d '{"policy":{"min_kya_level":"L1","require_receipt":false},"agent":"<handle or RS number>"}'
Policy (schema: https://www.rightshold.com/schemas/gate-policy-v1.json)
| Field | Meaning |
|---|---|
min_kya_level | Minimum RS-KYA level, L0–L5 |
require_kya | Require a valid signed kya+jwt token, not just a claimed handle |
require_receipt | Uploads must include a Rights Receipt |
require_cleared | The receipt's verdict must be allowed (default true) |
allow_agents / deny_agents | Handles, RS numbers or agent ids |
The answer: { "decision": "allow" | "deny", "allow": bool, "reasons": [...], "kya": {...}, "receipt": {...}, "cache_seconds": 300 }. MCP: gate_check.
Express / Node middleware
// rightshold-gate.mjs — CC0. Node 18+ (global fetch). No dependencies.
const GATE = "https://www.rightshold.com/v1/gate/check";
const cache = new Map(); // token|receipt -> { until, result }
export function rightsholdGate(policy = { min_kya_level: "L1" }, opts = {}) {
const failOpen = opts.failOpen ?? false; // if Rightshold is unreachable: allow (true) or 503 (false)
return async function (req, res, next) {
const kya_token = req.get("RS-KYA-Token") || undefined; // kya+jwt from the agent
const agent = req.get("RS-Agent") || undefined; // claimed handle / RS number (weaker)
const receipt = req.body?.rights_receipt || undefined; // envelope as issued, for uploads
const asset_sha256 = req.body?.asset_sha256 || undefined;
const key = JSON.stringify([kya_token, agent, receipt?.receipt?.id, asset_sha256]);
const hit = cache.get(key);
let result = hit && hit.until > Date.now() ? hit.result : null;
if (!result) {
try {
const r = await fetch(GATE, { method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ policy, kya_token, agent, receipt, asset_sha256 }), signal: AbortSignal.timeout(2500) });
result = await r.json();
if (result.cache_seconds) cache.set(key, { until: Date.now() + result.cache_seconds * 1000, result });
} catch { if (failOpen) return next(); return res.status(503).send("Rightshold Gate unreachable; try again shortly"); }
}
req.rightshold = result;
if (result.allow) return next();
res.status(403).set("Link", '<https://www.rightshold.com/gate>; rel="help"').json({ error: "rightshold_gate_denied", reasons: result.reasons });
};
}
// app.use("/api/agents", express.json(), rightsholdGate({ min_kya_level: "L2", require_receipt: true }));
Offline Rights Receipt verification (Node, JWKS)
// verify-receipt.mjs — CC0. Verifies a Rights Receipt with no call to Rightshold except a cached JWKS fetch.
import { createPublicKey, verify, createHash } from "node:crypto";
const canon = (v) => v === null || typeof v !== "object" ? JSON.stringify(v)
: Array.isArray(v) ? "[" + v.map(canon).join(",") + "]"
: "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}";
let jwks = null, jwksAt = 0;
async function keys() {
if (!jwks || Date.now() - jwksAt > 3600_000) { jwks = await (await fetch("https://www.rightshold.com/.well-known/jwks.json")).json(); jwksAt = Date.now(); }
return jwks.keys;
}
export async function verifyReceipt(env, fileBytes /* optional Buffer */) {
const r = env.receipt, reasons = [];
const jwk = (await keys()).find((k) => k.kid === r.kid);
const ok = !!jwk && verify(null, Buffer.from(canon(r)), createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: jwk.x }, format: "jwk" }), Buffer.from(env.signature, "base64"));
if (!ok) reasons.push("signature_invalid");
if (r.spec !== "rights-receipt/1") reasons.push("unsupported_spec");
if (Date.parse(r.expires_at) <= Date.now()) reasons.push("expired");
if (fileBytes && createHash("sha256").update(fileBytes).digest("hex") !== r.asset.sha256) reasons.push("asset_hash_mismatch");
return { valid: reasons.length === 0, cleared: reasons.length === 0 && r.verdict === "allowed", reasons, conditions: r.conditions };
}
// Ledger inclusion (optional, online): POST https://www.rightshold.com/v1/receipts/verify with the envelope.
Cloudflare Worker / edge
// Cloudflare Worker — CC0. Gates /api/* by RS-KYA level before your origin sees the request.
const POLICY = { min_kya_level: "L1" };
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (!url.pathname.startsWith("/api/")) return fetch(request);
const kya_token = request.headers.get("RS-KYA-Token") || undefined;
const agent = request.headers.get("RS-Agent") || undefined;
const cacheKey = new Request("https://gate.cache/" + encodeURIComponent(JSON.stringify([kya_token, agent])));
let res = await caches.default.match(cacheKey);
if (!res) {
res = await fetch("https://www.rightshold.com/v1/gate/check", { method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ policy: POLICY, kya_token, agent }) });
res = new Response(res.body, res); res.headers.set("cache-control", "max-age=300");
ctx.waitUntil(caches.default.put(cacheKey, res.clone()));
}
const result = await res.json();
if (!result.allow) return Response.json({ error: "rightshold_gate_denied", reasons: result.reasons }, { status: 403 });
return fetch(request);
},
};
Python: FastAPI dependency and Flask hook
# rightshold_gate.py — CC0. pip install httpx
import time, json, httpx
GATE = "https://www.rightshold.com/v1/gate/check"
_cache: dict[str, tuple[float, dict]] = {}
def check(policy: dict, kya_token=None, agent=None, receipt=None, asset_sha256=None) -> dict:
key = json.dumps([kya_token, agent, (receipt or {}).get("receipt", {}).get("id"), asset_sha256])
hit = _cache.get(key)
if hit and hit[0] > time.time():
return hit[1]
r = httpx.post(GATE, json={"policy": policy, "kya_token": kya_token, "agent": agent,
"receipt": receipt, "asset_sha256": asset_sha256}, timeout=2.5).json()
if r.get("cache_seconds"):
_cache[key] = (time.time() + r["cache_seconds"], r)
return r
# FastAPI
from fastapi import Depends, HTTPException, Request
def rightshold_gate(policy: dict):
def dep(request: Request):
r = check(policy, request.headers.get("RS-KYA-Token"), request.headers.get("RS-Agent"))
if not r["allow"]:
raise HTTPException(403, {"error": "rightshold_gate_denied", "reasons": r["reasons"]})
return r
return dep
# @app.post("/upload", dependencies=[Depends(rightshold_gate({"min_kya_level": "L2"}))])
# Flask
# @app.before_request
# def gate():
# r = check({"min_kya_level": "L1"}, request.headers.get("RS-KYA-Token"), request.headers.get("RS-Agent"))
# if not r["allow"]: return {"error": "rightshold_gate_denied", "reasons": r["reasons"]}, 403
nginx auth_request pattern
# nginx — CC0. auth_request sends a body-less subrequest, so point it at a tiny local sidecar
# (the Express middleware above mounted on 127.0.0.1:8787 at GET /auth) that calls the Gate and caches answers.
location /api/ {
auth_request /_rightshold_gate;
error_page 403 = @rightshold_denied;
proxy_pass http://app_upstream;
}
location = /_rightshold_gate {
internal;
proxy_pass http://127.0.0.1:8787/auth;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header RS-KYA-Token $http_rs_kya_token;
proxy_set_header RS-Agent $http_rs_agent;
proxy_cache_valid 200 403 5m; # with a proxy_cache zone keyed on $http_rs_kya_token
}
location @rightshold_denied { default_type application/json; return 403 '{"error":"rightshold_gate_denied","help":"https://www.rightshold.com/gate"}'; }
# sidecar (Node): app.get("/auth", rightsholdGate({ min_kya_level: "L1" }), (req, res) => res.sendStatus(204));
Notes
- Fail closed or open is your choice (
failOpenin the Express snippet). - A claimed handle without a token is unauthenticated; use
require_kya: truefor anything that matters. - Snippets are CC0. Spec: https://www.rightshold.com/standards/rights-receipt
