# qub > Seal your words today. Prove when you said them. Even AI can't fake the past. qub lets a person — or an AI agent — write something today that the world can only read at a future moment, and prove later that it was written before that moment arrived. As more of the internet becomes machine-generated, being able to point at something and prove "this existed at that exact moment, and I wrote it" gets more valuable, not less. qub is the simplest tool we know of for doing that. A qub is sealed in the user's (or agent's) browser before anything is uploaded — the platform itself cannot read sealed content, including before the unlock moment arrives. Sealed bytes live in permanent public storage — a decentralised network qub does not run; verification works without qub being online. After the unlock time, anyone with the link can decrypt and verify the qub. Public attribution is **opt-in per qub** — by default a qub goes up unsigned and there is no on-chain link from it back to a creator. Authors who want credit (a "called it" moment, a receipt that they said something first) can attach their fingerprint at seal time. ## What agents can do - **Create qubs** — seal a qub 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 - **Verify creator identity** — look up an author's verified email/handle by public-key fingerprint - **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 - Timestamp ideas, designs, or hypotheses before exposure (`intent: "proof"`) - Seal informal agreements between agents or between an agent and a human (`intent: "commitment"`) - Author reply-qubs that link back to a parent qub via `reply_to` (qub_id inside the encrypted envelope) and `parent_tx_id` (public storage tag) — the conversational viral loop - Create verifiable audit trails with cryptographic timing guarantees - Build workflows that trigger on qub unlock events ## API Base URL: `https://qub.social` ### Authentication Agent integrations authenticate with an API key. Pass it in the `Authorization` header: ``` Authorization: Bearer qub_sk_... ``` Required for `/api/v1/seal`, `/api/v1/webhooks`, and key rotation. For `/api/v1/upload`, `/api/v1/upload-auth`, and the pact endpoints, the API key is also accepted (browser callers without one supply a Cloudflare Turnstile token in the request body as `turnstile_token`). ### 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 (no decrypt). Returns `arweave_block_timestamp`, `intent`, `parent_tx_id` (reply-chain back-link), `author_fingerprint` (Author tag — the lookup key for the §9.5 attestation chain), `watching` (live watch count), `reactions` (called_it / wrong / total tallies for revealed predictions), and `denylisted` (moderation flag the embed iframe gates on). The viewer and the `` iframe re-poll this every 30s during countdown; 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}/verdict-watch` | None | Commit to watching for the verdict on a verdict-bearing qub. Per-device deduped (re-click no-op; unsubscribe doesn't decrement). Body: `{ device_id }`. Returns `{ count, watching }`. | | GET | `/api/v1/qub/{tx_id}/verdict-watch` | None | Read the current verdict-watcher count without modifying the set. Used by the reveal-page 30s poll. | | 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/api-keys/rotate` | API key | Rotate the current API key, returns the new secret with a grace period | | GET | `/api/v1/api-keys/mine` | Magic-link (device_id) | List the API keys owned by the verified email — backs `/developer/keys` | | PATCH | `/api/v1/api-keys/{keyHash}` | Magic-link (device_id) | Update label / cap / IP allowlist / scope / disabled on a key the caller owns | | GET | `/api/v1/handle/{handle}` | None | Public handle reverse lookup. Resolves a handle to its owning fingerprint and attestation record. 60-second edge cache. Used by viewer surfaces and `@handle` autocomplete. | | 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 permanent storage | | DELETE | `/api/v1/pact/stage/{id}` | API key or Turnstile | Retract a staged pact before co-signing (proof-of-possession required) | | POST | `/api/v1/pact/invite/accept` | None | One-click accept of a pact invite from the email link — writes the cosign-enabling marker; the email click proves email control | | GET | `/api/v1/pacts/mine` | None | Cross-device list of staged pacts authored by the device's linked email | | GET | `/api/v1/identity/attestation/{fingerprint}` | None | Look up the attestation for a public key fingerprint | | 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 | `/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 storage `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", "title": "Q1 BTC call" } ``` `title` is optional — a short plaintext label (≤100 NFC code points, no control characters) shown on the viewer countdown before reveal. Bound to `qub_id` via `title_hash`, so a gateway cannot swap it. Omit the field to leave the countdown without a creator-set title. The `Author` tag is **opt-in per qub**: API callers attach their fingerprint by passing `author_fingerprint` (64-char lowercase hex of the signing pubkey, PROTOCOL.md §2.2) on the upload request. Omit the field to seal an unattributed qub — nothing in permanent storage links it to a creator. Attribution is decided per qub and is permanent once written. 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", "intent": "prediction" } ``` 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. `intent` is optional and accepts `prediction | letter | secret | announcement | thesis | commitment | proof | verdict`. The eighth value, `verdict`, is system-emitted — only the `/verdict/{tx_id}` flow produces verdict-tagged qubs; agents should not pass it directly when sealing. When supplied, the reveal email uses an intent-aware subject and body where a per-intent template exists (e.g. "The prediction reveals now." instead of the generic "The qub just revealed."); intents without a per-intent template fall through to the generic copy. Unknown values are silently dropped server-side and fall back to the generic template — sending an unrecognised intent never errors. ### 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 POST /api/v1/qub/{tx_id}/verdict-watch → { "count": 1248, "watching": true } // verdict-bearing qubs only — per-device deduped; body { device_id } GET /api/v1/qub/{tx_id}/verdict-watch → { "count": 1248 } // poll ``` ### 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`. The Worker also emails Party B a link `https://qub.social/p/?t=` carrying an HMAC-signed invite token. 2. **Party B accepts the invite** — clicks the email link. The client posts the token to `POST /api/v1/pact/invite/accept`, which writes the cosign-enabling marker; the email click proves email control. 3. **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. 4. **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 permanent storage. Returns the `tx_id` and delivery URL. 5. **Retraction** — Party A can retract before co-signing via `DELETE /api/v1/pact/stage/{id}` (proof-of-possession required). 6. **Cross-device discovery** — Party A on a new device can fetch their staged pacts via `GET /api/v1/pacts/mine?device_id=` (resolves to the linked email and returns the `pact-by-email:*` author index). Staged pacts expire after 7 days if not co-signed. ### Identity attestation lookup Authors can link a verified email and a personalised handle to their signing key so viewers can see who wrote a qub. Agents can verify that link by looking it up by fingerprint: ``` GET /api/v1/identity/attestation/{fingerprint} → { "fingerprint": "...", "email": "...", "handle": "@alice", "verified_at": 1746057600 } ``` `fingerprint` is the lowercase 64-char hex of `SHA3-256(pubkey)` — the same value returned in the `Author` tag and in `getQubMeta`'s `author_fingerprint` field. Returns 404 if no attestation exists for that fingerprint. Establishing or revoking an attestation is a creator-side flow done in the qub web app (email-confirmation challenge + ML-DSA-65 proof of possession) and is not part of the agent API surface. ## 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 (inner):** drand timelock encryption (tlock) using the quicknet chain (BLS12-381, 3-second rounds). The qub cannot be opened until drand publishes the relevant round signature — qub does not run drand and cannot ask it to publish ahead of time. - **Encryption (outer wrapper):** every upload to permanent storage is also AES-256-GCM-wrapped (PROTOCOL.md §13). The 32-byte wrapper key K rides as the URL fragment of the delivery URL (`#`); browsers do not transmit URL fragments to servers, so neither qub nor any storage gateway is byte-aware of K. Wrapped bytes in permanent storage are byte-indistinguishable from arbitrary ciphertext, closing the bulk-decrypt enumeration channel that the inner protocol layer alone left open. - **Storage:** permanent public storage (immutable, censorship-resistant) — qub does not control it. - **Hashing:** SHA3-256 for content integrity. The 32-byte `qub_id` preimage is 92 bytes covering version, content type, created/unlock timestamps, body hash, and the SHA3-256 of the optional NFC-normalised title (so a gateway cannot swap a title without the qub failing verification). - **Authorship signing:** ML-DSA-65 (FIPS 204, post-quantum) — opt-in, keypair stays on the creator's device; verifiers don't need qub online to check it. - **Public attribution:** opt-in per qub via the `Author` tag. There is no creator-keyed index on the qub side; the public profile at `/u/` is a verified-identity card and does not list a creator's qubs. - **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/qubsocial/qub-public