
hors-luma
HORS is CORS for AI agents—private function sharing governed by human origin
Demo
About
This project is HORS — Human-Origin Resource Sharing — and HORS Party, the first function we published behind it. CORS governs resource sharing between web origins. HORS governs function sharing between human-backed agents. We shipped it as hors-sdk and hors-cli 0.1.0 (https://github.com/cqlyj/hors): wrap your MCP server or HTTP API, write a policy on the function, and any of your agents can call it as you. The answer leaves. The secret stays. The problem is not that agents cannot talk. A2A, MCP and OAuth already let them. The problem is that a private function still costs a key. One super-agent: one leak ruins it. You hand over a key, they know the agent or the app, anyone with the key can call, and the secret leaves with the call. HORS inverts that. You publish a function. They know the human — any of your agents is you; others pass the policy. Don't share the agent. Share the function. hors-sdk is the gate. A policy has an origin (who may call: you, any verified human, the public, or a person), a rule (what else must be true, async, in order, keyed by that human), a challenge (any JSON on a denial; proof comes back in hors/meta), and middleware (audit, cap, redact — after the gate, never instead of it). The one-liner is gated.registerTool("spend", { hors: { origin: kid, rule, use } }, handler). In the slide we ship with the SDK, a kid calls spend(); the card never leaves that host; $20 a week; over that you approve from your phone. The ledger writes who, shop, amount. Never the card. hors-cli is how a human and an agent talk to a horsed function. connect: World App once, pin the human behind this agent. list: unsigned, tools and policies, like a CORS preflight. call --meta: signed, and the way you answer a challenge. mcp: bridge any host — Claude Code, Codex, Cursor, a ChatGPT GPT — so the host never holds the key. One World App scan registers the agent's wallet in World's AgentBook on World Chain. Every later call is a signed envelope hors-sdk resolves to an anonymous humanId. Two agents of one human are one human. A wallet that is not registered is not a human. A new wallet is not a new person. HORS Party is that gate used for commerce, live. horsparty.eth is a Luma event (https://luma.com/o2mvf1me) whose registration is also a HORS-gated MCP service. A party an agent registers for by calling its name. You do not fill the Luma form. You say to your agent: "Reserve a seat for me at https://luma.com/o2mvf1me." In the demo that agent is a ChatGPT GPT. It opens Luma, sees the $1 ticket, then notices this is a HORS Party: agents register through hors-cli, not the Luma button. If your profile is not a verified human yet, it stops, puts the World App QR on screen, and waits for you to scan. That is the human boundary hors-cli refuses to cross for the agent. After the scan it calls claim on horsparty.eth. hors-sdk has already resolved you to a humanId. The first claim is denied PAYMENT_REQUIRED — a HORS challenge carrying an x402 price list, not an error. The agent signs 0.10 USDC or 1 HBAR on Hedera testnet and submits nothing; Blocky402 settles it. The retry with hors/meta.x402 grants the seat, mints a receipt subname, and hands back a free Luma coupon. You open the coupon, the ticket is $0, one click. The demo that already ran: seat 1, receipt seat-1.horsparty.eth, coupon HORS-2SNS, 1 HBAR on HashScan from 0.0.10482581 to 0.0.10482105, facilitator 0.0.7162784 paying the fee. That is the custom-policy unlock we actually shipped: pay as a human, not a wallet. Agents pay in the challenge. HORS keys the receipt to the humanId, not the wallet — the next agent of the same person is free. A script with a key can read seats (public). It cannot buy. The same pattern is why the rest of the slide exists: never give the kid the card; ask a trading agent and keep the edge; prove a live bank session with zkTLS and never put the page on chain; call swap() or supply() and keep the key. Origin, rule, challenge, middleware. The rest is yours. Three chains, one job each, joined by hors-sdk. Sepolia ENSv2 is the name: horsparty.eth points at the service, carries the instructions (agent-context), and hors.remaining. Each guest leaves with {label}.horsparty.eth — "HORS Party · going" — non-transferable, revocable, expiring. Hedera testnet is the money. World Chain is the human. Luma stays the guest list. Guests never need the URL https://horsparty.fly.dev/mcp; hors-cli finds it in the name. Registration closes 2026-10-05T09:00Z. Don't share the secret. Share the function.
How it's made
We built two packages that share a version, then a party that is the first function published behind them. hors-sdk and hors-cli 0.1.0 (https://github.com/cqlyj/hors) are the gate and the client. HORS Party (https://github.com/cqlyj/hors-luma) is a stock McpServer passed to hors() from hors-sdk/mcp. The party does not fork HORS. That is the point of the SDK: you publish a function, you do not hand over a key. hors-sdk is TypeScript, Node ≥ 22.18, native type-stripped config, createGate() at the centre. A caller signs a SIWE envelope ("HORS v1 call") binding host, path, function (urn:hors:fn:…), a hash of the arguments, a nonce and a five-minute expiry. It rides in params.\_meta["hors/auth"] on MCP or in HORS-Authorization on HTTP. The gate verifies eip191 / eip1271 / eip6492, looks the wallet up in AgentBook on World Chain through @worldcoin/agentkit-core (lookupHuman on 0xA23aB2712eA7BBa896930544C7d6636a96b944dA), and applies the function's policy. The handler sees ctx.hors.callerHumanId. The secret — the card, the book, the prompt — never enters the call. Identity fails closed: World Chain down and the human uncached is HORS_UNAVAILABLE. The policy model is what we actually designed. Origin: who may call — same-human, any-human, public, a person, or a function. Rule: what else must be true, async, in order, keyed by that human, under ruleTimeoutMs; returns true or { deny, code, challenge }. Challenge: any JSON on a denial; the caller satisfies it out of band and retries with hors/meta, which is unsigned on purpose — the gate does not vouch for the attachment. Middleware (use): wraps the handler after origin and rules, so a denial never reaches the ledger. The kid-spend snippet in the pitch is not a sketch of a future app; it is the API. origin: kid, rule: week <= 20 or OVER_CAP with an approval challenge, use: ledger.write(callerHumanId, args) and return. The card never left the host. hors-sdk/mcp replaces registerTool so every tool is gated (tools registered before hors() are refused). hors-sdk/http is the same model on fetch or Express. hors-sdk/client signs. hors-sdk/resolvers turns a name, a URL, ens: / .eth (ENSIP-26 agent-endpoint[mcp]) or erc8004:… into https. Policies publish on tools/list and /.well-known/hors.json, like CORS headers; rules stay private. Denials are results, not JSON-RPC errors. hors-cli is the other half of 0.1.0. connect shows a World App QR and pins the human — the one command an agent must never run. list is unsigned. call --meta answers a challenge. hors mcp bridges any host so the host never holds the key: hors_status, hors_list, hors_call. HORS_MOCK=1 keeps signatures real and mocks only AgentBook; it throws under NODE_ENV=production. HORS Party is how we proved the challenge slot is enough for commerce. Three tools, two custom policies, inline. seats is public. claim is any-human plus notClosed, noSeatYet, notSoldOut, paymentVerified. ticket is any-human plus hasSeat. Origin is the bot check — HORS_UNSIGNED / HORS_NOT_HUMAN never reach a party rule. noSeatYet keys on humanId: pay as a human, not a wallet; the next agent of the same person calls ticket() and pays nothing. The hack: x402 is HTTP 402. HORS already owns the tool call, so we refused @x402/mcp and put the protocol objects in the slots we had built. The rule's challenge is { type: "x402", paymentRequired, how } from x402ResourceServer.createPaymentRequiredResponse. The signed PaymentPayload comes back as hors/meta.x402. Settlement is payment in the result. @x402/core is unchanged. how is the name's agent-context, so the agent that has not read the name still gets pay.mjs. We raised ruleTimeoutMs to 30 s because verify runs inside the rule. Partner tech, Hedera. Blocky402 (https://api.testnet.blocky402.com) is the facilitator. initialize() reads /supported for extra.feePayer (0.0.7162784 — never hard-coded). The guest signs with ExactHederaScheme.createPaymentPayload from @x402/hedera and submits nothing; the facilitator submits the transfer and pays the fee. We skipped the x402 client spend-policy engine because it does not know HBAR 0.0.0. pay.mjs is fifteen lines in agent-context.md and refuses any network other than hedera:testnet. Grant then settle in one SQLite BEGIN IMMEDIATE: human_id primary key, then settle. success: false releases the seat. A thrown settle keeps the seat with payment = NULL rather than charge a guest who has no seat. The HashScan in the video is that path: 1 HBAR from the guest to 0.0.10482105, facilitator 0.0.7162784 paying the fee. A documented settlement is 0.0.7162784@1789161315.246414908. Partner tech, ENS. hors-sdk already resolves agent-endpoint[mcp]. rpc.ens points at Sepolia; the resolver is unchanged. The party adds the tree: three Verifiable Factory proxies, ETH registrar, setParent, grantRootRoles, authorizeTextRoles so the hot key may write only hors.remaining. Enhanced Access Control is the multisig. A compromised server can mint junk receipts; it cannot move the name or rewrite the instructions. Receipts: register with role bitmap 0 and expiry = party end + 7 days — non-transferable, revocable, expiring. description = HORS Party · going. The coupon never enters a record. Partner tech, World. The party has no World ID code. hors-cli connect is the human, once. AgentKit's public guide is written around agentkit.fetch; we needed lookupHuman, which lives in @worldcoin/agentkit-core and is not on that page. REGISTRATION.md still says Base; lookups are World Chain. Sandbox proofs never enter AgentBook. hors-sdk absorbs that. The party writes origin: "any-human". Runtime: Node 24, @modelcontextprotocol/server v2, @hono/node-server, viem, zod, @x402/core and @x402/hedera 2.25, node:sqlite, Fly.io app horsparty in syd. One createGate() per process. No web page, no guest-side wrapper, no fakes. A guest has hors-cli, Node, a Hedera testnet account, and the text of the name. Origin, rule, challenge, middleware. The rest is yours.