# qub > Seal your words today. Prove when you said them. qub is a timed commitment system. You compose a message, choose an unlock date, and qub timelock-encrypts it using drand's distributed randomness network. The sealed qub is stored permanently on Arweave. Before the unlock time, nobody — not even qub — can read the content. After the unlock time, anyone with the link can decrypt and verify the message. ## What agents can do - **Create qubs** — seal a message with a future unlock date via API or MCP server - **Create pacts** — stage a bilateral agreement, share the staging link, and co-sign when both parties are ready - **Read qubs** — fetch qub status and content (if unlocked) as structured JSON - **Verify qubs** — cryptographic proof that content existed before the unlock time - **Monitor qubs** — check status, register webhooks for unlock notifications - **Engage with sealed qubs** — increment watch counters, subscribe an email for reveal-time notification - **Manage identity** — link a device to an email via magic link, recover a previously-linked identity for cross-device qub history - **Attest identity** — verify an email against a signing key so authored qubs display a verified identity - **Rotate API keys** — issue a new key with a grace period so deployments can roll over without downtime ## Use cases for AI agents - Provably commit to predictions before outcomes are known - Schedule time-locked announcements or reveals - Create verifiable audit trails with cryptographic timing guarantees - Build workflows that trigger on qub unlock events ## API Base URL: `https://qub.social` ### Authentication There are three authentication paths: 1. **API key** — programmatic access. Pass `Authorization: Bearer qub_sk_...`. Required for `/api/v1/seal`, `/api/v1/webhooks`, and key rotation. 2. **Cloudflare Turnstile** — browser-only. The token is passed in the request body as `turnstile_token`. Used by `/api/v1/upload` and `/api/v1/upload-auth` when no API key is present. 3. **Magic link (email)** — user-facing identity verification. The browser requests a magic link via `/api/v1/auth/magic-link`, the user clicks it, and the resulting `/api/v1/auth/verify` call links a device to an email so the same identity can be recovered on a new device. Tokens are HMAC-signed, single-use, and expire in 15 minutes. This is the path the web app uses; agents typically don't need it unless they're impersonating a browser session. ``` Authorization: Bearer qub_sk_... ``` ### Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/api/v1/qub/{tx_id}` | Optional | Read qub status and content (JSON) | | GET | `/api/v1/qub/{tx_id}/meta` | None | Lightweight metadata read (Arweave block timestamp + intent tag + current watching count); no decrypt. The viewer and the `` iframe re-poll this every 30s during countdown so watching counts climb live; polling halts in the final minute before unlock. | | GET | `/api/v1/qub/{tx_id}/bytes` | None | Raw sealed CBOR bytes (R2 cache-aside, immutable caching). Used by embed + SPA for client-side decrypt. | | POST | `/api/v1/qub/{tx_id}/watch` | None | Increment the pre-reveal "watching" counter for social proof | | POST | `/api/v1/qub/{tx_id}/view` | None | Increment the post-reveal view counter | | POST | `/api/v1/qub/{tx_id}/notify` | None | Subscribe an email to be notified when the qub unlocks | | POST | `/api/v1/qub/{tx_id}/react` | None | Record a `called_it` / `wrong` reaction for a revealed prediction; returns updated tallies | | POST | `/api/v1/seal` | API key | Create a qub (server-side encryption) | | POST | `/api/v1/upload` | API key or Turnstile | Upload pre-sealed CBOR (client-side encryption) | | POST | `/api/v1/upload-auth` | API key or Turnstile | Pre-check upload eligibility | | GET | `/api/v1/entitlements` | None | Check device tier and remaining qubs | | POST | `/api/v1/checkout` | API key or Turnstile | Create Stripe checkout session for qub credits | | POST | `/api/v1/api-keys/checkout` | Turnstile | Create Stripe checkout session for a Builder API key | | GET | `/api/v1/api-keys/provisioned` | None | One-time retrieval of a newly provisioned API key (by Stripe `session_id`) | | POST | `/api/v1/api-keys/rotate` | API key | Rotate the current API key, returns the new secret with a grace period | | POST | `/api/v1/auth/magic-link` | None | Issue a magic-link token to an email + device | | POST | `/api/v1/auth/verify` | None | Verify a magic-link token, link the device, return the identity | | GET | `/api/v1/identity` | None | Read-only lookup of the linked identity for a device | | GET | `/api/v1/creator/qubs` | None | Identity-scoped aggregate of a creator's sealed qubs (oldest, next reveal, total watchers) | | POST | `/api/v1/webhooks` | API key | Register unlock notification webhook | | GET | `/api/v1/webhooks` | API key | List registered webhooks | | DELETE | `/api/v1/webhooks/{id}` | API key | Remove a webhook | | POST | `/api/v1/pact/stage` | API key or Turnstile | Stage a signed pact envelope for co-signing | | GET | `/api/v1/pact/stage/{id}` | None | Review a staged pact's terms and metadata | | POST | `/api/v1/pact/cosign/{id}` | API key or Turnstile | Co-sign a staged pact and trigger upload to Arweave | | DELETE | `/api/v1/pact/stage/{id}` | API key or Turnstile | Retract a staged pact before co-signing (proof-of-possession required) | | POST | `/api/v1/identity/attestation/email/begin` | None | Start email attestation — sends a 6-digit code | | POST | `/api/v1/identity/attestation/email/verify` | None | Verify the 6-digit code to link email to signing key | | DELETE | `/api/v1/identity/attestation/email` | None | Revoke email attestation (proof-of-possession required) | | GET | `/api/v1/identity/attestation/{fingerprint}` | None | Look up the attestation for a public key fingerprint | | POST | `/api/v1/report` | None | Submit a content report for moderation | | GET | `/api/v1/openapi.json` | None | OpenAPI 3.1 specification | | GET | `/api/v1/og/{tx_id}.png` | None | Dynamic Open Graph image (sealed: reveal date + watching count; revealed: called-it percentage). R2 cache-aside. | | GET | `/api/v1/lifecycle/unsubscribe` | None | One-click unsubscribe from creator lifecycle emails (token in query string) | | GET | `/c/{tx_id}` | None | Viewer page (HTML for browsers, OG meta tags for bots, redirects to JSON for `Accept: application/json`) | | GET | `/s/{code}` | None | 7-character short-URL redirect (302) to `/c/{tx_id}`. Allocated at upload and seal time; preferred for share surfaces because it saves ~56 characters vs the full Arweave `tx_id`. | | GET | `/oembed` | None | oEmbed discovery endpoint for auto-embedding qub viewer cards on WordPress, Notion, Medium, Substack | ### Creating a qub (server-side seal) ``` POST /api/v1/seal Authorization: Bearer qub_sk_... Content-Type: application/json { "body": "My prediction: the market will close above 5000 on Friday.", "unlock_at": 1746057600, "sender_label": "Agent Smith" } ``` Response: ```json { "tx_id": "abc123...", "delivery_url": "https://qub.social/c/abc123...", "short_delivery_url": "https://qub.social/s/aB12xYz", "qub_id": "f955de1c...", "unlock_at": 1746057600, "drand_round": 17754411 } ``` The `short_delivery_url` is present when a 7-character base62 short code was allocated at seal time; prefer it for social-channel share surfaces (tweets, QR matrices) because it saves ~56 characters vs the `/c/{tx_id}` form. Both URLs resolve to the same viewer page. Absent if allocation failed transiently — `delivery_url` is always usable as a fallback. Note: The server-side seal endpoint passes plaintext through the server for convenience. For end-to-end encryption where plaintext never leaves your machine, use the MCP server. ### Reading a qub ``` GET /api/v1/qub/{tx_id} ``` Returns JSON with `status: "locked"` (with countdown metadata) or `status: "unlocked"` (with verified body content). ### Webhooks Register a webhook to be notified when a qub unlocks: ``` POST /api/v1/webhooks Authorization: Bearer qub_sk_... Content-Type: application/json { "tx_id": "abc123...", "url": "https://your-agent.example/callback", "secret": "your-hmac-secret-min-16-chars" } ``` All three fields are required. `secret` must be at least 16 characters. The callback receives a POST with the qub data and an `X-Qub-Signature` HMAC-SHA256 header computed with `secret`. ### Notify-me (email subscription) For human-facing flows where a webhook isn't appropriate, an email address can be subscribed to a sealed qub. The Worker's reveal-time cron sends a one-shot email when the qub unlocks. No verification step — best-effort delivery. ``` POST /api/v1/qub/{tx_id}/notify Content-Type: application/json { "email": "alice@example.com", "unlock_at": 1746057600, "locale": "en" } ``` Hard-capped at 1000 subscribers per qub. Rate-limited 5/min/IP. Response is always `{"ok": true}` on success — the user shouldn't be able to enumerate which qubs already have a given subscriber. ### Engagement counters Lightweight social-proof counters. Both endpoints are unauthenticated, rate-limited 10/min/IP, and return the new count. ``` POST /api/v1/qub/{tx_id}/watch → { "count": 42 } // pre-reveal POST /api/v1/qub/{tx_id}/view → { "count": 318 } // post-reveal ``` ### Identity recovery After a magic-link verify, the device → email link is stored server-side. A new browser on the same device can recover the linked identity (and the user's sealed-history index) without re-clicking a magic link: ``` GET /api/v1/identity?device_id=<32-hex> → { "ok": true, "email": "...", "tier": "free", "qubs_remaining": 8, "sealed_history": [...], "locale": "en" } ``` Returns 404 with `{"code": "NOT_LINKED"}` if the device hasn't been linked yet. ### Creator qubs summary Aggregates the creator's `sealed_history` into the three retention-facing facts the `/identity` surface needs in a single round-trip — oldest sealed qub, next upcoming reveal, and total watchers across every qub they've sealed. Same device-id auth surface as `/api/v1/identity`. ``` GET /api/v1/creator/qubs?device_id=<32-hex> → { "oldest_sealed": { "tx_id": "...", "sealed_at": 1700000000, "unlock_at": 1710000000, "intent": "prediction" }, "next_reveal": { "tx_id": "...", "sealed_at": 1720000000, "unlock_at": 1750000000, "intent": "letter", "days_remaining": 42 }, "aggregate_watchers": 1337, "qub_count": 12, "truncated": false } ``` Returns 404 `{"code": "NOT_LINKED"}` when the device isn't linked. `next_reveal` is `null` once every qub has already unlocked. `truncated: true` means the aggregate was computed over the 200 most recent seals (defensive cap — not reached in practice). ### Pact staging (bilateral agreements) A pact is a structured agreement between two parties, sealed as a qub with content type `0x03`. The staging flow: 1. **Party A stages** — sends a signed envelope containing the pact terms (title, key-value terms, party identifiers, optional notes) via `POST /api/v1/pact/stage`. Returns a `staging_id`. 2. **Party B reviews** — fetches the staged pact via `GET /api/v1/pact/stage/{id}`. Returns the CBOR envelope, terms summary, and Party A's public key. 3. **Party B co-signs** — sends their signature via `POST /api/v1/pact/cosign/{id}`. The Worker merges both signatures into the envelope and uploads to Arweave. Returns the `tx_id` and delivery URL. 4. **Retraction** — Party A can retract before co-signing via `DELETE /api/v1/pact/stage/{id}` (proof-of-possession required). Staged pacts expire after 7 days if not co-signed. ### Email attestation Link an email address to a signing key so viewers can see who authored a qub. The flow: 1. **Begin** — `POST /api/v1/identity/attestation/email/begin` with the email, public key fingerprint, and a proof-of-possession signature. Sends a 6-digit code via email. 2. **Verify** — `POST /api/v1/identity/attestation/email/verify` with the code and fingerprint. On success, the attestation is stored and visible to viewers. 3. **Revoke** — `DELETE /api/v1/identity/attestation/email` with a proof-of-possession signature. Removes the attestation. 4. **Lookup** — `GET /api/v1/identity/attestation/{fingerprint}` returns the current attestation for a public key (used by viewers during reveal). ## MCP Server For Claude and other tool-using LLMs, install the qub MCP server: ```json { "mcpServers": { "qub": { "command": "qub-mcp", "env": { "QUB_API_KEY": "qub_sk_..." } } } } ``` The MCP server encrypts locally using the same cryptographic library as the web app — plaintext never leaves your machine. Tools: `create_qub`, `read_qub`, `check_status`. The MCP server is intentionally minimal — it covers the core seal/read/status operations and nothing else. For notify-me, engagement counters, identity linking, webhooks, and key rotation, use the HTTP API directly. ## Protocol - **Encryption:** drand timelock encryption (tlock) using quicknet chain (BLS12-381, 3-second rounds) - **Storage:** Arweave permanent storage (immutable, censorship-resistant) - **Hashing:** SHA3-256 for content integrity (body_hash, qub_id derivation) - **Serialisation:** Canonical CBOR (RFC 8949 deterministic profile) - **Verification:** Any third party can independently verify a qub without qub's cooperation ## Links - Website: https://qub.social - Full bundle (llms.txt + protocol + agent reference, single file): https://qub.social/llms-full.txt - API spec: https://qub.social/api/v1/openapi.json - Protocol spec: https://qub.social/protocol - Sitemap: https://qub.social/sitemap.xml - MCP discovery: https://qub.social/.well-known/mcp.json - Source: https://github.com/svailsa/qub