Rightshold Sign up

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)

FieldMeaning
min_kya_levelMinimum RS-KYA level, L0–L5
require_kyaRequire a valid signed kya+jwt token, not just a claimed handle
require_receiptUploads must include a Rights Receipt
require_clearedThe receipt's verdict must be allowed (default true)
allow_agents / deny_agentsHandles, 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