From 3197d612438f0089a4ae624632caf6472506ea78 Mon Sep 17 00:00:00 2001 From: felirami <6752178+felirami@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:38:04 +0000 Subject: [PATCH] chore: sync farcasterorg source snapshot --- src/data/farcasterorg-sources.json | 62 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/data/farcasterorg-sources.json b/src/data/farcasterorg-sources.json index bd6d405..97799b8 100644 --- a/src/data/farcasterorg-sources.json +++ b/src/data/farcasterorg-sources.json @@ -8,7 +8,7 @@ "publicRepoCount": 4, "createdAt": "2026-02-15T23:02:53Z" }, - "sourceUpdatedAt": "2026-06-04T21:59:38Z", + "sourceUpdatedAt": "2026-08-09T14:25:25Z", "publicNode": { "baseUrl": "https://haatz.quilibrium.com", "infoEndpoint": "/v1/info", @@ -58,11 +58,11 @@ "isEmpty": false, "primaryLanguage": "Rust", "license": "GPL-3.0", - "stars": 60, - "forks": 22, + "stars": 61, + "forks": 23, "openIssues": 13, - "pushedAt": "2026-06-04T21:59:38Z", - "updatedAt": "2026-06-04T21:59:43Z", + "pushedAt": "2026-08-09T14:25:25Z", + "updatedAt": "2026-08-09T14:26:20Z", "latestRelease": { "tagName": "v0.11.8", "name": "v0.11.8", @@ -449,7 +449,7 @@ "sourcePath": "src/playground.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/playground.md", "slug": "playground", - "contentHtml": "

API playground #

\n

Run real requests against a Hypersnap node without leaving this page. Every \"Try it\" panel in the docs uses the same machinery you see here — this page just collects one example per endpoint family in one place.

\n

How it works #

\n\n
\n

Everything happens in your browser. The docs are a static site. \"Connect wallet\" opens your wallet extension directly. \"Run\" issues a fetch from your browser to the configured Hypersnap host. The docs site is never in the middle and never sees your keys, signatures, or responses.

\n
\n

Public reads — no wallet needed #

\n

User lookup #

\n
\n

Cast by hash #

\n
\n

Following feed #

\n
\n

Trending feed #

\n
\n

User search #

\n
\n

Signed management — wallet required #

\n

These are the endpoints your custody key signs for. Click Connect wallet once, set your FID in the host bar, and every panel below will sign + submit when you click Run.

\n

Create a webhook #

\n
\n

List your webhooks #

\n
\n

Rotate a webhook signing secret #

\n
\n

Delete a webhook #

\n
\n

Register a mini app #

\n
\n

List your mini apps #

\n
\n

Rotate mini-app send secret #

\n
\n

Send a notification — per-app x-api-key #

\n

This endpoint authenticates with your mini app's send_secret, not your custody key. Paste the secret directly into the x-api-key field below — nothing is persisted, nothing is sent anywhere except to the Hypersnap host you configured.

\n
\n

Be careful pasting production secrets into a web page. For development + testing it's fine; for production sends, call the endpoint from your own backend.

\n
\n
\n

Running as a mini app #

\n

The docs themselves can run inside a Farcaster client as a mini app. When loaded that way, Connect wallet talks to the client's embedded wallet via the Farcaster mini-app SDK, and your FID is already known. See Run as a mini app for the one-line manifest that makes this work.

\n

Troubleshooting #

\n", + "contentHtml": "

API playground #

\n

Run real requests against a Hypersnap node without leaving this page. Every \"Try it\" panel in the docs uses the same machinery you see here — this page just collects one example per endpoint family in one place.

\n

How it works #

\n\n
\n

Everything happens in your browser. The docs are a static site. \"Connect wallet\" opens your wallet extension directly. \"Run\" issues a fetch from your browser to the configured Hypersnap host. The docs site is never in the middle and never sees your keys, signatures, or responses.

\n
\n

Public reads — no wallet needed #

\n

User lookup #

\n
\n

Cast by hash #

\n
\n

Following feed #

\n
\n

Trending feed #

\n
\n

User search #

\n
\n

Signed management — wallet required #

\n

These are the endpoints your custody key signs for. Click Connect wallet once, set your FID in the host bar, and every panel below will sign + submit when you click Run.

\n

Create a webhook #

\n
\n

List your webhooks #

\n
\n

Rotate a webhook signing secret #

\n
\n

Delete a webhook #

\n
\n

Register a mini app #

\n
\n

List your mini apps #

\n
\n

Rotate mini-app send secret #

\n
\n

Send a notification — per-app x-api-key #

\n

This endpoint authenticates with your mini app's send_secret, not your custody key. Paste the secret directly into the x-api-key field below — nothing is persisted, nothing is sent anywhere except to the Hypersnap host you configured.

\n
\n

Be careful pasting production secrets into a web page. For development + testing it's fine; for production sends, call the endpoint from your own backend.

\n
\n
\n

Running as a mini app #

\n

The docs themselves can run inside a Farcaster client as a mini app. When loaded that way, Connect wallet talks to the client's embedded wallet via the Farcaster mini-app SDK, and your FID is already known. See Run as a mini app for the one-line manifest that makes this work.

\n

Troubleshooting #

\n", "contentMarkdown": "# API playground\n\nRun real requests against a Hypersnap node without leaving this page. Every \"Try it\" panel in the docs uses the same machinery you see here — this page just collects one example per endpoint family in one place.\n\n## How it works\n\n- **Host.** The panels default to `https://haatz.quilibrium.com`, the public node. Change the Host field on any panel and it's remembered for the rest of your session.\n- **FID.** Signed endpoints need to know which FID you're acting as. Set it once in the host bar and every signed panel picks it up.\n- **Wallet.** The **Connect wallet** button talks to any EIP-1193 provider: MetaMask, Frame, Rabby, a Ledger via Frame, or the wallet embedded in a Farcaster client if you've loaded this page as a mini app. Signed requests use `eth_signTypedData_v4` — the library never sees your private key. Nothing is sent anywhere except directly to the node you configured.\n- **Keccak-256.** The `requestHash` field inside the EIP-712 payload is computed locally with a vendored pure-JS Keccak-256 (self-tested at load time). There is no external JS dependency loaded at runtime.\n\n> **Everything happens in your browser.** The docs are a static site. \"Connect wallet\" opens your wallet extension directly. \"Run\" issues a `fetch` from your browser to the configured Hypersnap host. The docs site is never in the middle and never sees your keys, signatures, or responses.\n\n## Public reads — no wallet needed\n\n### User lookup\n\n
\n\n### Cast by hash\n\n
\n\n### Following feed\n\n
\n\n### Trending feed\n\n
\n\n### User search\n\n
\n\n## Signed management — wallet required\n\nThese are the endpoints your custody key signs for. Click **Connect wallet** once, set your FID in the host bar, and every panel below will sign + submit when you click Run.\n\n### Create a webhook\n\n
\n\n### List your webhooks\n\n
\n\n### Rotate a webhook signing secret\n\n
\n\n### Delete a webhook\n\n
\n\n### Register a mini app\n\n
\n\n### List your mini apps\n\n
\n\n### Rotate mini-app send secret\n\n
\n\n## Send a notification — per-app x-api-key\n\nThis endpoint authenticates with your mini app's `send_secret`, not your custody key. Paste the secret directly into the `x-api-key` field below — nothing is persisted, nothing is sent anywhere except to the Hypersnap host you configured.\n\n> Be careful pasting production secrets into a web page. For development + testing it's fine; for production sends, call the endpoint from your own backend.\n\n
\n\n## Running as a mini app\n\nThe docs themselves can run inside a Farcaster client as a mini app. When loaded that way, Connect wallet talks to the client's embedded wallet via the Farcaster mini-app SDK, and your FID is already known. See [Run as a mini app](./guides/run-as-miniapp.md) for the one-line manifest that makes this work.\n\n## Troubleshooting\n\n- **\"No Ethereum provider found\"** — install a wallet extension (MetaMask / Frame / Rabby) or open the page inside a Farcaster client.\n- **`401 signature mismatch`** — your FID's custody address on-chain doesn't match the key you just signed with. Check your wallet is on the right account.\n- **`401 clock skew too large`** — your device clock is off. Fix the clock and retry.\n- **`403 not the owner`** — you're trying to look up or mutate a webhook/app owned by a different FID. Set the FID that owns the resource, or create your own.\n- **`429`** — you hit the per-FID cap (default 25 webhooks / 25 mini apps). Delete something or ask your operator to raise the cap.\n- **Browser CORS error** — the host you configured either isn't CORS-enabled or isn't a Hypersnap node. `haatz.quilibrium.com` is CORS-open for all documented routes.\n", "toc": [ { @@ -591,7 +591,7 @@ "sourcePath": "src/concepts/authentication.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/concepts/authentication.md", "slug": "concepts/authentication", - "contentHtml": "

Signed operations (EIP-712) #

\n

Management requests — creating or modifying webhooks, registering a mini app, rotating a secret — are authenticated by having the FID's custody address sign a small structured payload using EIP-712 typed data. Hypersnap reads the custody address from on-chain IdRegistry state, so whoever controls the key controls the FID's resources, no extra enrollment step.

\n

What gets signed #

\n

All signed endpoints use the same typed-data shape:

\n
Domain:\n  {\n    name:    \"Hypersnap\",\n    version: \"1\",\n    chainId: 10\n  }\n\nType:\n  HypersnapSignedOp(\n    string  op,           // operation name (see list below)\n    uint64  fid,\n    uint256 signedAt,     // unix seconds\n    bytes32 nonce,\n    bytes32 requestHash   // keccak256(raw HTTP body bytes)\n  )\n
\n

The requestHash is keccak256 of the raw bytes of the HTTP body — no JSON re-canonicalization. This means the client and server hash the same bytes exactly.

\n

Required HTTP headers #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
HeaderValue
X-Hypersnap-FidYour FID (decimal string).
X-Hypersnap-OpThe operation name — see the table below.
X-Hypersnap-Signed-AtUnix seconds at time of signing. Must be within signed_at_window_secs (default 5 min) of the server clock.
X-Hypersnap-Nonce0x-prefixed 32-byte random nonce. Deduped in-memory for the duration of the signed_at window so a replay is impossible.
X-Hypersnap-Signature0x-prefixed 65-byte EIP-712 signature over the typed data above.
\n

Operation names #

\n

The X-Hypersnap-Op header (and the op field inside the signed typed data) must match the HTTP method + path of the request. The server cross-checks them — a signed webhook.create cannot be replayed against a DELETE route.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EndpointOp string
POST /v2/farcaster/webhook/webhook.create
PUT /v2/farcaster/webhook/webhook.update
DELETE /v2/farcaster/webhook/webhook.delete
GET /v2/farcaster/webhook/ (lookup)webhook.read
GET /v2/farcaster/webhook/listwebhook.read
POST /v2/farcaster/webhook/secret/rotatewebhook.rotate_secret
POST /v2/farcaster/frame/app/app.create
PUT /v2/farcaster/frame/app/app.update
DELETE /v2/farcaster/frame/app/app.delete
GET /v2/farcaster/frame/app/ (lookup)app.read
GET /v2/farcaster/frame/app/listapp.read
POST /v2/farcaster/frame/app/secret/rotateapp.rotate_secret
\n

Server-side verification #

\n

The server performs these checks, in order, before routing a request to its handler:

\n
    \n
  1. Clock skew — reject if |now − signed_at| > signed_at_window_secs.
  2. \n
  3. Nonce replay — reject if (fid, nonce) was used within the signed_at window.
  4. \n
  5. Typed data recovery — compute the EIP-712 hash, recover the signer address via ecrecover.
  6. \n
  7. Custody match — look up custodyOf(fid) on-chain and reject if the recovered address doesn't match.
  8. \n
  9. Op ↔ route match — reject if the signed op doesn't match the actual HTTP method/path.
  10. \n
\n

If any step fails you get 401 Unauthorized with a short message body.

\n

Why this shape #

\n\n

Minimal JavaScript example #

\n
import { ethers } from \"ethers\";\nimport { randomBytes } from \"crypto\";\n\nconst wallet = new ethers.Wallet(privateKey);\n\nconst body = JSON.stringify({\n  name: \"my webhook\",\n  url: \"https://receiver.example.com/hook\",\n  subscription: { cast_created: { author_fids: [3] } },\n});\n\nconst signedAt = Math.floor(Date.now() / 1000);\nconst nonce = \"0x\" + randomBytes(32).toString(\"hex\");\nconst requestHash = ethers.keccak256(ethers.toUtf8Bytes(body));\n\nconst domain  = { name: \"Hypersnap\", version: \"1\", chainId: 10 };\nconst types   = {\n  HypersnapSignedOp: [\n    { name: \"op\",          type: \"string\"  },\n    { name: \"fid\",         type: \"uint64\"  },\n    { name: \"signedAt\",    type: \"uint256\" },\n    { name: \"nonce\",       type: \"bytes32\" },\n    { name: \"requestHash\", type: \"bytes32\" },\n  ],\n};\nconst value = {\n  op: \"webhook.create\",\n  fid: 3n,\n  signedAt: BigInt(signedAt),\n  nonce,\n  requestHash,\n};\n\nconst signature = await wallet.signTypedData(domain, types, value);\n\nconst resp = await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"X-Hypersnap-Fid\":       \"3\",\n    \"X-Hypersnap-Op\":        \"webhook.create\",\n    \"X-Hypersnap-Signed-At\": String(signedAt),\n    \"X-Hypersnap-Nonce\":     nonce,\n    \"X-Hypersnap-Signature\": signature,\n  },\n  body,\n});\n
\n

See the Sign an EIP-712 request guide for Python and Rust equivalents.

", + "contentHtml": "

Signed operations (EIP-712) #

\n

Management requests — creating or modifying webhooks, registering a mini app, rotating a secret — are authenticated by having the FID's custody address sign a small structured payload using EIP-712 typed data. Hypersnap reads the custody address from on-chain IdRegistry state, so whoever controls the key controls the FID's resources, no extra enrollment step.

\n

What gets signed #

\n

All signed endpoints use the same typed-data shape:

\n
Domain:\n  {\n    name:    \"Hypersnap\",\n    version: \"1\",\n    chainId: 10\n  }\n\nType:\n  HypersnapSignedOp(\n    string  op,           // operation name (see list below)\n    uint64  fid,\n    uint256 signedAt,     // unix seconds\n    bytes32 nonce,\n    bytes32 requestHash   // keccak256(raw HTTP body bytes)\n  )\n
\n

The requestHash is keccak256 of the raw bytes of the HTTP body — no JSON re-canonicalization. This means the client and server hash the same bytes exactly.

\n

Required HTTP headers #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
HeaderValue
X-Hypersnap-FidYour FID (decimal string).
X-Hypersnap-OpThe operation name — see the table below.
X-Hypersnap-Signed-AtUnix seconds at time of signing. Must be within signed_at_window_secs (default 5 min) of the server clock.
X-Hypersnap-Nonce0x-prefixed 32-byte random nonce. Deduped in-memory for the duration of the signed_at window so a replay is impossible.
X-Hypersnap-Signature0x-prefixed 65-byte EIP-712 signature over the typed data above.
\n

Operation names #

\n

The X-Hypersnap-Op header (and the op field inside the signed typed data) must match the HTTP method + path of the request. The server cross-checks them — a signed webhook.create cannot be replayed against a DELETE route.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EndpointOp string
POST /v2/farcaster/webhook/webhook.create
PUT /v2/farcaster/webhook/webhook.update
DELETE /v2/farcaster/webhook/webhook.delete
GET /v2/farcaster/webhook/ (lookup)webhook.read
GET /v2/farcaster/webhook/listwebhook.read
POST /v2/farcaster/webhook/secret/rotatewebhook.rotate_secret
POST /v2/farcaster/frame/app/app.create
PUT /v2/farcaster/frame/app/app.update
DELETE /v2/farcaster/frame/app/app.delete
GET /v2/farcaster/frame/app/ (lookup)app.read
GET /v2/farcaster/frame/app/listapp.read
POST /v2/farcaster/frame/app/secret/rotateapp.rotate_secret
\n

Server-side verification #

\n

The server performs these checks, in order, before routing a request to its handler:

\n
    \n
  1. Clock skew — reject if |now − signed_at| > signed_at_window_secs.
  2. \n
  3. Nonce replay — reject if (fid, nonce) was used within the signed_at window.
  4. \n
  5. Typed data recovery — compute the EIP-712 hash, recover the signer address via ecrecover.
  6. \n
  7. Custody match — look up custodyOf(fid) on-chain and reject if the recovered address doesn't match.
  8. \n
  9. Op ↔ route match — reject if the signed op doesn't match the actual HTTP method/path.
  10. \n
\n

If any step fails you get 401 Unauthorized with a short message body.

\n

Why this shape #

\n\n

Minimal JavaScript example #

\n
import { ethers } from \"ethers\";\nimport { randomBytes } from \"crypto\";\n\nconst wallet = new ethers.Wallet(privateKey);\n\nconst body = JSON.stringify({\n  name: \"my webhook\",\n  url: \"https://receiver.example.com/hook\",\n  subscription: { cast_created: { author_fids: [3] } },\n});\n\nconst signedAt = Math.floor(Date.now() / 1000);\nconst nonce = \"0x\" + randomBytes(32).toString(\"hex\");\nconst requestHash = ethers.keccak256(ethers.toUtf8Bytes(body));\n\nconst domain  = { name: \"Hypersnap\", version: \"1\", chainId: 10 };\nconst types   = {\n  HypersnapSignedOp: [\n    { name: \"op\",          type: \"string\"  },\n    { name: \"fid\",         type: \"uint64\"  },\n    { name: \"signedAt\",    type: \"uint256\" },\n    { name: \"nonce\",       type: \"bytes32\" },\n    { name: \"requestHash\", type: \"bytes32\" },\n  ],\n};\nconst value = {\n  op: \"webhook.create\",\n  fid: 3n,\n  signedAt: BigInt(signedAt),\n  nonce,\n  requestHash,\n};\n\nconst signature = await wallet.signTypedData(domain, types, value);\n\nconst resp = await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"X-Hypersnap-Fid\":       \"3\",\n    \"X-Hypersnap-Op\":        \"webhook.create\",\n    \"X-Hypersnap-Signed-At\": String(signedAt),\n    \"X-Hypersnap-Nonce\":     nonce,\n    \"X-Hypersnap-Signature\": signature,\n  },\n  body,\n});\n
\n

See the Sign an EIP-712 request guide for Python and Rust equivalents.

", "contentMarkdown": "# Signed operations (EIP-712)\n\nManagement requests — creating or modifying webhooks, registering a mini app, rotating a secret — are authenticated by having the **FID's custody address** sign a small structured payload using EIP-712 typed data. Hypersnap reads the custody address from on-chain `IdRegistry` state, so whoever controls the key controls the FID's resources, no extra enrollment step.\n\n## What gets signed\n\nAll signed endpoints use the same typed-data shape:\n\n```text\nDomain:\n {\n name: \"Hypersnap\",\n version: \"1\",\n chainId: 10\n }\n\nType:\n HypersnapSignedOp(\n string op, // operation name (see list below)\n uint64 fid,\n uint256 signedAt, // unix seconds\n bytes32 nonce,\n bytes32 requestHash // keccak256(raw HTTP body bytes)\n )\n```\n\nThe `requestHash` is `keccak256` of the **raw bytes** of the HTTP body — no JSON re-canonicalization. This means the client and server hash the same bytes exactly.\n\n## Required HTTP headers\n\n| Header | Value |\n|---|---|\n| `X-Hypersnap-Fid` | Your FID (decimal string). |\n| `X-Hypersnap-Op` | The operation name — see the table below. |\n| `X-Hypersnap-Signed-At` | Unix seconds at time of signing. Must be within `signed_at_window_secs` (default 5 min) of the server clock. |\n| `X-Hypersnap-Nonce` | `0x`-prefixed 32-byte random nonce. Deduped in-memory for the duration of the signed_at window so a replay is impossible. |\n| `X-Hypersnap-Signature` | `0x`-prefixed 65-byte EIP-712 signature over the typed data above. |\n\n## Operation names\n\nThe `X-Hypersnap-Op` header (and the `op` field inside the signed typed data) must match the HTTP method + path of the request. The server cross-checks them — a signed `webhook.create` cannot be replayed against a `DELETE` route.\n\n| Endpoint | Op string |\n|---|---|\n| `POST /v2/farcaster/webhook/` | `webhook.create` |\n| `PUT /v2/farcaster/webhook/` | `webhook.update` |\n| `DELETE /v2/farcaster/webhook/` | `webhook.delete` |\n| `GET /v2/farcaster/webhook/` (lookup) | `webhook.read` |\n| `GET /v2/farcaster/webhook/list` | `webhook.read` |\n| `POST /v2/farcaster/webhook/secret/rotate` | `webhook.rotate_secret` |\n| `POST /v2/farcaster/frame/app/` | `app.create` |\n| `PUT /v2/farcaster/frame/app/` | `app.update` |\n| `DELETE /v2/farcaster/frame/app/` | `app.delete` |\n| `GET /v2/farcaster/frame/app/` (lookup) | `app.read` |\n| `GET /v2/farcaster/frame/app/list` | `app.read` |\n| `POST /v2/farcaster/frame/app/secret/rotate` | `app.rotate_secret` |\n\n## Server-side verification\n\nThe server performs these checks, in order, before routing a request to its handler:\n\n1. **Clock skew** — reject if `|now − signed_at| > signed_at_window_secs`.\n2. **Nonce replay** — reject if `(fid, nonce)` was used within the signed_at window.\n3. **Typed data recovery** — compute the EIP-712 hash, recover the signer address via `ecrecover`.\n4. **Custody match** — look up `custodyOf(fid)` on-chain and reject if the recovered address doesn't match.\n5. **Op ↔ route match** — reject if the signed `op` doesn't match the actual HTTP method/path.\n\nIf any step fails you get `401 Unauthorized` with a short message body.\n\n## Why this shape\n\n- **Body bytes are signed directly**, so JSON canonicalization is a non-issue. You hash what you send.\n- **Typed data is EIP-712-native**, so every Ethereum wallet library can produce the signature with a small adapter.\n- **No session tokens.** Each request is individually signed; there is no login state the server has to track.\n- **The nonce prevents replay** inside the signed_at window. Outside the window, clock skew rejects the request anyway.\n\n## Minimal JavaScript example\n\n```javascript\nimport { ethers } from \"ethers\";\nimport { randomBytes } from \"crypto\";\n\nconst wallet = new ethers.Wallet(privateKey);\n\nconst body = JSON.stringify({\n name: \"my webhook\",\n url: \"https://receiver.example.com/hook\",\n subscription: { cast_created: { author_fids: [3] } },\n});\n\nconst signedAt = Math.floor(Date.now() / 1000);\nconst nonce = \"0x\" + randomBytes(32).toString(\"hex\");\nconst requestHash = ethers.keccak256(ethers.toUtf8Bytes(body));\n\nconst domain = { name: \"Hypersnap\", version: \"1\", chainId: 10 };\nconst types = {\n HypersnapSignedOp: [\n { name: \"op\", type: \"string\" },\n { name: \"fid\", type: \"uint64\" },\n { name: \"signedAt\", type: \"uint256\" },\n { name: \"nonce\", type: \"bytes32\" },\n { name: \"requestHash\", type: \"bytes32\" },\n ],\n};\nconst value = {\n op: \"webhook.create\",\n fid: 3n,\n signedAt: BigInt(signedAt),\n nonce,\n requestHash,\n};\n\nconst signature = await wallet.signTypedData(domain, types, value);\n\nconst resp = await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Hypersnap-Fid\": \"3\",\n \"X-Hypersnap-Op\": \"webhook.create\",\n \"X-Hypersnap-Signed-At\": String(signedAt),\n \"X-Hypersnap-Nonce\": nonce,\n \"X-Hypersnap-Signature\": signature,\n },\n body,\n});\n```\n\nSee the [Sign an EIP-712 request](../guides/sign-eip712.md) guide for Python and Rust equivalents.\n", "toc": [ { @@ -689,7 +689,7 @@ "sourcePath": "src/concepts/rate-limits.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/concepts/rate-limits.md", "slug": "concepts/rate-limits", - "contentHtml": "

Rate limits #

\n

Hypersnap is designed to be run by node operators for their own apps, not as a centralized API vendor, so the server-side rate-limiting story is modest. Here's the full picture:

\n

Public reads #

\n

There is no built-in per-IP rate limit on the public read endpoints. Your operator may run a reverse proxy (nginx, Cloudflare, envoy, etc.) in front of the node with its own rate limiting — check their operations docs.

\n

In practice the node will happily serve tens of thousands of requests per second off RocksDB as long as disk/CPU hold up. Be a good citizen: cache heavy responses (feeds, search), obey your operator's posted limits, back off on 5xx.

\n

Webhook deliveries (outbound) #

\n

Each webhook has a per-webhook rate limit enforced by the dispatcher:

\n\n

If you expect high-volume event streams, either bump the rate limit with your operator or narrow your subscription filters so fewer events match.

\n

Mini-app notification send #

\n

The send endpoint has three applicable limits:

\n
    \n
  1. Per-app dedupe window. (fid, notificationId) is deduped for 24 hours by default. Sending the same notification twice within that window is a no-op for the recipient. This follows the Farcaster Mini App spec exactly.
  2. \n
  3. Per-token client limits. The Farcaster client (Warpcast, etc.) enforces the spec-defined 1 notification per 30 seconds and 100 per day ceilings. Tokens that exceed those limits come back in rate_limited_tokens from the client, and Hypersnap returns the corresponding fids in retryable_fids so your code can retry later.
  4. \n
  5. Per-app token cap. No hard limit — the registered token store is bounded by however many Farcaster users have added your mini app.
  6. \n
\n

Per-FID management caps #

\n

When signing management requests:

\n\n

Both caps are operator-configurable.

\n

Summary #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SurfaceLimitEnforced where
Public readsNone by defaultYour operator's reverse proxy
Webhook delivery1000 events / 60 sec (default)Hypersnap dispatcher
Send → client batch100 tokens per POSTHypersnap sender (spec-mandated)
Send → per token1 / 30 sec, 100 / dayFarcaster client (spec-mandated), surfaced as retryable_fids
Send → dedupe(fid, notificationId) for 24hHypersnap (spec-mandated)
Webhooks per FID25 (default)Hypersnap store
Mini apps per FID25 (default)Hypersnap store
", + "contentHtml": "

Rate limits #

\n

Hypersnap is designed to be run by node operators for their own apps, not as a centralized API vendor, so the server-side rate-limiting story is modest. Here's the full picture:

\n

Public reads #

\n

There is no built-in per-IP rate limit on the public read endpoints. Your operator may run a reverse proxy (nginx, Cloudflare, envoy, etc.) in front of the node with its own rate limiting — check their operations docs.

\n

In practice the node will happily serve tens of thousands of requests per second off RocksDB as long as disk/CPU hold up. Be a good citizen: cache heavy responses (feeds, search), obey your operator's posted limits, back off on 5xx.

\n

Webhook deliveries (outbound) #

\n

Each webhook has a per-webhook rate limit enforced by the dispatcher:

\n\n

If you expect high-volume event streams, either bump the rate limit with your operator or narrow your subscription filters so fewer events match.

\n

Mini-app notification send #

\n

The send endpoint has three applicable limits:

\n
    \n
  1. Per-app dedupe window. (fid, notificationId) is deduped for 24 hours by default. Sending the same notification twice within that window is a no-op for the recipient. This follows the Farcaster Mini App spec exactly.
  2. \n
  3. Per-token client limits. The Farcaster client (Warpcast, etc.) enforces the spec-defined 1 notification per 30 seconds and 100 per day ceilings. Tokens that exceed those limits come back in rate_limited_tokens from the client, and Hypersnap returns the corresponding fids in retryable_fids so your code can retry later.
  4. \n
  5. Per-app token cap. No hard limit — the registered token store is bounded by however many Farcaster users have added your mini app.
  6. \n
\n

Per-FID management caps #

\n

When signing management requests:

\n\n

Both caps are operator-configurable.

\n

Summary #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SurfaceLimitEnforced where
Public readsNone by defaultYour operator's reverse proxy
Webhook delivery1000 events / 60 sec (default)Hypersnap dispatcher
Send → client batch100 tokens per POSTHypersnap sender (spec-mandated)
Send → per token1 / 30 sec, 100 / dayFarcaster client (spec-mandated), surfaced as retryable_fids
Send → dedupe(fid, notificationId) for 24hHypersnap (spec-mandated)
Webhooks per FID25 (default)Hypersnap store
Mini apps per FID25 (default)Hypersnap store
", "contentMarkdown": "# Rate limits\n\nHypersnap is designed to be run by node operators for their own apps, not as a centralized API vendor, so the server-side rate-limiting story is modest. Here's the full picture:\n\n## Public reads\n\nThere is **no built-in per-IP rate limit** on the public read endpoints. Your operator may run a reverse proxy (nginx, Cloudflare, envoy, etc.) in front of the node with its own rate limiting — check their operations docs.\n\nIn practice the node will happily serve tens of thousands of requests per second off RocksDB as long as disk/CPU hold up. Be a good citizen: cache heavy responses (feeds, search), obey your operator's posted limits, back off on 5xx.\n\n## Webhook deliveries (outbound)\n\nEach webhook has a per-webhook rate limit enforced by the dispatcher:\n\n- Default: **1000 events per 60 seconds**, per webhook.\n- Configured at node level via `default_rate_limit` / `default_rate_limit_duration_secs`.\n- Events above the limit are dropped (counted in the `webhooks.delivery.rate_limited` metric), **not** queued.\n\nIf you expect high-volume event streams, either bump the rate limit with your operator or narrow your subscription filters so fewer events match.\n\n## Mini-app notification send\n\nThe send endpoint has three applicable limits:\n\n1. **Per-app dedupe window.** `(fid, notificationId)` is deduped for 24 hours by default. Sending the same notification twice within that window is a no-op for the recipient. This follows the Farcaster Mini App spec exactly.\n2. **Per-token client limits.** The Farcaster client (Warpcast, etc.) enforces the spec-defined `1 notification per 30 seconds` and `100 per day` ceilings. Tokens that exceed those limits come back in `rate_limited_tokens` from the client, and Hypersnap returns the corresponding `fids` in `retryable_fids` so your code can retry later.\n3. **Per-app token cap.** No hard limit — the registered token store is bounded by however many Farcaster users have added your mini app.\n\n## Per-FID management caps\n\nWhen signing management requests:\n\n- **Webhooks:** `max_webhooks_per_owner` (default **25**). Creating a 26th returns `429 Too Many Requests`.\n- **Mini apps:** `max_apps_per_owner` (default **25**). Same behavior.\n\nBoth caps are operator-configurable.\n\n## Summary\n\n| Surface | Limit | Enforced where |\n|---|---|---|\n| Public reads | None by default | Your operator's reverse proxy |\n| Webhook delivery | 1000 events / 60 sec (default) | Hypersnap dispatcher |\n| Send → client batch | 100 tokens per POST | Hypersnap sender (spec-mandated) |\n| Send → per token | 1 / 30 sec, 100 / day | Farcaster client (spec-mandated), surfaced as `retryable_fids` |\n| Send → dedupe | `(fid, notificationId)` for 24h | Hypersnap (spec-mandated) |\n| Webhooks per FID | 25 (default) | Hypersnap store |\n| Mini apps per FID | 25 (default) | Hypersnap store |\n", "toc": [ { @@ -725,7 +725,7 @@ "sourcePath": "src/concepts/errors.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/concepts/errors.md", "slug": "concepts/errors", - "contentHtml": "

Errors #

\n

All error responses have the same shape:

\n
{ \"message\": \"human-readable explanation\" }\n
\n

HTTP status codes #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
CodeMeaningExample causes
200 OKSuccessNormal read, successful management write.
400 Bad RequestMalformed inputMissing required query param, invalid JSON body, invalid regex in a subscription filter, URL fails SSRF check, missing ?owner_fid= when creating a webhook.
401 UnauthorizedBad signature / auth headersEIP-712 signature doesn't recover to the custody address, clock skew > 5min, nonce replayed, JFS signature invalid, send-endpoint x-api-key wrong, unknown FID.
403 ForbiddenNot the ownerYou signed a valid EIP-712 request but the resource (webhook/app) belongs to a different FID.
404 Not FoundResource doesn't existwebhook_id / app_id / FID not found, unknown route.
429 Too Many RequestsPer-owner cap hitCreating webhook #26 when max_webhooks_per_owner=25, or mini app #26.
500 Internal Server ErrorUnexpected server faultRocksDB error, panic in a handler, transient storage failure. Please file a bug.
\n

Debugging auth failures #

\n

If you get a 401 on a signed request, work down this list:

\n
    \n
  1. Is the raw body identical to what you hashed? The server computes keccak256(body_bytes) on the literal HTTP body it received. If your HTTP client re-serializes JSON between signing and sending (pretty-print, key reordering), the hash won't match. Send raw bytes.
  2. \n
  3. Is your clock correct? |now − signed_at| must be ≤ signed_at_window_secs (default 5 min). date -u on the client should match the server.
  4. \n
  5. Is the nonce fresh? A replayed (fid, nonce) pair rejects for up to 2 × signed_at_window_secs.
  6. \n
  7. Is your op string exactly right? webhook.create not webhook_create or createWebhook. See Signed operations for the full list.
  8. \n
  9. Does the op match the HTTP method/path? A signed webhook.create on DELETE /.../webhook/ returns 400 \"signed op does not match the HTTP method/path\".
  10. \n
  11. Is your custody address up to date? Hypersnap reads the current custody from on-chain IdRegistry state. If you recently transferred the FID, give the indexer a few seconds to catch up.
  12. \n
\n

Debugging 429 #

\n

Per-owner caps are opt-in and operator-controlled. If you're hitting a cap, either:

\n\n

Debugging webhook delivery failures #

\n

Webhook delivery errors don't come back on your management-API call — they come back on the webhook delivery. See Delivery contract for how successes and failures propagate.

", + "contentHtml": "

Errors #

\n

All error responses have the same shape:

\n
{ \"message\": \"human-readable explanation\" }\n
\n

HTTP status codes #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
CodeMeaningExample causes
200 OKSuccessNormal read, successful management write.
400 Bad RequestMalformed inputMissing required query param, invalid JSON body, invalid regex in a subscription filter, URL fails SSRF check, missing ?owner_fid= when creating a webhook.
401 UnauthorizedBad signature / auth headersEIP-712 signature doesn't recover to the custody address, clock skew > 5min, nonce replayed, JFS signature invalid, send-endpoint x-api-key wrong, unknown FID.
403 ForbiddenNot the ownerYou signed a valid EIP-712 request but the resource (webhook/app) belongs to a different FID.
404 Not FoundResource doesn't existwebhook_id / app_id / FID not found, unknown route.
429 Too Many RequestsPer-owner cap hitCreating webhook #26 when max_webhooks_per_owner=25, or mini app #26.
500 Internal Server ErrorUnexpected server faultRocksDB error, panic in a handler, transient storage failure. Please file a bug.
\n

Debugging auth failures #

\n

If you get a 401 on a signed request, work down this list:

\n
    \n
  1. Is the raw body identical to what you hashed? The server computes keccak256(body_bytes) on the literal HTTP body it received. If your HTTP client re-serializes JSON between signing and sending (pretty-print, key reordering), the hash won't match. Send raw bytes.
  2. \n
  3. Is your clock correct? |now − signed_at| must be ≤ signed_at_window_secs (default 5 min). date -u on the client should match the server.
  4. \n
  5. Is the nonce fresh? A replayed (fid, nonce) pair rejects for up to 2 × signed_at_window_secs.
  6. \n
  7. Is your op string exactly right? webhook.create not webhook_create or createWebhook. See Signed operations for the full list.
  8. \n
  9. Does the op match the HTTP method/path? A signed webhook.create on DELETE /.../webhook/ returns 400 \"signed op does not match the HTTP method/path\".
  10. \n
  11. Is your custody address up to date? Hypersnap reads the current custody from on-chain IdRegistry state. If you recently transferred the FID, give the indexer a few seconds to catch up.
  12. \n
\n

Debugging 429 #

\n

Per-owner caps are opt-in and operator-controlled. If you're hitting a cap, either:

\n\n

Debugging webhook delivery failures #

\n

Webhook delivery errors don't come back on your management-API call — they come back on the webhook delivery. See Delivery contract for how successes and failures propagate.

", "contentMarkdown": "# Errors\n\nAll error responses have the same shape:\n\n```json\n{ \"message\": \"human-readable explanation\" }\n```\n\n## HTTP status codes\n\n| Code | Meaning | Example causes |\n|---|---|---|\n| `200 OK` | Success | Normal read, successful management write. |\n| `400 Bad Request` | Malformed input | Missing required query param, invalid JSON body, invalid regex in a subscription filter, URL fails SSRF check, missing `?owner_fid=` when creating a webhook. |\n| `401 Unauthorized` | Bad signature / auth headers | EIP-712 signature doesn't recover to the custody address, clock skew > 5min, nonce replayed, JFS signature invalid, send-endpoint `x-api-key` wrong, unknown FID. |\n| `403 Forbidden` | Not the owner | You signed a valid EIP-712 request but the resource (webhook/app) belongs to a different FID. |\n| `404 Not Found` | Resource doesn't exist | `webhook_id` / `app_id` / FID not found, unknown route. |\n| `429 Too Many Requests` | Per-owner cap hit | Creating webhook #26 when `max_webhooks_per_owner=25`, or mini app #26. |\n| `500 Internal Server Error` | Unexpected server fault | RocksDB error, panic in a handler, transient storage failure. Please file a bug. |\n\n## Debugging auth failures\n\nIf you get a `401` on a signed request, work down this list:\n\n1. **Is the raw body identical to what you hashed?** The server computes `keccak256(body_bytes)` on the literal HTTP body it received. If your HTTP client re-serializes JSON between signing and sending (pretty-print, key reordering), the hash won't match. Send raw bytes.\n2. **Is your clock correct?** `|now − signed_at|` must be ≤ `signed_at_window_secs` (default 5 min). `date -u` on the client should match the server.\n3. **Is the nonce fresh?** A replayed `(fid, nonce)` pair rejects for up to `2 × signed_at_window_secs`.\n4. **Is your op string exactly right?** `webhook.create` not `webhook_create` or `createWebhook`. See [Signed operations](./authentication.md) for the full list.\n5. **Does the op match the HTTP method/path?** A signed `webhook.create` on `DELETE /.../webhook/` returns `400 \"signed op does not match the HTTP method/path\"`.\n6. **Is your custody address up to date?** Hypersnap reads the current custody from on-chain `IdRegistry` state. If you recently transferred the FID, give the indexer a few seconds to catch up.\n\n## Debugging 429\n\nPer-owner caps are opt-in and operator-controlled. If you're hitting a cap, either:\n\n- Delete unused webhooks/apps via `DELETE /v2/farcaster/webhook/` or `DELETE /v2/farcaster/frame/app/` to free up slots, or\n- Ask your operator to raise `max_webhooks_per_owner` / `max_apps_per_owner`.\n\n## Debugging webhook delivery failures\n\nWebhook delivery errors don't come back on your management-API call — they come back on the webhook delivery. See [Delivery contract](../reference/webhooks/delivery.md) for how successes and failures propagate.\n", "toc": [ { @@ -777,7 +777,7 @@ "sourcePath": "src/reference/reads/users.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/users.md", "slug": "reference/reads/users", - "contentHtml": "

Users #

\n

All user endpoints return (or contain) the shared User object. See src/api/types.rs for the full field list; common fields include fid, username, display_name, pfp_url, profile.bio, follower_count, following_count, verified_addresses, and power_badge.

\n
\n

GET /v2/farcaster/user #

\n

Look up a single user by FID.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe FID to fetch.
\n

Response

\n
{ \"user\": { \"fid\": 3, \"username\": \"dwr.eth\", \"...\" : \"...\" } }\n
\n

Example

\n
curl -s \"https://haatz.quilibrium.com/v2/farcaster/user?fid=3\"\n
\n
\n
\n

GET /v2/farcaster/user/bulk #

\n

Batch lookup by FID list.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidsstringyesComma-separated list of FIDs, e.g. 3,5,191
\n

Response

\n
{ \"users\": [ { \"fid\": 3, \"...\" : \"...\" }, { \"fid\": 5, \"...\" : \"...\" } ] }\n
\n

Missing FIDs are silently omitted from the response — if you asked for 3 fids and only 2 exist, you get 2 objects back, not a 404.

\n
\n
\n

GET /v2/farcaster/user/bulk-by-address #

\n

Batch lookup by verified Ethereum address.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
addressesstringyesComma-separated 0x-addresses. Matches against User.verified_addresses.eth_addresses.
\n

Response — same BulkUsersResponse shape.

\n

Addresses that aren't verified against any FID are silently omitted.

\n
\n
\n

GET /v2/farcaster/user/by-username #

\n

Look up a user by username. Accepts fnames, ENS names, and Basenames — whatever resolves through the username-proof table. Also reachable as GET /v2/farcaster/user/by_username (underscore variant).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
usernamestringyesCase-sensitive. Without a leading @.
\n

Response — single UserResponse. 404 if the name isn't registered.

\n
\n
\n

GET /v2/farcaster/user/custody-address #

\n

Reverse-lookup: find the user whose custody Ethereum address matches.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
custody_addressstringyes0x-prefixed Ethereum address.
\n

ResponseUserResponse. 404 if no FID is registered to that address. If multiple FIDs share the address (legacy), the first is returned.

\n
\n
\n

GET /v2/farcaster/user/by_x_username #

\n

Look up an FID by the user's self-declared X/Twitter username. Backed by a reverse index built from UserDataAdd messages of type USER_DATA_TYPE_TWITTER.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
usernamestringyesCase-insensitive match against the stored X username.
\n

ResponseUserResponse. 404 if no user has that X username registered.

\n
\n
\n

GET /v2/farcaster/user/by_location #

\n

Find users whose declared location matches a prefix. Backed by a reverse index built from UserDataAdd messages of type USER_DATA_TYPE_LOCATION.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
locationstringyesPrefix-match against the stored location string (case-insensitive).
limitusizenoDefault 10.
\n

ResponseBulkUsersResponse.

\n
\n
\n

GET /v2/farcaster/user/search #

\n

Prefix-style search against the username index.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
qstringyesSearch query.
limitusizenoDefault 10.
\n

ResponseBulkUsersResponse.

\n
\n
\n

GET /v2/farcaster/user/verifications #

\n

Returns the same User object as /v2/farcaster/user, with the verified_addresses section populated. Provided as an alias for callers that want to signal intent.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n
\n
\n

GET /v2/farcaster/user/storage-allocations #

\n

How much Farcaster storage an FID currently has allocated to it.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n

Response

\n
{\n  \"total_active_units\": 2,\n  \"allocations\": [\n    {\n      \"object\": \"storage_allocation\",\n      \"fid\": 3,\n      \"units\": 2,\n      \"expiry\": 1760000000,\n      \"timestamp\": 1700000000\n    }\n  ]\n}\n
\n
\n
\n

GET /v2/farcaster/user/storage-usage #

\n

How much of the allocated storage the FID has actually consumed, broken down by message type.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n

Response

\n
{\n  \"object\": \"storage_usage\",\n  \"units\": [\n    { \"store_type\": \"casts\",       \"used\": 250, \"capacity\": 5000 },\n    { \"store_type\": \"reactions\",   \"used\": 800, \"capacity\": 2500 },\n    { \"store_type\": \"links\",       \"used\": 300, \"capacity\": 2500 },\n    { \"store_type\": \"verifications\", \"used\": 1, \"capacity\": 25 }\n  ]\n}\n
\n
\n

Also reachable as GET /v2/farcaster/storage/usage and GET /v2/farcaster/storage/allocations — identical behavior, different paths.

\n
\n

GET /v2/farcaster/user/fid #

\n

List registered FIDs on the network, paginated.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
limitusizenoDefault 10.
cursorstringnoHex-encoded pagination cursor.
\n

Response

\n
{ \"fids\": [1, 2, 3], \"next\": { \"cursor\": \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/user/channels #

\n

Channels a user has recently been active in. Alias of GET /v2/farcaster/channel/user-active.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n

ResponseChannelsResponse.

\n
\n
\n

GET /v2/farcaster/user/memberships/list #

\n

Alias of GET /v2/farcaster/user/channels — returns the same active-channels list in a membership-oriented response shape.

\n
\n
\n

GET /v2/farcaster/user/best_friends #

\n

Users that both follow and are followed by fid — the intersection. Sorted by most recent mutual follow.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n

ResponseFollowersResponse.

\n
\n
\n

GET /v2/farcaster/user/interactions #

\n

Summarize the interaction history between two FIDs: mention counts, reaction counts, mutual-follow state.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe \"from\" user.
target_fidu64noThe \"to\" user. If omitted, an empty summary is returned.
\n

Response

\n
{\n  \"interactions\": {\n    \"fid\": 3,\n    \"target_fid\": 5,\n    \"mentions\": 7,\n    \"reactions\": 42,\n    \"mutual_follow\": true\n  }\n}\n
\n

Mentions are computed from the CastsByMention index (casts by fid that mention target_fid). Reactions are computed from the reactor's reaction set. Mutual-follow is a single index lookup.

\n
\n
\n

Endpoints with no protocol data #

\n

The following endpoints are registered for SDK compatibility but return empty responses because the Farcaster protocol does not track the underlying data:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PathReturnsWhy
GET /v2/farcaster/user/power_users{ \"users\": [] }Power-user curation is a proprietary scoring signal, not protocol data.
GET /v2/farcaster/user/balance{ \"balances\": [], \"next\": { \"cursor\": null } }Token balances are on-chain state outside the Farcaster protocol.
GET /v2/farcaster/user/subscribed_to{ \"subscriptions\": [], \"next\": { \"cursor\": null } }User-to-user subscriptions are not in the protocol.
GET /v2/farcaster/user/subscribers{ \"subscriptions\": [], \"next\": { \"cursor\": null } }Same as above.
GET /v2/farcaster/user/subscriptions_created{ \"subscriptions\": [], \"next\": { \"cursor\": null } }Same as above.
\n

Write endpoints #

\n

Endpoints that mutate user state (POST /v2/farcaster/user/register, POST /v2/farcaster/user/follow, DELETE /v2/farcaster/user/follow, POST /v2/farcaster/user/verification, DELETE /v2/farcaster/user/verification, PATCH /v2/farcaster/user) are registered but return 501 Not Implemented. Write operations require submitting a signed Farcaster protocol message via the gRPC SubmitMessage endpoint instead.

", + "contentHtml": "

Users #

\n

All user endpoints return (or contain) the shared User object. See src/api/types.rs for the full field list; common fields include fid, username, display_name, pfp_url, profile.bio, follower_count, following_count, verified_addresses, and power_badge.

\n
\n

GET /v2/farcaster/user #

\n

Look up a single user by FID.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe FID to fetch.
\n

Response

\n
{ \"user\": { \"fid\": 3, \"username\": \"dwr.eth\", \"...\" : \"...\" } }\n
\n

Example

\n
curl -s \"https://haatz.quilibrium.com/v2/farcaster/user?fid=3\"\n
\n
\n
\n

GET /v2/farcaster/user/bulk #

\n

Batch lookup by FID list.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidsstringyesComma-separated list of FIDs, e.g. 3,5,191
\n

Response

\n
{ \"users\": [ { \"fid\": 3, \"...\" : \"...\" }, { \"fid\": 5, \"...\" : \"...\" } ] }\n
\n

Missing FIDs are silently omitted from the response — if you asked for 3 fids and only 2 exist, you get 2 objects back, not a 404.

\n
\n
\n

GET /v2/farcaster/user/bulk-by-address #

\n

Batch lookup by verified Ethereum address.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
addressesstringyesComma-separated 0x-addresses. Matches against User.verified_addresses.eth_addresses.
\n

Response — same BulkUsersResponse shape.

\n

Addresses that aren't verified against any FID are silently omitted.

\n
\n
\n

GET /v2/farcaster/user/by-username #

\n

Look up a user by username. Accepts fnames, ENS names, and Basenames — whatever resolves through the username-proof table. Also reachable as GET /v2/farcaster/user/by_username (underscore variant).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
usernamestringyesCase-sensitive. Without a leading @.
\n

Response — single UserResponse. 404 if the name isn't registered.

\n
\n
\n

GET /v2/farcaster/user/custody-address #

\n

Reverse-lookup: find the user whose custody Ethereum address matches.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
custody_addressstringyes0x-prefixed Ethereum address.
\n

ResponseUserResponse. 404 if no FID is registered to that address. If multiple FIDs share the address (legacy), the first is returned.

\n
\n
\n

GET /v2/farcaster/user/by_x_username #

\n

Look up an FID by the user's self-declared X/Twitter username. Backed by a reverse index built from UserDataAdd messages of type USER_DATA_TYPE_TWITTER.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
usernamestringyesCase-insensitive match against the stored X username.
\n

ResponseUserResponse. 404 if no user has that X username registered.

\n
\n
\n

GET /v2/farcaster/user/by_location #

\n

Find users whose declared location matches a prefix. Backed by a reverse index built from UserDataAdd messages of type USER_DATA_TYPE_LOCATION.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
locationstringyesPrefix-match against the stored location string (case-insensitive).
limitusizenoDefault 10.
\n

ResponseBulkUsersResponse.

\n
\n
\n

GET /v2/farcaster/user/search #

\n

Prefix-style search against the username index.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
qstringyesSearch query.
limitusizenoDefault 10.
\n

ResponseBulkUsersResponse.

\n
\n
\n

GET /v2/farcaster/user/verifications #

\n

Returns the same User object as /v2/farcaster/user, with the verified_addresses section populated. Provided as an alias for callers that want to signal intent.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n
\n
\n

GET /v2/farcaster/user/storage-allocations #

\n

How much Farcaster storage an FID currently has allocated to it.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n

Response

\n
{\n  \"total_active_units\": 2,\n  \"allocations\": [\n    {\n      \"object\": \"storage_allocation\",\n      \"fid\": 3,\n      \"units\": 2,\n      \"expiry\": 1760000000,\n      \"timestamp\": 1700000000\n    }\n  ]\n}\n
\n
\n
\n

GET /v2/farcaster/user/storage-usage #

\n

How much of the allocated storage the FID has actually consumed, broken down by message type.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n

Response

\n
{\n  \"object\": \"storage_usage\",\n  \"units\": [\n    { \"store_type\": \"casts\",       \"used\": 250, \"capacity\": 5000 },\n    { \"store_type\": \"reactions\",   \"used\": 800, \"capacity\": 2500 },\n    { \"store_type\": \"links\",       \"used\": 300, \"capacity\": 2500 },\n    { \"store_type\": \"verifications\", \"used\": 1, \"capacity\": 25 }\n  ]\n}\n
\n
\n

Also reachable as GET /v2/farcaster/storage/usage and GET /v2/farcaster/storage/allocations — identical behavior, different paths.

\n
\n

GET /v2/farcaster/user/fid #

\n

List registered FIDs on the network, paginated.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
limitusizenoDefault 10.
cursorstringnoHex-encoded pagination cursor.
\n

Response

\n
{ \"fids\": [1, 2, 3], \"next\": { \"cursor\": \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/user/channels #

\n

Channels a user has recently been active in. Alias of GET /v2/farcaster/channel/user-active.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n

ResponseChannelsResponse.

\n
\n
\n

GET /v2/farcaster/user/memberships/list #

\n

Alias of GET /v2/farcaster/user/channels — returns the same active-channels list in a membership-oriented response shape.

\n
\n
\n

GET /v2/farcaster/user/best_friends #

\n

Users that both follow and are followed by fid — the intersection. Sorted by most recent mutual follow.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n

ResponseFollowersResponse.

\n
\n
\n

GET /v2/farcaster/user/interactions #

\n

Summarize the interaction history between two FIDs: mention counts, reaction counts, mutual-follow state.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe \"from\" user.
target_fidu64noThe \"to\" user. If omitted, an empty summary is returned.
\n

Response

\n
{\n  \"interactions\": {\n    \"fid\": 3,\n    \"target_fid\": 5,\n    \"mentions\": 7,\n    \"reactions\": 42,\n    \"mutual_follow\": true\n  }\n}\n
\n

Mentions are computed from the CastsByMention index (casts by fid that mention target_fid). Reactions are computed from the reactor's reaction set. Mutual-follow is a single index lookup.

\n
\n
\n

Endpoints with no protocol data #

\n

The following endpoints are registered for SDK compatibility but return empty responses because the Farcaster protocol does not track the underlying data:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PathReturnsWhy
GET /v2/farcaster/user/power_users{ \"users\": [] }Power-user curation is a proprietary scoring signal, not protocol data.
GET /v2/farcaster/user/balance{ \"balances\": [], \"next\": { \"cursor\": null } }Token balances are on-chain state outside the Farcaster protocol.
GET /v2/farcaster/user/subscribed_to{ \"subscriptions\": [], \"next\": { \"cursor\": null } }User-to-user subscriptions are not in the protocol.
GET /v2/farcaster/user/subscribers{ \"subscriptions\": [], \"next\": { \"cursor\": null } }Same as above.
GET /v2/farcaster/user/subscriptions_created{ \"subscriptions\": [], \"next\": { \"cursor\": null } }Same as above.
\n

Write endpoints #

\n

Endpoints that mutate user state (POST /v2/farcaster/user/register, POST /v2/farcaster/user/follow, DELETE /v2/farcaster/user/follow, POST /v2/farcaster/user/verification, DELETE /v2/farcaster/user/verification, PATCH /v2/farcaster/user) are registered but return 501 Not Implemented. Write operations require submitting a signed Farcaster protocol message via the gRPC SubmitMessage endpoint instead.

", "contentMarkdown": "# Users\n\nAll user endpoints return (or contain) the shared `User` object. See `src/api/types.rs` for the full field list; common fields include `fid`, `username`, `display_name`, `pfp_url`, `profile.bio`, `follower_count`, `following_count`, `verified_addresses`, and `power_badge`.\n\n---\n\n## GET /v2/farcaster/user\n\nLook up a single user by FID.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The FID to fetch. |\n\n**Response**\n\n```json\n{ \"user\": { \"fid\": 3, \"username\": \"dwr.eth\", \"...\" : \"...\" } }\n```\n\n**Example**\n\n```bash\ncurl -s \"https://haatz.quilibrium.com/v2/farcaster/user?fid=3\"\n```\n\n
\n\n---\n\n## GET /v2/farcaster/user/bulk\n\nBatch lookup by FID list.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fids` | string | yes | Comma-separated list of FIDs, e.g. `3,5,191` |\n\n**Response**\n\n```json\n{ \"users\": [ { \"fid\": 3, \"...\" : \"...\" }, { \"fid\": 5, \"...\" : \"...\" } ] }\n```\n\nMissing FIDs are silently omitted from the response — if you asked for 3 fids and only 2 exist, you get 2 objects back, not a 404.\n\n
\n\n---\n\n## GET /v2/farcaster/user/bulk-by-address\n\nBatch lookup by verified Ethereum address.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `addresses` | string | yes | Comma-separated 0x-addresses. Matches against `User.verified_addresses.eth_addresses`. |\n\n**Response** — same `BulkUsersResponse` shape.\n\nAddresses that aren't verified against any FID are silently omitted.\n\n
\n\n---\n\n## GET /v2/farcaster/user/by-username\n\nLook up a user by username. Accepts fnames, ENS names, and Basenames — whatever resolves through the username-proof table. Also reachable as `GET /v2/farcaster/user/by_username` (underscore variant).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `username` | string | yes | Case-sensitive. Without a leading `@`. |\n\n**Response** — single `UserResponse`. `404` if the name isn't registered.\n\n
\n\n---\n\n## GET /v2/farcaster/user/custody-address\n\nReverse-lookup: find the user whose custody Ethereum address matches.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `custody_address` | string | yes | `0x`-prefixed Ethereum address. |\n\n**Response** — `UserResponse`. `404` if no FID is registered to that address. If multiple FIDs share the address (legacy), the first is returned.\n\n
\n\n---\n\n## GET /v2/farcaster/user/by_x_username\n\nLook up an FID by the user's self-declared X/Twitter username. Backed by a reverse index built from `UserDataAdd` messages of type `USER_DATA_TYPE_TWITTER`.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `username` | string | yes | Case-insensitive match against the stored X username. |\n\n**Response** — `UserResponse`. `404` if no user has that X username registered.\n\n
\n\n---\n\n## GET /v2/farcaster/user/by_location\n\nFind users whose declared location matches a prefix. Backed by a reverse index built from `UserDataAdd` messages of type `USER_DATA_TYPE_LOCATION`.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `location` | string | yes | Prefix-match against the stored location string (case-insensitive). |\n| `limit` | usize | no | Default `10`. |\n\n**Response** — `BulkUsersResponse`.\n\n
\n\n---\n\n## GET /v2/farcaster/user/search\n\nPrefix-style search against the username index.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `q` | string | yes | Search query. |\n| `limit` | usize | no | Default `10`. |\n\n**Response** — `BulkUsersResponse`.\n\n
\n\n---\n\n## GET /v2/farcaster/user/verifications\n\nReturns the same `User` object as `/v2/farcaster/user`, with the `verified_addresses` section populated. Provided as an alias for callers that want to signal intent.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n\n
\n\n---\n\n## GET /v2/farcaster/user/storage-allocations\n\nHow much Farcaster storage an FID currently has allocated to it.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n\n**Response**\n\n```json\n{\n \"total_active_units\": 2,\n \"allocations\": [\n {\n \"object\": \"storage_allocation\",\n \"fid\": 3,\n \"units\": 2,\n \"expiry\": 1760000000,\n \"timestamp\": 1700000000\n }\n ]\n}\n```\n\n
\n\n---\n\n## GET /v2/farcaster/user/storage-usage\n\nHow much of the allocated storage the FID has actually consumed, broken down by message type.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n\n**Response**\n\n```json\n{\n \"object\": \"storage_usage\",\n \"units\": [\n { \"store_type\": \"casts\", \"used\": 250, \"capacity\": 5000 },\n { \"store_type\": \"reactions\", \"used\": 800, \"capacity\": 2500 },\n { \"store_type\": \"links\", \"used\": 300, \"capacity\": 2500 },\n { \"store_type\": \"verifications\", \"used\": 1, \"capacity\": 25 }\n ]\n}\n```\n\n
\n\nAlso reachable as `GET /v2/farcaster/storage/usage` and `GET /v2/farcaster/storage/allocations` — identical behavior, different paths.\n\n---\n\n## GET /v2/farcaster/user/fid\n\nList registered FIDs on the network, paginated.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Hex-encoded pagination cursor. |\n\n**Response**\n\n```json\n{ \"fids\": [1, 2, 3], \"next\": { \"cursor\": \"...\" } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/user/channels\n\nChannels a user has recently been active in. Alias of `GET /v2/farcaster/channel/user-active`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n**Response** — `ChannelsResponse`.\n\n
\n\n---\n\n## GET /v2/farcaster/user/memberships/list\n\nAlias of `GET /v2/farcaster/user/channels` — returns the same active-channels list in a membership-oriented response shape.\n\n
\n\n---\n\n## GET /v2/farcaster/user/best_friends\n\nUsers that both follow and are followed by `fid` — the intersection. Sorted by most recent mutual follow.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n**Response** — `FollowersResponse`.\n\n
\n\n---\n\n## GET /v2/farcaster/user/interactions\n\nSummarize the interaction history between two FIDs: mention counts, reaction counts, mutual-follow state.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The \"from\" user. |\n| `target_fid` | u64 | no | The \"to\" user. If omitted, an empty summary is returned. |\n\n**Response**\n\n```json\n{\n \"interactions\": {\n \"fid\": 3,\n \"target_fid\": 5,\n \"mentions\": 7,\n \"reactions\": 42,\n \"mutual_follow\": true\n }\n}\n```\n\nMentions are computed from the `CastsByMention` index (casts by `fid` that mention `target_fid`). Reactions are computed from the reactor's reaction set. Mutual-follow is a single index lookup.\n\n
\n\n---\n\n## Endpoints with no protocol data\n\nThe following endpoints are registered for SDK compatibility but return empty responses because the Farcaster protocol does not track the underlying data:\n\n| Path | Returns | Why |\n|---|---|---|\n| `GET /v2/farcaster/user/power_users` | `{ \"users\": [] }` | Power-user curation is a proprietary scoring signal, not protocol data. |\n| `GET /v2/farcaster/user/balance` | `{ \"balances\": [], \"next\": { \"cursor\": null } }` | Token balances are on-chain state outside the Farcaster protocol. |\n| `GET /v2/farcaster/user/subscribed_to` | `{ \"subscriptions\": [], \"next\": { \"cursor\": null } }` | User-to-user subscriptions are not in the protocol. |\n| `GET /v2/farcaster/user/subscribers` | `{ \"subscriptions\": [], \"next\": { \"cursor\": null } }` | Same as above. |\n| `GET /v2/farcaster/user/subscriptions_created` | `{ \"subscriptions\": [], \"next\": { \"cursor\": null } }` | Same as above. |\n\n## Write endpoints\n\nEndpoints that mutate user state (`POST /v2/farcaster/user/register`, `POST /v2/farcaster/user/follow`, `DELETE /v2/farcaster/user/follow`, `POST /v2/farcaster/user/verification`, `DELETE /v2/farcaster/user/verification`, `PATCH /v2/farcaster/user`) are registered but return `501 Not Implemented`. Write operations require submitting a signed Farcaster protocol message via the gRPC `SubmitMessage` endpoint instead.\n", "toc": [ { @@ -878,7 +878,7 @@ "sourcePath": "src/reference/reads/casts.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/casts.md", "slug": "reference/reads/casts", - "contentHtml": "

Casts #

\n

Casts are Farcaster messages — the core unit of user-generated content. Hypersnap serves them directly from its local RocksDB indexes over the message store.

\n

The common Cast response shape includes hash, author (full User), text, timestamp, parent_hash, parent_url, root_parent_url, embeds, mentioned_profiles, reactions (aggregate counts), and replies.count. See src/api/types.rs for the complete type.

\n
\n

GET /v2/farcaster/cast #

\n

Look up a single cast by hash or URL.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
identifierstringyesEither a 0x-prefixed cast hash or a Warpcast-style URL.
type\"hash\" | \"url\"noDefaults to \"hash\".
fidu64noWhen type=\"hash\" and the hash is ambiguous, narrows to a specific author.
\n

Response

\n
{ \"cast\": { \"hash\": \"0x...\", \"author\": { ... }, \"text\": \"...\", \"...\" : \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/cast/bulk #

\n

Batch cast lookup by hash list. Also reachable as GET /v2/farcaster/casts (spec-style plural), which accepts the same list under the casts query parameter instead of hashes.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
hashesstringyes (for /cast/bulk)Comma-separated 0x-prefixed cast hashes.
castsstringyes (for /casts)Same shape — accepted as an alternate parameter name.
\n

Response

\n
{ \"casts\": [ { \"hash\": \"0x...\", \"...\" : \"...\" } ] }\n
\n

Each lookup is O(1) via the cast_hash index — no shard scan.

\n
\n
\n

GET /v2/farcaster/cast/search #

\n

Full-text search over cast content.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
qstringyesSearch query.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{ \"casts\": [ ... ], \"next\": { \"cursor\": \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/cast/conversation #

\n

Fetch a cast plus its reply tree up to a given depth. Ideal for rendering a thread view.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
identifierstringyesRoot cast hash or URL.
type\"hash\" | \"url\"yesDisambiguate what identifier is.
reply_depthu32no05, default 2. How many levels of replies to include.
\n

Response

\n
{\n  \"cast\": { \"hash\": \"0x...\", \"...\" : \"...\" },\n  \"replies\": [\n    {\n      \"cast\": { \"hash\": \"0x...\", \"...\" : \"...\" },\n      \"replies\": [ /* same recursive shape, up to reply_depth */ ]\n    }\n  ]\n}\n
\n

The top-level cast is the thread root. Each reply node has its own cast plus a nested replies array. Depth is capped at 5 to keep responses bounded.

\n
\n
\n

GET /v2/farcaster/cast/quotes #

\n

Casts that quote (embed the CastId of) a given cast. Backed by the CastQuotesIndexer — a reverse index populated on backfill and kept live.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
identifierstringyes0x-prefixed hash of the quoted cast.
type\"hash\"noOnly hash is supported.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{ \"casts\": [ { \"hash\": \"0x...\", \"...\" : \"...\" } ], \"next\": { \"cursor\": null } }\n
\n
\n
\n

GET /v2/farcaster/cast/metrics #

\n

Aggregate cast search volume over a time interval. Currently returns an empty metrics array — per-cast metrics are available via the feed endpoints (which attach likes/recasts/replies counts), but aggregate time-series analytics over arbitrary search queries are not computed on-node.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
qstringyesSearch query.
intervalstringno1d, 7d, 30d, 90d, 180d.
author_fidu64noNarrow to a specific author.
channel_idstringnoNarrow to a specific channel.
\n

Response

\n
{ \"metrics\": [], \"next\": { \"cursor\": null } }\n
\n
\n
\n

GET /v2/farcaster/cast/conversation/summary #

\n

LLM-generated conversation summary. This node does not run an LLM — the endpoint is registered for SDK compatibility and returns a short placeholder string.

\n

Response

\n
{ \"summary\": \"Conversation summaries require LLM integration which is not available on this node.\" }\n
\n
\n

GET /v2/farcaster/cast/embed/crawl #

\n

Crawl and extract metadata from an embed URL. URL crawling requires an external HTTP service — not performed on-node. Returns { \"metadata\": null }.

\n

Write endpoints #

\n

POST /v2/farcaster/cast and DELETE /v2/farcaster/cast are registered but return 501 Not Implemented. Submit casts via signed protocol messages through the gRPC SubmitMessage endpoint.

", + "contentHtml": "

Casts #

\n

Casts are Farcaster messages — the core unit of user-generated content. Hypersnap serves them directly from its local RocksDB indexes over the message store.

\n

The common Cast response shape includes hash, author (full User), text, timestamp, parent_hash, parent_url, root_parent_url, embeds, mentioned_profiles, reactions (aggregate counts), and replies.count. See src/api/types.rs for the complete type.

\n
\n

GET /v2/farcaster/cast #

\n

Look up a single cast by hash or URL.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
identifierstringyesEither a 0x-prefixed cast hash or a Warpcast-style URL.
type\"hash\" | \"url\"noDefaults to \"hash\".
fidu64noWhen type=\"hash\" and the hash is ambiguous, narrows to a specific author.
\n

Response

\n
{ \"cast\": { \"hash\": \"0x...\", \"author\": { ... }, \"text\": \"...\", \"...\" : \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/cast/bulk #

\n

Batch cast lookup by hash list. Also reachable as GET /v2/farcaster/casts (spec-style plural), which accepts the same list under the casts query parameter instead of hashes.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
hashesstringyes (for /cast/bulk)Comma-separated 0x-prefixed cast hashes.
castsstringyes (for /casts)Same shape — accepted as an alternate parameter name.
\n

Response

\n
{ \"casts\": [ { \"hash\": \"0x...\", \"...\" : \"...\" } ] }\n
\n

Each lookup is O(1) via the cast_hash index — no shard scan.

\n
\n
\n

GET /v2/farcaster/cast/search #

\n

Full-text search over cast content.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
qstringyesSearch query.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{ \"casts\": [ ... ], \"next\": { \"cursor\": \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/cast/conversation #

\n

Fetch a cast plus its reply tree up to a given depth. Ideal for rendering a thread view.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
identifierstringyesRoot cast hash or URL.
type\"hash\" | \"url\"yesDisambiguate what identifier is.
reply_depthu32no05, default 2. How many levels of replies to include.
\n

Response

\n
{\n  \"cast\": { \"hash\": \"0x...\", \"...\" : \"...\" },\n  \"replies\": [\n    {\n      \"cast\": { \"hash\": \"0x...\", \"...\" : \"...\" },\n      \"replies\": [ /* same recursive shape, up to reply_depth */ ]\n    }\n  ]\n}\n
\n

The top-level cast is the thread root. Each reply node has its own cast plus a nested replies array. Depth is capped at 5 to keep responses bounded.

\n
\n
\n

GET /v2/farcaster/cast/quotes #

\n

Casts that quote (embed the CastId of) a given cast. Backed by the CastQuotesIndexer — a reverse index populated on backfill and kept live.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
identifierstringyes0x-prefixed hash of the quoted cast.
type\"hash\"noOnly hash is supported.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{ \"casts\": [ { \"hash\": \"0x...\", \"...\" : \"...\" } ], \"next\": { \"cursor\": null } }\n
\n
\n
\n

GET /v2/farcaster/cast/metrics #

\n

Aggregate cast search volume over a time interval. Currently returns an empty metrics array — per-cast metrics are available via the feed endpoints (which attach likes/recasts/replies counts), but aggregate time-series analytics over arbitrary search queries are not computed on-node.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
qstringyesSearch query.
intervalstringno1d, 7d, 30d, 90d, 180d.
author_fidu64noNarrow to a specific author.
channel_idstringnoNarrow to a specific channel.
\n

Response

\n
{ \"metrics\": [], \"next\": { \"cursor\": null } }\n
\n
\n
\n

GET /v2/farcaster/cast/conversation/summary #

\n

LLM-generated conversation summary. This node does not run an LLM — the endpoint is registered for SDK compatibility and returns a short placeholder string.

\n

Response

\n
{ \"summary\": \"Conversation summaries require LLM integration which is not available on this node.\" }\n
\n
\n

GET /v2/farcaster/cast/embed/crawl #

\n

Crawl and extract metadata from an embed URL. URL crawling requires an external HTTP service — not performed on-node. Returns { \"metadata\": null }.

\n

Write endpoints #

\n

POST /v2/farcaster/cast and DELETE /v2/farcaster/cast are registered but return 501 Not Implemented. Submit casts via signed protocol messages through the gRPC SubmitMessage endpoint.

", "contentMarkdown": "# Casts\n\nCasts are Farcaster messages — the core unit of user-generated content. Hypersnap serves them directly from its local RocksDB indexes over the message store.\n\nThe common `Cast` response shape includes `hash`, `author` (full `User`), `text`, `timestamp`, `parent_hash`, `parent_url`, `root_parent_url`, `embeds`, `mentioned_profiles`, `reactions` (aggregate counts), and `replies.count`. See `src/api/types.rs` for the complete type.\n\n---\n\n## GET /v2/farcaster/cast\n\nLook up a single cast by hash or URL.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `identifier` | string | yes | Either a `0x`-prefixed cast hash or a Warpcast-style URL. |\n| `type` | `\"hash\"` \\| `\"url\"` | no | Defaults to `\"hash\"`. |\n| `fid` | u64 | no | When `type=\"hash\"` and the hash is ambiguous, narrows to a specific author. |\n\n**Response**\n\n```json\n{ \"cast\": { \"hash\": \"0x...\", \"author\": { ... }, \"text\": \"...\", \"...\" : \"...\" } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/cast/bulk\n\nBatch cast lookup by hash list. Also reachable as `GET /v2/farcaster/casts` (spec-style plural), which accepts the same list under the `casts` query parameter instead of `hashes`.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `hashes` | string | yes (for `/cast/bulk`) | Comma-separated `0x`-prefixed cast hashes. |\n| `casts` | string | yes (for `/casts`) | Same shape — accepted as an alternate parameter name. |\n\n**Response**\n\n```json\n{ \"casts\": [ { \"hash\": \"0x...\", \"...\" : \"...\" } ] }\n```\n\nEach lookup is O(1) via the `cast_hash` index — no shard scan.\n\n
\n\n---\n\n## GET /v2/farcaster/cast/search\n\nFull-text search over cast content.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `q` | string | yes | Search query. |\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Pagination cursor. |\n\n**Response**\n\n```json\n{ \"casts\": [ ... ], \"next\": { \"cursor\": \"...\" } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/cast/conversation\n\nFetch a cast plus its reply tree up to a given depth. Ideal for rendering a thread view.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `identifier` | string | yes | Root cast hash or URL. |\n| `type` | `\"hash\"` \\| `\"url\"` | yes | Disambiguate what `identifier` is. |\n| `reply_depth` | u32 | no | `0`–`5`, default `2`. How many levels of replies to include. |\n\n**Response**\n\n```json\n{\n \"cast\": { \"hash\": \"0x...\", \"...\" : \"...\" },\n \"replies\": [\n {\n \"cast\": { \"hash\": \"0x...\", \"...\" : \"...\" },\n \"replies\": [ /* same recursive shape, up to reply_depth */ ]\n }\n ]\n}\n```\n\nThe top-level `cast` is the thread root. Each reply node has its own `cast` plus a nested `replies` array. Depth is capped at 5 to keep responses bounded.\n\n
\n\n---\n\n## GET /v2/farcaster/cast/quotes\n\nCasts that quote (embed the `CastId` of) a given cast. Backed by the `CastQuotesIndexer` — a reverse index populated on backfill and kept live.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `identifier` | string | yes | `0x`-prefixed hash of the quoted cast. |\n| `type` | `\"hash\"` | no | Only `hash` is supported. |\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Pagination cursor. |\n\n**Response**\n\n```json\n{ \"casts\": [ { \"hash\": \"0x...\", \"...\" : \"...\" } ], \"next\": { \"cursor\": null } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/cast/metrics\n\nAggregate cast search volume over a time interval. Currently returns an empty metrics array — per-cast metrics are available via the feed endpoints (which attach likes/recasts/replies counts), but aggregate time-series analytics over arbitrary search queries are not computed on-node.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `q` | string | yes | Search query. |\n| `interval` | string | no | `1d`, `7d`, `30d`, `90d`, `180d`. |\n| `author_fid` | u64 | no | Narrow to a specific author. |\n| `channel_id` | string | no | Narrow to a specific channel. |\n\n**Response**\n\n```json\n{ \"metrics\": [], \"next\": { \"cursor\": null } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/cast/conversation/summary\n\nLLM-generated conversation summary. This node does not run an LLM — the endpoint is registered for SDK compatibility and returns a short placeholder string.\n\n**Response**\n\n```json\n{ \"summary\": \"Conversation summaries require LLM integration which is not available on this node.\" }\n```\n\n---\n\n## GET /v2/farcaster/cast/embed/crawl\n\nCrawl and extract metadata from an embed URL. URL crawling requires an external HTTP service — not performed on-node. Returns `{ \"metadata\": null }`.\n\n## Write endpoints\n\n`POST /v2/farcaster/cast` and `DELETE /v2/farcaster/cast` are registered but return `501 Not Implemented`. Submit casts via signed protocol messages through the gRPC `SubmitMessage` endpoint.\n", "toc": [ { @@ -934,7 +934,7 @@ "sourcePath": "src/reference/reads/feeds.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/feeds.md", "slug": "reference/reads/feeds", - "contentHtml": "

Feeds #

\n

Feed endpoints return ordered lists of casts. Shape is always:

\n
{ \"casts\": [ { /* Cast */ } ], \"next\": { \"cursor\": \"...\" } }\n
\n

See Pagination & cursors for how to walk large result sets.

\n
\n

GET /v2/farcaster/feed #

\n

Generic feed endpoint. Behavior depends on feed_type.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
feed_typestringno\"following\" (default), \"trending\", or a future feed type.
fidu64conditionalRequired when feed_type=\"following\" — whose feed to render.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n
\n
\n

GET /v2/farcaster/feed/following #

\n

Explicit alias of /feed?feed_type=following.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n

Semantics — casts authored by users fid follows, ordered newest-first.

\n
\n
\n

GET /v2/farcaster/feed/trending #

\n

Network-wide trending casts, ranked by an engagement heuristic over a rolling window.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/channels #

\n

Feed of casts parented to one or more channels (either by channel id or by parent URL).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
channel_idsstringyesComma-separated channel ids or channel parent URLs.
\n

Example

\n
curl -s \"https://haatz.quilibrium.com/v2/farcaster/feed/channels?channel_ids=memes,base,dev\"\n
\n
\n
\n

GET /v2/farcaster/feed/parent_urls #

\n

Feed of casts with a matching parent_url. Useful when you have raw parent URLs (not yet resolved to a channel id).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
parent_urlsstringyesComma-separated parent URLs.
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/for_you #

\n

Personalized \"For You\" feed. Personalization requires model inference which isn't available on-node, so this endpoint currently returns the trending feed. Registered for SDK compatibility.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/topic #

\n

Feed filtered by topic slug. No on-node hashtag/topic indexing exists, so this endpoint currently returns the trending feed. Registered for SDK compatibility.

\n
\n
\n

GET /v2/farcaster/feed/user/casts #

\n

User's casts in reverse chronological order. Each cast is enriched with engagement metrics (likes, recasts, replies counts) from the MetricsIndexer.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/user/popular #

\n

A user's top 10 casts by engagement score (likes + recasts + replies), computed from the local MetricsIndexer.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n
\n
\n

GET /v2/farcaster/feed/user/replies_and_recasts #

\n

A user's casts filtered to only replies (casts with a parent field).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
", + "contentHtml": "

Feeds #

\n

Feed endpoints return ordered lists of casts. Shape is always:

\n
{ \"casts\": [ { /* Cast */ } ], \"next\": { \"cursor\": \"...\" } }\n
\n

See Pagination & cursors for how to walk large result sets.

\n
\n

GET /v2/farcaster/feed #

\n

Generic feed endpoint. Behavior depends on feed_type.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
feed_typestringno\"following\" (default), \"trending\", or a future feed type.
fidu64conditionalRequired when feed_type=\"following\" — whose feed to render.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n
\n
\n

GET /v2/farcaster/feed/following #

\n

Explicit alias of /feed?feed_type=following.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n

Semantics — casts authored by users fid follows, ordered newest-first.

\n
\n
\n

GET /v2/farcaster/feed/trending #

\n

Network-wide trending casts, ranked by an engagement heuristic over a rolling window.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/channels #

\n

Feed of casts parented to one or more channels (either by channel id or by parent URL).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
channel_idsstringyesComma-separated channel ids or channel parent URLs.
\n

Example

\n
curl -s \"https://haatz.quilibrium.com/v2/farcaster/feed/channels?channel_ids=memes,base,dev\"\n
\n
\n
\n

GET /v2/farcaster/feed/parent_urls #

\n

Feed of casts with a matching parent_url. Useful when you have raw parent URLs (not yet resolved to a channel id).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
parent_urlsstringyesComma-separated parent URLs.
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/for_you #

\n

Personalized \"For You\" feed. Personalization requires model inference which isn't available on-node, so this endpoint currently returns the trending feed. Registered for SDK compatibility.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/topic #

\n

Feed filtered by topic slug. No on-node hashtag/topic indexing exists, so this endpoint currently returns the trending feed. Registered for SDK compatibility.

\n
\n
\n

GET /v2/farcaster/feed/user/casts #

\n

User's casts in reverse chronological order. Each cast is enriched with engagement metrics (likes, recasts, replies counts) from the MetricsIndexer.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/feed/user/popular #

\n

A user's top 10 casts by engagement score (likes + recasts + replies), computed from the local MetricsIndexer.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n
\n
\n

GET /v2/farcaster/feed/user/replies_and_recasts #

\n

A user's casts filtered to only replies (casts with a parent field).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
", "contentMarkdown": "# Feeds\n\nFeed endpoints return ordered lists of casts. Shape is always:\n\n```json\n{ \"casts\": [ { /* Cast */ } ], \"next\": { \"cursor\": \"...\" } }\n```\n\nSee [Pagination & cursors](../../concepts/pagination.md) for how to walk large result sets.\n\n---\n\n## GET /v2/farcaster/feed\n\nGeneric feed endpoint. Behavior depends on `feed_type`.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `feed_type` | string | no | `\"following\"` (default), `\"trending\"`, or a future feed type. |\n| `fid` | u64 | conditional | Required when `feed_type=\"following\"` — whose feed to render. |\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Pagination cursor. |\n\n
\n\n---\n\n## GET /v2/farcaster/feed/following\n\nExplicit alias of `/feed?feed_type=following`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n**Semantics** — casts authored by users `fid` follows, ordered newest-first.\n\n
\n\n---\n\n## GET /v2/farcaster/feed/trending\n\nNetwork-wide trending casts, ranked by an engagement heuristic over a rolling window.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n---\n\n## GET /v2/farcaster/feed/channels\n\nFeed of casts parented to one or more channels (either by channel id or by parent URL).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `channel_ids` | string | yes | Comma-separated channel ids or channel parent URLs. |\n\n**Example**\n\n```bash\ncurl -s \"https://haatz.quilibrium.com/v2/farcaster/feed/channels?channel_ids=memes,base,dev\"\n```\n\n
\n\n---\n\n## GET /v2/farcaster/feed/parent_urls\n\nFeed of casts with a matching `parent_url`. Useful when you have raw parent URLs (not yet resolved to a channel id).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `parent_urls` | string | yes | Comma-separated parent URLs. |\n| `limit` | usize | no | |\n| `cursor` | string | no | |\n\n
\n\n---\n\n## GET /v2/farcaster/feed/for_you\n\nPersonalized \"For You\" feed. Personalization requires model inference which isn't available on-node, so this endpoint currently returns the trending feed. Registered for SDK compatibility.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n---\n\n## GET /v2/farcaster/feed/topic\n\nFeed filtered by topic slug. No on-node hashtag/topic indexing exists, so this endpoint currently returns the trending feed. Registered for SDK compatibility.\n\n
\n\n---\n\n## GET /v2/farcaster/feed/user/casts\n\nUser's casts in reverse chronological order. Each cast is enriched with engagement metrics (likes, recasts, replies counts) from the `MetricsIndexer`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n---\n\n## GET /v2/farcaster/feed/user/popular\n\nA user's top 10 casts by engagement score (likes + recasts + replies), computed from the local `MetricsIndexer`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no | Default `10`, capped at `10`. |\n\n
\n\n---\n\n## GET /v2/farcaster/feed/user/replies_and_recasts\n\nA user's casts filtered to only replies (casts with a `parent` field).\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n", "toc": [ { @@ -995,7 +995,7 @@ "sourcePath": "src/reference/reads/channels.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/channels.md", "slug": "reference/reads/channels", - "contentHtml": "

Channels #

\n

Farcaster channels are parent-URL-scoped subcommunities. Hypersnap maintains a local channel registry and membership index.

\n

Common Channel fields: id, parent_url, name, image_url, description, lead (the channel host's User), moderator_fids, follower_count, created_at.

\n
\n

GET /v2/farcaster/channel #

\n

Look up a single channel.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
idstringyesEither the channel id (e.g. memes) or the parent URL.
type\"id\" | \"parent_url\"noDefault \"id\".
\n

Response

\n
{ \"channel\": { \"id\": \"memes\", \"...\" : \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/channel/all #

\n

List every channel the node knows about. Also reachable as GET /v2/farcaster/channel/list (spec-compat alias).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/channel/bulk #

\n

Batch channel lookup.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
idsstringyesComma-separated channel ids.
\n
\n
\n

GET /v2/farcaster/channel/search #

\n

Prefix-style search against the channel name index.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
qstringyes
limitusizeno
\n
\n
\n

GET /v2/farcaster/channel/trending #

\n

Channels with the most engagement over a rolling window.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
\n
\n
\n

GET /v2/farcaster/channel/members #

\n

Members of a specific channel. Also reachable as /v2/farcaster/channel/member/list, /v2/farcaster/channel/followers, and /v2/farcaster/channel/followers/relevant — all three forward to the same member-list handler.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
channel_idstringyesThe channel id.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{ \"users\": [ { /* User */ } ], \"next\": { \"cursor\": \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/channel/member/invite/list #

\n

List open channel-member invites. The Farcaster protocol does not include an on-chain invite system — this endpoint is registered for SDK compatibility and returns an empty list.

\n

Response

\n
{ \"members\": [], \"next\": { \"cursor\": null } }\n
\n
\n

GET /v2/farcaster/channel/user-active #

\n

Channels where a specific user has recently been active. Also reachable as GET /v2/farcaster/channel/user (spec-compat alias).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n

Write endpoints #

\n

POST /v2/farcaster/channel/follow, DELETE /v2/farcaster/channel/follow, POST /v2/farcaster/channel/member/invite, PUT /v2/farcaster/channel/member/invite, and DELETE /v2/farcaster/channel/member are registered but return 501 Not Implemented. Follow/unfollow is expressed through the protocol via signed LinkAdd/LinkRemove messages.

", + "contentHtml": "

Channels #

\n

Farcaster channels are parent-URL-scoped subcommunities. Hypersnap maintains a local channel registry and membership index.

\n

Common Channel fields: id, parent_url, name, image_url, description, lead (the channel host's User), moderator_fids, follower_count, created_at.

\n
\n

GET /v2/farcaster/channel #

\n

Look up a single channel.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
idstringyesEither the channel id (e.g. memes) or the parent URL.
type\"id\" | \"parent_url\"noDefault \"id\".
\n

Response

\n
{ \"channel\": { \"id\": \"memes\", \"...\" : \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/channel/all #

\n

List every channel the node knows about. Also reachable as GET /v2/farcaster/channel/list (spec-compat alias).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/channel/bulk #

\n

Batch channel lookup.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
idsstringyesComma-separated channel ids.
\n
\n
\n

GET /v2/farcaster/channel/search #

\n

Prefix-style search against the channel name index.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
qstringyes
limitusizeno
\n
\n
\n

GET /v2/farcaster/channel/trending #

\n

Channels with the most engagement over a rolling window.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
limitusizeno
\n
\n
\n

GET /v2/farcaster/channel/members #

\n

Members of a specific channel. Also reachable as /v2/farcaster/channel/member/list, /v2/farcaster/channel/followers, and /v2/farcaster/channel/followers/relevant — all three forward to the same member-list handler.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
channel_idstringyesThe channel id.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{ \"users\": [ { /* User */ } ], \"next\": { \"cursor\": \"...\" } }\n
\n
\n
\n

GET /v2/farcaster/channel/member/invite/list #

\n

List open channel-member invites. The Farcaster protocol does not include an on-chain invite system — this endpoint is registered for SDK compatibility and returns an empty list.

\n

Response

\n
{ \"members\": [], \"next\": { \"cursor\": null } }\n
\n
\n

GET /v2/farcaster/channel/user-active #

\n

Channels where a specific user has recently been active. Also reachable as GET /v2/farcaster/channel/user (spec-compat alias).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n

Write endpoints #

\n

POST /v2/farcaster/channel/follow, DELETE /v2/farcaster/channel/follow, POST /v2/farcaster/channel/member/invite, PUT /v2/farcaster/channel/member/invite, and DELETE /v2/farcaster/channel/member are registered but return 501 Not Implemented. Follow/unfollow is expressed through the protocol via signed LinkAdd/LinkRemove messages.

", "contentMarkdown": "# Channels\n\nFarcaster channels are parent-URL-scoped subcommunities. Hypersnap maintains a local channel registry and membership index.\n\nCommon `Channel` fields: `id`, `parent_url`, `name`, `image_url`, `description`, `lead` (the channel host's `User`), `moderator_fids`, `follower_count`, `created_at`.\n\n---\n\n## GET /v2/farcaster/channel\n\nLook up a single channel.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `id` | string | yes | Either the channel id (e.g. `memes`) or the parent URL. |\n| `type` | `\"id\"` \\| `\"parent_url\"` | no | Default `\"id\"`. |\n\n**Response**\n\n```json\n{ \"channel\": { \"id\": \"memes\", \"...\" : \"...\" } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/channel/all\n\nList every channel the node knows about. Also reachable as `GET /v2/farcaster/channel/list` (spec-compat alias).\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n---\n\n## GET /v2/farcaster/channel/bulk\n\nBatch channel lookup.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `ids` | string | yes | Comma-separated channel ids. |\n\n
\n\n---\n\n## GET /v2/farcaster/channel/search\n\nPrefix-style search against the channel name index.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `q` | string | yes |\n| `limit` | usize | no |\n\n
\n\n---\n\n## GET /v2/farcaster/channel/trending\n\nChannels with the most engagement over a rolling window.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `limit` | usize | no |\n\n
\n\n---\n\n## GET /v2/farcaster/channel/members\n\nMembers of a specific channel. Also reachable as `/v2/farcaster/channel/member/list`, `/v2/farcaster/channel/followers`, and `/v2/farcaster/channel/followers/relevant` — all three forward to the same member-list handler.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `channel_id` | string | yes | The channel id. |\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Pagination cursor. |\n\n**Response**\n\n```json\n{ \"users\": [ { /* User */ } ], \"next\": { \"cursor\": \"...\" } }\n```\n\n
\n\n---\n\n## GET /v2/farcaster/channel/member/invite/list\n\nList open channel-member invites. The Farcaster protocol does not include an on-chain invite system — this endpoint is registered for SDK compatibility and returns an empty list.\n\n**Response**\n\n```json\n{ \"members\": [], \"next\": { \"cursor\": null } }\n```\n\n---\n\n## GET /v2/farcaster/channel/user-active\n\nChannels where a specific user has recently been active. Also reachable as `GET /v2/farcaster/channel/user` (spec-compat alias).\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n## Write endpoints\n\n`POST /v2/farcaster/channel/follow`, `DELETE /v2/farcaster/channel/follow`, `POST /v2/farcaster/channel/member/invite`, `PUT /v2/farcaster/channel/member/invite`, and `DELETE /v2/farcaster/channel/member` are registered but return `501 Not Implemented`. Follow/unfollow is expressed through the protocol via signed `LinkAdd`/`LinkRemove` messages.\n", "toc": [ { @@ -1051,7 +1051,7 @@ "sourcePath": "src/reference/reads/reactions.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/reactions.md", "slug": "reference/reads/reactions", - "contentHtml": "

Reactions #

\n

Reactions are likes and recasts. Hypersnap indexes both directions — \"who reacted to this cast\" and \"what has this user reacted to\".

\n

Common response shape:

\n
{\n  \"reactions\": [\n    {\n      \"reaction_type\": \"like\",\n      \"user\": { /* User */ },\n      \"cast\":  { /* Cast */ },\n      \"timestamp\": 1712345678\n    }\n  ],\n  \"next\": { \"cursor\": \"...\" }\n}\n
\n
\n

GET /v2/farcaster/reaction #

\n

Generic reaction lookup. If hash is provided, acts like /reaction/cast (reactions targeting the cast). Otherwise acts like /reaction/user (reactions made by fid).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
hashstringconditionalIf provided, switches to cast mode.
types\"likes\" | \"recasts\"noFor cast mode. Default \"likes\".
type\"likes\" | \"recasts\"noFor user mode. Default \"likes\".
fidu64conditionalRequired in user mode.
limitusizenoDefault 10.
\n
\n
\n

GET /v2/farcaster/reaction/cast #

\n

Who has reacted to a specific cast. Also reachable as GET /v2/farcaster/reactions/cast (spec-compat plural).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
hashstringyes0x-prefixed cast hash.
types\"likes\" | \"recasts\"noDefault \"likes\".
fidu64noIf the cast hash is ambiguous, narrows to a specific author.
limitusizenoDefault 10.
\n
\n
\n

GET /v2/farcaster/reaction/user #

\n

What a specific user has liked or recasted. Also reachable as GET /v2/farcaster/reactions/user (spec-compat plural).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe reactor.
type\"likes\" | \"recasts\"noDefault \"likes\".
limitusizeno
\n
\n

Write endpoints #

\n

POST /v2/farcaster/reaction and DELETE /v2/farcaster/reaction are registered but return 501 Not Implemented. Submit signed ReactionAdd/ReactionRemove messages via the gRPC SubmitMessage endpoint.

", + "contentHtml": "

Reactions #

\n

Reactions are likes and recasts. Hypersnap indexes both directions — \"who reacted to this cast\" and \"what has this user reacted to\".

\n

Common response shape:

\n
{\n  \"reactions\": [\n    {\n      \"reaction_type\": \"like\",\n      \"user\": { /* User */ },\n      \"cast\":  { /* Cast */ },\n      \"timestamp\": 1712345678\n    }\n  ],\n  \"next\": { \"cursor\": \"...\" }\n}\n
\n
\n

GET /v2/farcaster/reaction #

\n

Generic reaction lookup. If hash is provided, acts like /reaction/cast (reactions targeting the cast). Otherwise acts like /reaction/user (reactions made by fid).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
hashstringconditionalIf provided, switches to cast mode.
types\"likes\" | \"recasts\"noFor cast mode. Default \"likes\".
type\"likes\" | \"recasts\"noFor user mode. Default \"likes\".
fidu64conditionalRequired in user mode.
limitusizenoDefault 10.
\n
\n
\n

GET /v2/farcaster/reaction/cast #

\n

Who has reacted to a specific cast. Also reachable as GET /v2/farcaster/reactions/cast (spec-compat plural).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
hashstringyes0x-prefixed cast hash.
types\"likes\" | \"recasts\"noDefault \"likes\".
fidu64noIf the cast hash is ambiguous, narrows to a specific author.
limitusizenoDefault 10.
\n
\n
\n

GET /v2/farcaster/reaction/user #

\n

What a specific user has liked or recasted. Also reachable as GET /v2/farcaster/reactions/user (spec-compat plural).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe reactor.
type\"likes\" | \"recasts\"noDefault \"likes\".
limitusizeno
\n
\n

Write endpoints #

\n

POST /v2/farcaster/reaction and DELETE /v2/farcaster/reaction are registered but return 501 Not Implemented. Submit signed ReactionAdd/ReactionRemove messages via the gRPC SubmitMessage endpoint.

", "contentMarkdown": "# Reactions\n\nReactions are likes and recasts. Hypersnap indexes both directions — \"who reacted to this cast\" and \"what has this user reacted to\".\n\nCommon response shape:\n\n```json\n{\n \"reactions\": [\n {\n \"reaction_type\": \"like\",\n \"user\": { /* User */ },\n \"cast\": { /* Cast */ },\n \"timestamp\": 1712345678\n }\n ],\n \"next\": { \"cursor\": \"...\" }\n}\n```\n\n---\n\n## GET /v2/farcaster/reaction\n\nGeneric reaction lookup. If `hash` is provided, acts like `/reaction/cast` (reactions targeting the cast). Otherwise acts like `/reaction/user` (reactions made by `fid`).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `hash` | string | conditional | If provided, switches to cast mode. |\n| `types` | `\"likes\"` \\| `\"recasts\"` | no | For cast mode. Default `\"likes\"`. |\n| `type` | `\"likes\"` \\| `\"recasts\"` | no | For user mode. Default `\"likes\"`. |\n| `fid` | u64 | conditional | Required in user mode. |\n| `limit` | usize | no | Default `10`. |\n\n
\n\n---\n\n## GET /v2/farcaster/reaction/cast\n\nWho has reacted to a specific cast. Also reachable as `GET /v2/farcaster/reactions/cast` (spec-compat plural).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `hash` | string | yes | `0x`-prefixed cast hash. |\n| `types` | `\"likes\"` \\| `\"recasts\"` | no | Default `\"likes\"`. |\n| `fid` | u64 | no | If the cast hash is ambiguous, narrows to a specific author. |\n| `limit` | usize | no | Default `10`. |\n\n
\n\n---\n\n## GET /v2/farcaster/reaction/user\n\nWhat a specific user has liked or recasted. Also reachable as `GET /v2/farcaster/reactions/user` (spec-compat plural).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The reactor. |\n| `type` | `\"likes\"` \\| `\"recasts\"` | no | Default `\"likes\"`. |\n| `limit` | usize | no | |\n\n
\n\n## Write endpoints\n\n`POST /v2/farcaster/reaction` and `DELETE /v2/farcaster/reaction` are registered but return `501 Not Implemented`. Submit signed `ReactionAdd`/`ReactionRemove` messages via the gRPC `SubmitMessage` endpoint.\n", "toc": [ { @@ -1082,7 +1082,7 @@ "sourcePath": "src/reference/reads/follows.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/follows.md", "slug": "reference/reads/follows", - "contentHtml": "

Follows #

\n

Who follows whom, with cursor pagination.

\n

Response shape:

\n
{\n  \"users\": [ { /* User */ } ],\n  \"next\": { \"cursor\": \"...\" }\n}\n
\n
\n

GET /v2/farcaster/user/followers #

\n

Users who follow fid. Also reachable as the alias GET /v2/farcaster/followers.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesWhose followers to list.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n
\n
\n

GET /v2/farcaster/user/following #

\n

Users that fid follows. Also reachable as GET /v2/farcaster/following and GET /v2/farcaster/follows (spec-compat aliases).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe follower.
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/followers/relevant #

\n

Users who follow fid with relevance-style context. Relevance scoring is not implemented on-node, so this endpoint returns the same data as /user/followers (for SDK compatibility).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/followers/reciprocal #

\n

Users where fid both follows and is followed by them (mutual follows). Computed on-demand from the social graph — iterates the followers list and filters each entry through are_mutual_follows().

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/following/suggested #

\n

Suggested follows computed via friends-of-friends: who the people fid follows are also following, ranked by overlap frequency, excluding already-followed users.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n

ResponseFollowersResponse.

\n
\n

Write endpoints #

\n

POST /v2/farcaster/follow, DELETE /v2/farcaster/follow, POST /v2/farcaster/user/follow, and DELETE /v2/farcaster/user/follow are registered but return 501 Not Implemented. Submit signed LinkAdd/LinkRemove messages via the gRPC SubmitMessage endpoint.

", + "contentHtml": "

Follows #

\n

Who follows whom, with cursor pagination.

\n

Response shape:

\n
{\n  \"users\": [ { /* User */ } ],\n  \"next\": { \"cursor\": \"...\" }\n}\n
\n
\n

GET /v2/farcaster/user/followers #

\n

Users who follow fid. Also reachable as the alias GET /v2/farcaster/followers.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesWhose followers to list.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n
\n
\n

GET /v2/farcaster/user/following #

\n

Users that fid follows. Also reachable as GET /v2/farcaster/following and GET /v2/farcaster/follows (spec-compat aliases).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe follower.
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/followers/relevant #

\n

Users who follow fid with relevance-style context. Relevance scoring is not implemented on-node, so this endpoint returns the same data as /user/followers (for SDK compatibility).

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/followers/reciprocal #

\n

Users where fid both follows and is followed by them (mutual follows). Computed on-demand from the social graph — iterates the followers list and filters each entry through are_mutual_follows().

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
cursorstringno
\n
\n
\n

GET /v2/farcaster/following/suggested #

\n

Suggested follows computed via friends-of-friends: who the people fid follows are also following, ranked by overlap frequency, excluding already-followed users.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n

ResponseFollowersResponse.

\n
\n

Write endpoints #

\n

POST /v2/farcaster/follow, DELETE /v2/farcaster/follow, POST /v2/farcaster/user/follow, and DELETE /v2/farcaster/user/follow are registered but return 501 Not Implemented. Submit signed LinkAdd/LinkRemove messages via the gRPC SubmitMessage endpoint.

", "contentMarkdown": "# Follows\n\nWho follows whom, with cursor pagination.\n\nResponse shape:\n\n```json\n{\n \"users\": [ { /* User */ } ],\n \"next\": { \"cursor\": \"...\" }\n}\n```\n\n---\n\n## GET /v2/farcaster/user/followers\n\nUsers who follow `fid`. Also reachable as the alias `GET /v2/farcaster/followers`.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | Whose followers to list. |\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Pagination cursor. |\n\n
\n\n---\n\n## GET /v2/farcaster/user/following\n\nUsers that `fid` follows. Also reachable as `GET /v2/farcaster/following` and `GET /v2/farcaster/follows` (spec-compat aliases).\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The follower. |\n| `limit` | usize | no | |\n| `cursor` | string | no | |\n\n
\n\n---\n\n## GET /v2/farcaster/followers/relevant\n\nUsers who follow `fid` with relevance-style context. Relevance scoring is not implemented on-node, so this endpoint returns the same data as `/user/followers` (for SDK compatibility).\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n---\n\n## GET /v2/farcaster/followers/reciprocal\n\nUsers where `fid` both follows and is followed by them (mutual follows). Computed on-demand from the social graph — iterates the followers list and filters each entry through `are_mutual_follows()`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n| `cursor` | string | no |\n\n
\n\n---\n\n## GET /v2/farcaster/following/suggested\n\nSuggested follows computed via friends-of-friends: who the people `fid` follows are also following, ranked by overlap frequency, excluding already-followed users.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no | Default `10`. |\n\n**Response** — `FollowersResponse`.\n\n
\n\n## Write endpoints\n\n`POST /v2/farcaster/follow`, `DELETE /v2/farcaster/follow`, `POST /v2/farcaster/user/follow`, and `DELETE /v2/farcaster/user/follow` are registered but return `501 Not Implemented`. Submit signed `LinkAdd`/`LinkRemove` messages via the gRPC `SubmitMessage` endpoint.\n", "toc": [ { @@ -1123,7 +1123,7 @@ "sourcePath": "src/reference/reads/notifications.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/notifications.md", "slug": "reference/reads/notifications", - "contentHtml": "

User notifications #

\n

Not to be confused with mini-app push notifications — this endpoint is the in-app \"notifications tab\" for a single user: replies to their casts, mentions, likes, recasts, new followers.

\n
\n

GET /v2/farcaster/notifications #

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe viewer.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{\n  \"notifications\": [\n    {\n      \"type\": \"cast-reply\",\n      \"most_recent_timestamp\": 1712345678,\n      \"cast\": { /* the triggering cast */ },\n      \"reactions\": [ /* optional aggregate */ ],\n      \"follows\": [ /* optional aggregate */ ]\n    }\n  ],\n  \"next\": { \"cursor\": \"...\" }\n}\n
\n

The type field discriminates what the notification represents:

\n\n

Notifications are aggregated over a short window so that \"15 people liked your cast\" comes back as one entry with a count, not 15 separate rows.

\n
\n
\n

GET /v2/farcaster/notifications/channel #

\n

Notifications for fid filtered to casts in the specified channel(s). Mentions/replies whose parent cast is in one of the provided channels are returned; everything else is dropped.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe viewer.
channel_idsstringyesComma-separated channel ids or channel parent URLs.
limitusizeno
cursorstringno
\n

Channel ids are resolved to parent URLs via the channels index; raw URLs (starting with http or chain://) are used verbatim.

\n
\n
\n

GET /v2/farcaster/notifications/parent_url #

\n

Notifications for fid filtered to casts whose parent_url matches one of the provided URLs. Same semantics as /notifications/channel but accepts raw parent URLs directly.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe viewer.
parent_urlsstringyesComma-separated parent URLs.
limitusizeno
cursorstringno
\n
\n

Write endpoints #

\n

POST /v2/farcaster/notifications/seen and POST /v2/farcaster/notifications/mark_seen are registered but return 501 Not Implemented. Seen-state is not part of the Farcaster protocol.

", + "contentHtml": "

User notifications #

\n

Not to be confused with mini-app push notifications — this endpoint is the in-app \"notifications tab\" for a single user: replies to their casts, mentions, likes, recasts, new followers.

\n
\n

GET /v2/farcaster/notifications #

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe viewer.
limitusizenoDefault 10.
cursorstringnoPagination cursor.
\n

Response

\n
{\n  \"notifications\": [\n    {\n      \"type\": \"cast-reply\",\n      \"most_recent_timestamp\": 1712345678,\n      \"cast\": { /* the triggering cast */ },\n      \"reactions\": [ /* optional aggregate */ ],\n      \"follows\": [ /* optional aggregate */ ]\n    }\n  ],\n  \"next\": { \"cursor\": \"...\" }\n}\n
\n

The type field discriminates what the notification represents:

\n\n

Notifications are aggregated over a short window so that \"15 people liked your cast\" comes back as one entry with a count, not 15 separate rows.

\n
\n
\n

GET /v2/farcaster/notifications/channel #

\n

Notifications for fid filtered to casts in the specified channel(s). Mentions/replies whose parent cast is in one of the provided channels are returned; everything else is dropped.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe viewer.
channel_idsstringyesComma-separated channel ids or channel parent URLs.
limitusizeno
cursorstringno
\n

Channel ids are resolved to parent URLs via the channels index; raw URLs (starting with http or chain://) are used verbatim.

\n
\n
\n

GET /v2/farcaster/notifications/parent_url #

\n

Notifications for fid filtered to casts whose parent_url matches one of the provided URLs. Same semantics as /notifications/channel but accepts raw parent URLs directly.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequiredNotes
fidu64yesThe viewer.
parent_urlsstringyesComma-separated parent URLs.
limitusizeno
cursorstringno
\n
\n

Write endpoints #

\n

POST /v2/farcaster/notifications/seen and POST /v2/farcaster/notifications/mark_seen are registered but return 501 Not Implemented. Seen-state is not part of the Farcaster protocol.

", "contentMarkdown": "# User notifications\n\nNot to be confused with [mini-app push notifications](../miniapps/index.md) — this endpoint is the in-app \"notifications tab\" for a single user: replies to their casts, mentions, likes, recasts, new followers.\n\n---\n\n## GET /v2/farcaster/notifications\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The viewer. |\n| `limit` | usize | no | Default `10`. |\n| `cursor` | string | no | Pagination cursor. |\n\n**Response**\n\n```json\n{\n \"notifications\": [\n {\n \"type\": \"cast-reply\",\n \"most_recent_timestamp\": 1712345678,\n \"cast\": { /* the triggering cast */ },\n \"reactions\": [ /* optional aggregate */ ],\n \"follows\": [ /* optional aggregate */ ]\n }\n ],\n \"next\": { \"cursor\": \"...\" }\n}\n```\n\nThe `type` field discriminates what the notification represents:\n\n- `\"cast-mention\"` — your FID was `@`-mentioned.\n- `\"cast-reply\"` — a reply to one of your casts.\n- `\"reaction\"` — a like or recast of your cast (aggregated when multiple users do it within a window).\n- `\"follow\"` — someone followed you.\n\nNotifications are aggregated over a short window so that \"15 people liked your cast\" comes back as one entry with a count, not 15 separate rows.\n\n
\n\n---\n\n## GET /v2/farcaster/notifications/channel\n\nNotifications for `fid` filtered to casts in the specified channel(s). Mentions/replies whose parent cast is in one of the provided channels are returned; everything else is dropped.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The viewer. |\n| `channel_ids` | string | yes | Comma-separated channel ids or channel parent URLs. |\n| `limit` | usize | no | |\n| `cursor` | string | no | |\n\nChannel ids are resolved to parent URLs via the channels index; raw URLs (starting with `http` or `chain://`) are used verbatim.\n\n
\n\n---\n\n## GET /v2/farcaster/notifications/parent_url\n\nNotifications for `fid` filtered to casts whose `parent_url` matches one of the provided URLs. Same semantics as `/notifications/channel` but accepts raw parent URLs directly.\n\n**Query parameters**\n\n| Name | Type | Required | Notes |\n|---|---|---|---|\n| `fid` | u64 | yes | The viewer. |\n| `parent_urls` | string | yes | Comma-separated parent URLs. |\n| `limit` | usize | no | |\n| `cursor` | string | no | |\n\n
\n\n## Write endpoints\n\n`POST /v2/farcaster/notifications/seen` and `POST /v2/farcaster/notifications/mark_seen` are registered but return `501 Not Implemented`. Seen-state is not part of the Farcaster protocol.\n", "toc": [ { @@ -1154,7 +1154,7 @@ "sourcePath": "src/reference/reads/usernames.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/usernames.md", "slug": "reference/reads/usernames", - "contentHtml": "

Usernames & proofs #

\n

Farcaster usernames are backed by off-chain \"fnames\" and on-chain ENS/Basenames. These endpoints let you check availability and look up the proof record attached to a name.

\n
\n

GET /v2/farcaster/fname/availability #

\n

Is an fname available to register?

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fnamestringyes
\n

Response

\n
{\n  \"available\": true,\n  \"username\": null\n}\n
\n

If the name is taken, available is false and username contains the normalized name string.

\n
\n
\n

GET /v2/farcaster/username-proof #

\n

Fetch the raw username proof record for a given name. Useful when you want to verify on-chain provenance yourself instead of trusting the resolved User.username field.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
usernamestringyes
\n

Response

\n
{\n  \"username_proof\": {\n    \"timestamp\": 1712345678,\n    \"name\": \"alice\",\n    \"owner\": \"0x...\",\n    \"signature\": \"0x...\",\n    \"fid\": 12345,\n    \"type\": \"USERNAME_TYPE_FNAME\"\n  }\n}\n
\n

If the name has no proof, username_proof is null.

\n
", + "contentHtml": "

Usernames & proofs #

\n

Farcaster usernames are backed by off-chain \"fnames\" and on-chain ENS/Basenames. These endpoints let you check availability and look up the proof record attached to a name.

\n
\n

GET /v2/farcaster/fname/availability #

\n

Is an fname available to register?

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fnamestringyes
\n

Response

\n
{\n  \"available\": true,\n  \"username\": null\n}\n
\n

If the name is taken, available is false and username contains the normalized name string.

\n
\n
\n

GET /v2/farcaster/username-proof #

\n

Fetch the raw username proof record for a given name. Useful when you want to verify on-chain provenance yourself instead of trusting the resolved User.username field.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
usernamestringyes
\n

Response

\n
{\n  \"username_proof\": {\n    \"timestamp\": 1712345678,\n    \"name\": \"alice\",\n    \"owner\": \"0x...\",\n    \"signature\": \"0x...\",\n    \"fid\": 12345,\n    \"type\": \"USERNAME_TYPE_FNAME\"\n  }\n}\n
\n

If the name has no proof, username_proof is null.

\n
", "contentMarkdown": "# Usernames & proofs\n\nFarcaster usernames are backed by off-chain \"fnames\" and on-chain ENS/Basenames. These endpoints let you check availability and look up the proof record attached to a name.\n\n---\n\n## GET /v2/farcaster/fname/availability\n\nIs an fname available to register?\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fname` | string | yes |\n\n**Response**\n\n```json\n{\n \"available\": true,\n \"username\": null\n}\n```\n\nIf the name is taken, `available` is `false` and `username` contains the normalized name string.\n\n
\n\n---\n\n## GET /v2/farcaster/username-proof\n\nFetch the raw username proof record for a given name. Useful when you want to verify on-chain provenance yourself instead of trusting the resolved `User.username` field.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `username` | string | yes |\n\n**Response**\n\n```json\n{\n \"username_proof\": {\n \"timestamp\": 1712345678,\n \"name\": \"alice\",\n \"owner\": \"0x...\",\n \"signature\": \"0x...\",\n \"fid\": 12345,\n \"type\": \"USERNAME_TYPE_FNAME\"\n }\n}\n```\n\nIf the name has no proof, `username_proof` is `null`.\n\n
\n", "toc": [ { @@ -1175,7 +1175,7 @@ "sourcePath": "src/reference/reads/signers.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/signers.md", "slug": "reference/reads/signers", - "contentHtml": "

Signers #

\n

Signer endpoints expose on-chain SignerEventBody records from the KeyRegistry contract — the ed25519 keys a user has registered to sign Farcaster protocol messages.

\n

All responses share the on-chain event shape:

\n
{\n  \"events\": [\n    {\n      \"object\": \"signer\",\n      \"fid\": 3,\n      \"event_type\": \"signer\",\n      \"block_number\": 12345678,\n      \"block_timestamp\": 1712345678,\n      \"signer_key\": \"0x<ed25519-pubkey>\",\n      \"key_type\": 1,\n      \"metadata_type\": 1\n    }\n  ],\n  \"next\": { \"cursor\": null }\n}\n
\n
\n

GET /v2/farcaster/signer #

\n

Signers registered by fid. Also reachable as GET /v2/farcaster/signers (plural) and GET /v2/farcaster/signer/list.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n
\n
\n

GET /v2/farcaster/onchain/signers #

\n

Identical to /v2/farcaster/signer — returns signer events for the given FID. Registered under the /onchain/ namespace for spec compatibility.

\n
\n
\n

GET /v2/farcaster/onchain/id_registry_event #

\n

IdRegistry events for fidRegister, Transfer, ChangeRecovery. Each event exposes block_number, block_timestamp, and event_type.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n
\n
\n

Registered-for-compatibility endpoints #

\n

These endpoints exist for SDK compatibility but return empty responses. They depend on managed signer infrastructure that a self-hosted node does not operate:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PathMethod
/v2/farcaster/signer/signed_keyGET
/v2/farcaster/signer/developer_managedGET
/v2/farcaster/signer/developer_managed/signed_keyGET
\n

Write endpoints #

\n

Signer creation and signed-key registration (POST /v2/farcaster/signer, POST /v2/farcaster/signer/signed_key, POST /v2/farcaster/signer/developer_managed, POST /v2/farcaster/signer/developer_managed/signed_key) return 501 Not Implemented. Register signers directly against the on-chain KeyRegistry contract instead — the node will pick up the new signer on the next block.

", + "contentHtml": "

Signers #

\n

Signer endpoints expose on-chain SignerEventBody records from the KeyRegistry contract — the ed25519 keys a user has registered to sign Farcaster protocol messages.

\n

All responses share the on-chain event shape:

\n
{\n  \"events\": [\n    {\n      \"object\": \"signer\",\n      \"fid\": 3,\n      \"event_type\": \"signer\",\n      \"block_number\": 12345678,\n      \"block_timestamp\": 1712345678,\n      \"signer_key\": \"0x<ed25519-pubkey>\",\n      \"key_type\": 1,\n      \"metadata_type\": 1\n    }\n  ],\n  \"next\": { \"cursor\": null }\n}\n
\n
\n

GET /v2/farcaster/signer #

\n

Signers registered by fid. Also reachable as GET /v2/farcaster/signers (plural) and GET /v2/farcaster/signer/list.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n
\n
\n

GET /v2/farcaster/onchain/signers #

\n

Identical to /v2/farcaster/signer — returns signer events for the given FID. Registered under the /onchain/ namespace for spec compatibility.

\n
\n
\n

GET /v2/farcaster/onchain/id_registry_event #

\n

IdRegistry events for fidRegister, Transfer, ChangeRecovery. Each event exposes block_number, block_timestamp, and event_type.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
\n
\n
\n

Registered-for-compatibility endpoints #

\n

These endpoints exist for SDK compatibility but return empty responses. They depend on managed signer infrastructure that a self-hosted node does not operate:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PathMethod
/v2/farcaster/signer/signed_keyGET
/v2/farcaster/signer/developer_managedGET
/v2/farcaster/signer/developer_managed/signed_keyGET
\n

Write endpoints #

\n

Signer creation and signed-key registration (POST /v2/farcaster/signer, POST /v2/farcaster/signer/signed_key, POST /v2/farcaster/signer/developer_managed, POST /v2/farcaster/signer/developer_managed/signed_key) return 501 Not Implemented. Register signers directly against the on-chain KeyRegistry contract instead — the node will pick up the new signer on the next block.

", "contentMarkdown": "# Signers\n\nSigner endpoints expose on-chain `SignerEventBody` records from the `KeyRegistry` contract — the ed25519 keys a user has registered to sign Farcaster protocol messages.\n\nAll responses share the on-chain event shape:\n\n```json\n{\n \"events\": [\n {\n \"object\": \"signer\",\n \"fid\": 3,\n \"event_type\": \"signer\",\n \"block_number\": 12345678,\n \"block_timestamp\": 1712345678,\n \"signer_key\": \"0x\",\n \"key_type\": 1,\n \"metadata_type\": 1\n }\n ],\n \"next\": { \"cursor\": null }\n}\n```\n\n---\n\n## GET /v2/farcaster/signer\n\nSigners registered by `fid`. Also reachable as `GET /v2/farcaster/signers` (plural) and `GET /v2/farcaster/signer/list`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n\n
\n\n---\n\n## GET /v2/farcaster/onchain/signers\n\nIdentical to `/v2/farcaster/signer` — returns signer events for the given FID. Registered under the `/onchain/` namespace for spec compatibility.\n\n
\n\n---\n\n## GET /v2/farcaster/onchain/id_registry_event\n\n`IdRegistry` events for `fid` — `Register`, `Transfer`, `ChangeRecovery`. Each event exposes `block_number`, `block_timestamp`, and `event_type`.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n\n
\n\n---\n\n## Registered-for-compatibility endpoints\n\nThese endpoints exist for SDK compatibility but return empty responses. They depend on managed signer infrastructure that a self-hosted node does not operate:\n\n| Path | Method |\n|---|---|\n| `/v2/farcaster/signer/signed_key` | GET |\n| `/v2/farcaster/signer/developer_managed` | GET |\n| `/v2/farcaster/signer/developer_managed/signed_key` | GET |\n\n## Write endpoints\n\nSigner creation and signed-key registration (`POST /v2/farcaster/signer`, `POST /v2/farcaster/signer/signed_key`, `POST /v2/farcaster/signer/developer_managed`, `POST /v2/farcaster/signer/developer_managed/signed_key`) return `501 Not Implemented`. Register signers directly against the on-chain `KeyRegistry` contract instead — the node will pick up the new signer on the next block.\n", "toc": [ { @@ -1211,7 +1211,7 @@ "sourcePath": "src/reference/reads/blocks-mutes.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/blocks-mutes.md", "slug": "reference/reads/blocks-mutes", - "contentHtml": "

Blocks, Mutes, Bans #

\n

Block and mute state is expressed in the Farcaster protocol as LinkAdd messages with a non-follow link_type (\"block\" or \"mute\"). Hypersnap surfaces the current block/mute list for a given FID by querying the LinkStore directly.

\n

Ban lists are app-level, not protocol-level — they do not exist in the Farcaster protocol and the corresponding endpoint returns an empty list.

\n

Response shape for all list endpoints:

\n
{\n  \"users\": [ { /* User */ } ],\n  \"next\": { \"cursor\": null }\n}\n
\n
\n

GET /v2/farcaster/block/list #

\n

FIDs that fid has blocked.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n

Backed by LinkStore::get_link_adds_by_fid(fid, \"block\", ...) — a direct index lookup, no scan.

\n
\n
\n

GET /v2/farcaster/mute/list #

\n

FIDs that fid has muted.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n

Backed by LinkStore::get_link_adds_by_fid(fid, \"mute\", ...).

\n
\n
\n

GET /v2/farcaster/ban/list #

\n

Registered for SDK compatibility — bans are an app-level concept and not part of the Farcaster protocol.

\n

Response

\n
{ \"bans\": [], \"next\": { \"cursor\": null } }\n
\n

Write endpoints #

\n

POST /v2/farcaster/block, DELETE /v2/farcaster/block, POST /v2/farcaster/mute, DELETE /v2/farcaster/mute, POST /v2/farcaster/ban, and DELETE /v2/farcaster/ban are registered but return 501 Not Implemented. Submit signed LinkAdd/LinkRemove messages with link_type = \"block\" or \"mute\" via gRPC SubmitMessage.

", + "contentHtml": "

Blocks, Mutes, Bans #

\n

Block and mute state is expressed in the Farcaster protocol as LinkAdd messages with a non-follow link_type (\"block\" or \"mute\"). Hypersnap surfaces the current block/mute list for a given FID by querying the LinkStore directly.

\n

Ban lists are app-level, not protocol-level — they do not exist in the Farcaster protocol and the corresponding endpoint returns an empty list.

\n

Response shape for all list endpoints:

\n
{\n  \"users\": [ { /* User */ } ],\n  \"next\": { \"cursor\": null }\n}\n
\n
\n

GET /v2/farcaster/block/list #

\n

FIDs that fid has blocked.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n

Backed by LinkStore::get_link_adds_by_fid(fid, \"block\", ...) — a direct index lookup, no scan.

\n
\n
\n

GET /v2/farcaster/mute/list #

\n

FIDs that fid has muted.

\n

Query parameters

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
fidu64yes
limitusizeno
\n

Backed by LinkStore::get_link_adds_by_fid(fid, \"mute\", ...).

\n
\n
\n

GET /v2/farcaster/ban/list #

\n

Registered for SDK compatibility — bans are an app-level concept and not part of the Farcaster protocol.

\n

Response

\n
{ \"bans\": [], \"next\": { \"cursor\": null } }\n
\n

Write endpoints #

\n

POST /v2/farcaster/block, DELETE /v2/farcaster/block, POST /v2/farcaster/mute, DELETE /v2/farcaster/mute, POST /v2/farcaster/ban, and DELETE /v2/farcaster/ban are registered but return 501 Not Implemented. Submit signed LinkAdd/LinkRemove messages with link_type = \"block\" or \"mute\" via gRPC SubmitMessage.

", "contentMarkdown": "# Blocks, Mutes, Bans\n\nBlock and mute state is expressed in the Farcaster protocol as `LinkAdd` messages with a non-follow `link_type` (`\"block\"` or `\"mute\"`). Hypersnap surfaces the current block/mute list for a given FID by querying the `LinkStore` directly.\n\nBan lists are **app-level**, not protocol-level — they do not exist in the Farcaster protocol and the corresponding endpoint returns an empty list.\n\nResponse shape for all list endpoints:\n\n```json\n{\n \"users\": [ { /* User */ } ],\n \"next\": { \"cursor\": null }\n}\n```\n\n---\n\n## GET /v2/farcaster/block/list\n\nFIDs that `fid` has blocked.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n\nBacked by `LinkStore::get_link_adds_by_fid(fid, \"block\", ...)` — a direct index lookup, no scan.\n\n
\n\n---\n\n## GET /v2/farcaster/mute/list\n\nFIDs that `fid` has muted.\n\n**Query parameters**\n\n| Name | Type | Required |\n|---|---|---|\n| `fid` | u64 | yes |\n| `limit` | usize | no |\n\nBacked by `LinkStore::get_link_adds_by_fid(fid, \"mute\", ...)`.\n\n
\n\n---\n\n## GET /v2/farcaster/ban/list\n\nRegistered for SDK compatibility — bans are an app-level concept and not part of the Farcaster protocol.\n\n**Response**\n\n```json\n{ \"bans\": [], \"next\": { \"cursor\": null } }\n```\n\n## Write endpoints\n\n`POST /v2/farcaster/block`, `DELETE /v2/farcaster/block`, `POST /v2/farcaster/mute`, `DELETE /v2/farcaster/mute`, `POST /v2/farcaster/ban`, and `DELETE /v2/farcaster/ban` are registered but return `501 Not Implemented`. Submit signed `LinkAdd`/`LinkRemove` messages with `link_type = \"block\"` or `\"mute\"` via gRPC `SubmitMessage`.\n", "toc": [ { @@ -1242,7 +1242,7 @@ "sourcePath": "src/reference/reads/batch.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/reads/batch.md", "slug": "reference/reads/batch", - "contentHtml": "

Batch reads #

\n

POST-body hydration endpoints for when you need to enrich a list of FIDs with additional state in one call. All of these are unauthenticated and take the same basic request shape:

\n
{ \"fids\": [12345, 67890] }\n
\n

Responses are objects keyed by stringified FID.

\n
\n

POST /v2/farcaster/batch/following #

\n

For each FID in the request, return the list of FIDs they follow (truncated to a cap).

\n

Request

\n
{ \"fids\": [3, 5] }\n
\n

Response

\n
{\n  \"3\": [\n    { \"fid\": 5,   \"followed_at\": \"2024-01-15T09:12:00Z\" },\n    { \"fid\": 191, \"followed_at\": \"2024-02-02T13:40:00Z\" }\n  ],\n  \"5\": [\n    { \"fid\": 3, \"followed_at\": \"2023-11-02T18:01:00Z\" }\n  ]\n}\n
\n
\n
\n

POST /v2/farcaster/batch/reactions #

\n

For each FID, return their recent reactions (likes + recasts).

\n

Response

\n
{\n  \"3\": [\n    { \"target_fid\": 5, \"timestamp\": 1712345678 }\n  ]\n}\n
\n
\n
\n

POST /v2/farcaster/batch/cast-interactions #

\n

For each FID, return a list of cast hashes and whether the FID liked, recasted, or replied to each.

\n

Response

\n
{\n  \"3\": [\n    { \"hash\": \"0xabc...\", \"liked\": true, \"recasted\": false, \"replied\": true }\n  ]\n}\n
\n

Useful for rendering \"you liked this\" state across a feed in one call instead of one lookup per cast.

\n
\n
\n

POST /v2/farcaster/batch/signers #

\n

For each FID, return its registered signer set from the on-chain KeyRegistry.

\n

Response

\n
{\n  \"3\": [\n    {\n      \"signer\":    \"0x<ed25519-pubkey>\",\n      \"created_at\": 1712345678,\n      \"key_type\":   1,\n      \"metadata_type\": 1\n    }\n  ]\n}\n
\n
\n
\n

POST /v2/farcaster/batch/id-registrations #

\n

For each FID, return its IdRegistry history — registration, transfers, and the current owner.

\n

Response

\n
{\n  \"3\": [\n    {\n      \"from\":      \"0x0000...0000\",\n      \"to\":        \"0xabcd...abcd\",\n      \"timestamp\": 1700000000,\n      \"event_type\": \"register\"\n    }\n  ]\n}\n
\n
", + "contentHtml": "

Batch reads #

\n

POST-body hydration endpoints for when you need to enrich a list of FIDs with additional state in one call. All of these are unauthenticated and take the same basic request shape:

\n
{ \"fids\": [12345, 67890] }\n
\n

Responses are objects keyed by stringified FID.

\n
\n

POST /v2/farcaster/batch/following #

\n

For each FID in the request, return the list of FIDs they follow (truncated to a cap).

\n

Request

\n
{ \"fids\": [3, 5] }\n
\n

Response

\n
{\n  \"3\": [\n    { \"fid\": 5,   \"followed_at\": \"2024-01-15T09:12:00Z\" },\n    { \"fid\": 191, \"followed_at\": \"2024-02-02T13:40:00Z\" }\n  ],\n  \"5\": [\n    { \"fid\": 3, \"followed_at\": \"2023-11-02T18:01:00Z\" }\n  ]\n}\n
\n
\n
\n

POST /v2/farcaster/batch/reactions #

\n

For each FID, return their recent reactions (likes + recasts).

\n

Response

\n
{\n  \"3\": [\n    { \"target_fid\": 5, \"timestamp\": 1712345678 }\n  ]\n}\n
\n
\n
\n

POST /v2/farcaster/batch/cast-interactions #

\n

For each FID, return a list of cast hashes and whether the FID liked, recasted, or replied to each.

\n

Response

\n
{\n  \"3\": [\n    { \"hash\": \"0xabc...\", \"liked\": true, \"recasted\": false, \"replied\": true }\n  ]\n}\n
\n

Useful for rendering \"you liked this\" state across a feed in one call instead of one lookup per cast.

\n
\n
\n

POST /v2/farcaster/batch/signers #

\n

For each FID, return its registered signer set from the on-chain KeyRegistry.

\n

Response

\n
{\n  \"3\": [\n    {\n      \"signer\":    \"0x<ed25519-pubkey>\",\n      \"created_at\": 1712345678,\n      \"key_type\":   1,\n      \"metadata_type\": 1\n    }\n  ]\n}\n
\n
\n
\n

POST /v2/farcaster/batch/id-registrations #

\n

For each FID, return its IdRegistry history — registration, transfers, and the current owner.

\n

Response

\n
{\n  \"3\": [\n    {\n      \"from\":      \"0x0000...0000\",\n      \"to\":        \"0xabcd...abcd\",\n      \"timestamp\": 1700000000,\n      \"event_type\": \"register\"\n    }\n  ]\n}\n
\n
", "contentMarkdown": "# Batch reads\n\nPOST-body hydration endpoints for when you need to enrich a list of FIDs with additional state in one call. All of these are unauthenticated and take the same basic request shape:\n\n```json\n{ \"fids\": [12345, 67890] }\n```\n\nResponses are objects keyed by stringified FID.\n\n---\n\n## POST /v2/farcaster/batch/following\n\nFor each FID in the request, return the list of FIDs they follow (truncated to a cap).\n\n**Request**\n\n```json\n{ \"fids\": [3, 5] }\n```\n\n**Response**\n\n```json\n{\n \"3\": [\n { \"fid\": 5, \"followed_at\": \"2024-01-15T09:12:00Z\" },\n { \"fid\": 191, \"followed_at\": \"2024-02-02T13:40:00Z\" }\n ],\n \"5\": [\n { \"fid\": 3, \"followed_at\": \"2023-11-02T18:01:00Z\" }\n ]\n}\n```\n\n
\n\n---\n\n## POST /v2/farcaster/batch/reactions\n\nFor each FID, return their recent reactions (likes + recasts).\n\n**Response**\n\n```json\n{\n \"3\": [\n { \"target_fid\": 5, \"timestamp\": 1712345678 }\n ]\n}\n```\n\n
\n\n---\n\n## POST /v2/farcaster/batch/cast-interactions\n\nFor each FID, return a list of cast hashes and whether the FID liked, recasted, or replied to each.\n\n**Response**\n\n```json\n{\n \"3\": [\n { \"hash\": \"0xabc...\", \"liked\": true, \"recasted\": false, \"replied\": true }\n ]\n}\n```\n\nUseful for rendering \"you liked this\" state across a feed in one call instead of one lookup per cast.\n\n
\n\n---\n\n## POST /v2/farcaster/batch/signers\n\nFor each FID, return its registered signer set from the on-chain `KeyRegistry`.\n\n**Response**\n\n```json\n{\n \"3\": [\n {\n \"signer\": \"0x\",\n \"created_at\": 1712345678,\n \"key_type\": 1,\n \"metadata_type\": 1\n }\n ]\n}\n```\n\n
\n\n---\n\n## POST /v2/farcaster/batch/id-registrations\n\nFor each FID, return its `IdRegistry` history — registration, transfers, and the current owner.\n\n**Response**\n\n```json\n{\n \"3\": [\n {\n \"from\": \"0x0000...0000\",\n \"to\": \"0xabcd...abcd\",\n \"timestamp\": 1700000000,\n \"event_type\": \"register\"\n }\n ]\n}\n```\n\n
\n", "toc": [ { @@ -1309,7 +1309,7 @@ "sourcePath": "src/reference/webhooks/managing.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/webhooks/managing.md", "slug": "reference/webhooks/managing", - "contentHtml": "

Managing webhooks #

\n

All endpoints under /v2/farcaster/webhook/* require an EIP-712 signature in headers as described in Signed operations. The request body (if any) is included verbatim in the hash that gets signed.

\n

Max body: 256 KB.

\n
\n

POST /v2/farcaster/webhook/ — create #

\n

X-Hypersnap-Op: webhook.create

\n

Request body (CreateWebhookRequest):

\n
{\n  \"name\": \"my webhook\",\n  \"url\": \"https://receiver.example.com/hook\",\n  \"description\": \"optional free-form\",\n  \"subscription\": {\n    \"cast_created\": {\n      \"author_fids\": [3, 5],\n      \"mentioned_fids\": [],\n      \"text\": \"optional regex\",\n      \"embeds\": \"optional regex\",\n      \"exclude_author_fids\": []\n    }\n  }\n}\n
\n

The subscription must contain at least one event type. See Subscription filters for every available field and the size/regex constraints.

\n

The url is SSRF-checked at create time. By default, loopback and RFC1918 addresses are rejected so you can't register http://127.0.0.1 or internal-network targets unless your operator explicitly allows it.

\n

Response (WebhookResponse):

\n
{\n  \"webhook\": {\n    \"webhook_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"owner_fid\": 3,\n    \"target_url\": \"https://receiver.example.com/hook\",\n    \"title\": \"my webhook\",\n    \"description\": \"optional free-form\",\n    \"active\": true,\n    \"secrets\": [\n      {\n        \"uid\": \"...\",\n        \"value\": \"<64-char hex signing secret>\",\n        \"expires_at\": null,\n        \"created_at\": 1712345678\n      }\n    ],\n    \"subscription\": { ... },\n    \"http_timeout\": 10,\n    \"rate_limit\": 1000,\n    \"rate_limit_duration\": 60,\n    \"created_at\": 1712345678,\n    \"updated_at\": 1712345678\n  }\n}\n
\n

Save secrets[0].value now. This is the HMAC signing secret you'll use to verify deliveries. You can list the webhook again later, but the secret value in subsequent responses is the same — rotating it via secret rotation invalidates the old one on a grace window.

\n

Errors

\n\n
\n
\n

GET /v2/farcaster/webhook/ — lookup #

\n

X-Hypersnap-Op: webhook.read

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
webhook_idUUIDyes
\n

Response — same WebhookResponse shape as create.

\n

403 if the webhook belongs to a different FID; 404 if not found.

\n
\n
\n

GET /v2/farcaster/webhook/list — list #

\n

X-Hypersnap-Op: webhook.read

\n

Query — none.

\n

Response

\n
{ \"webhooks\": [ { /* webhook record */ } ] }\n
\n

Returns up to the per-owner cap. Only webhooks owned by the signing FID are included.

\n
\n
\n

PUT /v2/farcaster/webhook/ — update #

\n

X-Hypersnap-Op: webhook.update

\n

Request body (UpdateWebhookRequest — all fields except webhook_id are optional, only supplied fields change):

\n
{\n  \"webhook_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"name\":        \"optional new name\",\n  \"url\":         \"https://new-receiver.example.com/hook\",\n  \"description\": \"optional new description\",\n  \"subscription\": { /* full replacement if supplied */ },\n  \"active\":      true\n}\n
\n

If you set active: false, the webhook stays registered but Hypersnap stops dispatching events to it — useful for pausing a receiver for maintenance without losing the filter config.

\n

Response — updated WebhookResponse. 403 if not owner.

\n
\n
\n

DELETE /v2/farcaster/webhook/ — delete #

\n

X-Hypersnap-Op: webhook.delete

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
webhook_idUUIDyes
\n

Response

\n
{ \"deleted\": true }\n
\n

Soft-deletes the record. Retries queued for in-flight events stop dispatching. 403 if not owner.

\n
\n
\n

POST /v2/farcaster/webhook/secret/rotate — rotate secret #

\n

X-Hypersnap-Op: webhook.rotate_secret

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
webhook_idUUIDyes
\n

Response — the full WebhookResponse. The secrets array now has one additional entry (the newest) and the previously-active secrets have expires_at set to now + secret_grace_period_secs (default 24h).

\n

How receivers should handle rotation

\n
    \n
  1. Call secret/rotate.
  2. \n
  3. Read the new secret from secrets[-1].value — this is what new deliveries will sign with.
  4. \n
  5. Your receiver should accept any currently-valid secret when verifying. For the duration of the grace window, deliveries might be signed with either the old or the new key depending on timing. Maintain a set of accepted secrets and drop the old one when its expires_at passes.
  6. \n
\n
", + "contentHtml": "

Managing webhooks #

\n

All endpoints under /v2/farcaster/webhook/* require an EIP-712 signature in headers as described in Signed operations. The request body (if any) is included verbatim in the hash that gets signed.

\n

Max body: 256 KB.

\n
\n

POST /v2/farcaster/webhook/ — create #

\n

X-Hypersnap-Op: webhook.create

\n

Request body (CreateWebhookRequest):

\n
{\n  \"name\": \"my webhook\",\n  \"url\": \"https://receiver.example.com/hook\",\n  \"description\": \"optional free-form\",\n  \"subscription\": {\n    \"cast_created\": {\n      \"author_fids\": [3, 5],\n      \"mentioned_fids\": [],\n      \"text\": \"optional regex\",\n      \"embeds\": \"optional regex\",\n      \"exclude_author_fids\": []\n    }\n  }\n}\n
\n

The subscription must contain at least one event type. See Subscription filters for every available field and the size/regex constraints.

\n

The url is SSRF-checked at create time. By default, loopback and RFC1918 addresses are rejected so you can't register http://127.0.0.1 or internal-network targets unless your operator explicitly allows it.

\n

Response (WebhookResponse):

\n
{\n  \"webhook\": {\n    \"webhook_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"owner_fid\": 3,\n    \"target_url\": \"https://receiver.example.com/hook\",\n    \"title\": \"my webhook\",\n    \"description\": \"optional free-form\",\n    \"active\": true,\n    \"secrets\": [\n      {\n        \"uid\": \"...\",\n        \"value\": \"<64-char hex signing secret>\",\n        \"expires_at\": null,\n        \"created_at\": 1712345678\n      }\n    ],\n    \"subscription\": { ... },\n    \"http_timeout\": 10,\n    \"rate_limit\": 1000,\n    \"rate_limit_duration\": 60,\n    \"created_at\": 1712345678,\n    \"updated_at\": 1712345678\n  }\n}\n
\n

Save secrets[0].value now. This is the HMAC signing secret you'll use to verify deliveries. You can list the webhook again later, but the secret value in subsequent responses is the same — rotating it via secret rotation invalidates the old one on a grace window.

\n

Errors

\n\n
\n
\n

GET /v2/farcaster/webhook/ — lookup #

\n

X-Hypersnap-Op: webhook.read

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
webhook_idUUIDyes
\n

Response — same WebhookResponse shape as create.

\n

403 if the webhook belongs to a different FID; 404 if not found.

\n
\n
\n

GET /v2/farcaster/webhook/list — list #

\n

X-Hypersnap-Op: webhook.read

\n

Query — none.

\n

Response

\n
{ \"webhooks\": [ { /* webhook record */ } ] }\n
\n

Returns up to the per-owner cap. Only webhooks owned by the signing FID are included.

\n
\n
\n

PUT /v2/farcaster/webhook/ — update #

\n

X-Hypersnap-Op: webhook.update

\n

Request body (UpdateWebhookRequest — all fields except webhook_id are optional, only supplied fields change):

\n
{\n  \"webhook_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"name\":        \"optional new name\",\n  \"url\":         \"https://new-receiver.example.com/hook\",\n  \"description\": \"optional new description\",\n  \"subscription\": { /* full replacement if supplied */ },\n  \"active\":      true\n}\n
\n

If you set active: false, the webhook stays registered but Hypersnap stops dispatching events to it — useful for pausing a receiver for maintenance without losing the filter config.

\n

Response — updated WebhookResponse. 403 if not owner.

\n
\n
\n

DELETE /v2/farcaster/webhook/ — delete #

\n

X-Hypersnap-Op: webhook.delete

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
webhook_idUUIDyes
\n

Response

\n
{ \"deleted\": true }\n
\n

Soft-deletes the record. Retries queued for in-flight events stop dispatching. 403 if not owner.

\n
\n
\n

POST /v2/farcaster/webhook/secret/rotate — rotate secret #

\n

X-Hypersnap-Op: webhook.rotate_secret

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
webhook_idUUIDyes
\n

Response — the full WebhookResponse. The secrets array now has one additional entry (the newest) and the previously-active secrets have expires_at set to now + secret_grace_period_secs (default 24h).

\n

How receivers should handle rotation

\n
    \n
  1. Call secret/rotate.
  2. \n
  3. Read the new secret from secrets[-1].value — this is what new deliveries will sign with.
  4. \n
  5. Your receiver should accept any currently-valid secret when verifying. For the duration of the grace window, deliveries might be signed with either the old or the new key depending on timing. Maintain a set of accepted secrets and drop the old one when its expires_at passes.
  6. \n
\n
", "contentMarkdown": "# Managing webhooks\n\nAll endpoints under `/v2/farcaster/webhook/*` require an EIP-712 signature in headers as described in [Signed operations](../../concepts/authentication.md). The request body (if any) is included verbatim in the hash that gets signed.\n\nMax body: **256 KB**.\n\n---\n\n## POST /v2/farcaster/webhook/ — create\n\n`X-Hypersnap-Op`: `webhook.create`\n\n**Request body** (`CreateWebhookRequest`):\n\n```json\n{\n \"name\": \"my webhook\",\n \"url\": \"https://receiver.example.com/hook\",\n \"description\": \"optional free-form\",\n \"subscription\": {\n \"cast_created\": {\n \"author_fids\": [3, 5],\n \"mentioned_fids\": [],\n \"text\": \"optional regex\",\n \"embeds\": \"optional regex\",\n \"exclude_author_fids\": []\n }\n }\n}\n```\n\nThe subscription must contain at least one event type. See [Subscription filters](./filters.md) for every available field and the size/regex constraints.\n\nThe `url` is SSRF-checked at create time. By default, loopback and RFC1918 addresses are rejected so you can't register `http://127.0.0.1` or internal-network targets unless your operator explicitly allows it.\n\n**Response** (`WebhookResponse`):\n\n```json\n{\n \"webhook\": {\n \"webhook_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"owner_fid\": 3,\n \"target_url\": \"https://receiver.example.com/hook\",\n \"title\": \"my webhook\",\n \"description\": \"optional free-form\",\n \"active\": true,\n \"secrets\": [\n {\n \"uid\": \"...\",\n \"value\": \"<64-char hex signing secret>\",\n \"expires_at\": null,\n \"created_at\": 1712345678\n }\n ],\n \"subscription\": { ... },\n \"http_timeout\": 10,\n \"rate_limit\": 1000,\n \"rate_limit_duration\": 60,\n \"created_at\": 1712345678,\n \"updated_at\": 1712345678\n }\n}\n```\n\n**Save `secrets[0].value` now.** This is the HMAC signing secret you'll use to verify deliveries. You can list the webhook again later, but the secret value in subsequent responses is the same — rotating it via [secret rotation](#post-v2farcasterwebhooksecretrotate--rotate-secret) invalidates the old one on a grace window.\n\n**Errors**\n\n- `400` — invalid JSON, empty subscription, filter too large, invalid regex, SSRF-blocked URL.\n- `401` — signature / clock / nonce / custody mismatch.\n- `429` — per-FID cap reached.\n\n
\n\n---\n\n## GET /v2/farcaster/webhook/ — lookup\n\n`X-Hypersnap-Op`: `webhook.read`\n\n**Query**\n\n| Name | Type | Required |\n|---|---|---|\n| `webhook_id` | UUID | yes |\n\n**Response** — same `WebhookResponse` shape as create.\n\n`403` if the webhook belongs to a different FID; `404` if not found.\n\n
\n\n---\n\n## GET /v2/farcaster/webhook/list — list\n\n`X-Hypersnap-Op`: `webhook.read`\n\n**Query** — none.\n\n**Response**\n\n```json\n{ \"webhooks\": [ { /* webhook record */ } ] }\n```\n\nReturns up to the per-owner cap. Only webhooks owned by the signing FID are included.\n\n
\n\n---\n\n## PUT /v2/farcaster/webhook/ — update\n\n`X-Hypersnap-Op`: `webhook.update`\n\n**Request body** (`UpdateWebhookRequest` — all fields except `webhook_id` are optional, only supplied fields change):\n\n```json\n{\n \"webhook_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"optional new name\",\n \"url\": \"https://new-receiver.example.com/hook\",\n \"description\": \"optional new description\",\n \"subscription\": { /* full replacement if supplied */ },\n \"active\": true\n}\n```\n\nIf you set `active: false`, the webhook stays registered but Hypersnap stops dispatching events to it — useful for pausing a receiver for maintenance without losing the filter config.\n\n**Response** — updated `WebhookResponse`. `403` if not owner.\n\n
\n\n---\n\n## DELETE /v2/farcaster/webhook/ — delete\n\n`X-Hypersnap-Op`: `webhook.delete`\n\n**Query**\n\n| Name | Type | Required |\n|---|---|---|\n| `webhook_id` | UUID | yes |\n\n**Response**\n\n```json\n{ \"deleted\": true }\n```\n\nSoft-deletes the record. Retries queued for in-flight events stop dispatching. `403` if not owner.\n\n
\n\n---\n\n## POST /v2/farcaster/webhook/secret/rotate — rotate secret\n\n`X-Hypersnap-Op`: `webhook.rotate_secret`\n\n**Query**\n\n| Name | Type | Required |\n|---|---|---|\n| `webhook_id` | UUID | yes |\n\n**Response** — the full `WebhookResponse`. The `secrets` array now has one additional entry (the newest) and the previously-active secrets have `expires_at` set to `now + secret_grace_period_secs` (default 24h).\n\n**How receivers should handle rotation**\n\n1. Call `secret/rotate`.\n2. Read the new secret from `secrets[-1].value` — this is what new deliveries will sign with.\n3. Your receiver should accept *any* currently-valid secret when verifying. For the duration of the grace window, deliveries might be signed with either the old or the new key depending on timing. Maintain a set of accepted secrets and drop the old one when its `expires_at` passes.\n\n
\n", "toc": [ { @@ -1350,7 +1350,7 @@ "sourcePath": "src/reference/webhooks/filters.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/webhooks/filters.md", "slug": "reference/webhooks/filters", - "contentHtml": "

Subscription filters #

\n

A WebhookSubscription object selects which Farcaster events you want delivered. The shape mirrors common Farcaster v2 contracts so existing filters port verbatim.

\n

Top-level shape #

\n
{\n  \"cast_created\":    { /* CastFilter */ },\n  \"cast_deleted\":    { /* CastFilter */ },\n  \"user_created\":    { },\n  \"user_updated\":    { \"fids\": [12345] },\n  \"follow_created\":  { \"fids\": [], \"target_fids\": [] },\n  \"follow_deleted\":  { \"fids\": [], \"target_fids\": [] },\n  \"reaction_created\":{ \"fids\": [], \"target_fids\": [], \"target_cast_hashes\": [] },\n  \"reaction_deleted\":{ \"fids\": [], \"target_fids\": [], \"target_cast_hashes\": [] }\n}\n
\n

At least one event must be present. Creating a webhook with an empty subscription returns 400.

\n

Any filter object can be omitted or included as {} to subscribe to everything of that event type.

\n

CastFilter #

\n

Used for both cast_created and cast_deleted.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
author_fidsVec<u64>Cast author FID is in this set.
exclude_author_fidsVec<u64>Cast author FID is not in this set.
mentioned_fidsVec<u64>Cast mentions any FID in this set.
parent_urlsVec<String>Cast is a reply under one of these parent URLs (channel scoping).
root_parent_urlsVec<String>Root-of-thread parent URL matches. (Accepted at create time; not enforced at dispatch.)
parent_hashesVec<String>Cast is a reply to a cast with one of these hashes.
parent_author_fidsVec<u64>Cast is a reply to a cast authored by one of these FIDs.
textOption<String>Regex. If set, cast text must match. Compiled at create time with the linear-time regex crate; lookaround is rejected.
embedsOption<String>Regex. Applied against embed URLs. (Accepted at create time; not enforced at dispatch.)
embedded_cast_author_fidsVec<u64>(Accepted; not enforced at dispatch.)
embedded_cast_hashesVec<String>(Accepted; not enforced at dispatch.)
\n

All supplied fields AND together. If you set both author_fids and mentioned_fids, the cast must match both. Arrays OR within themselves — author_fids: [3, 5] matches a cast by FID 3 or 5.

\n

An empty array means \"no restriction on this field\", not \"nothing matches\".

\n

FollowFilter #

\n

Used for both follow_created and follow_deleted.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
fidsVec<u64>The follower's FID is in this set.
target_fidsVec<u64>The target (the one being followed) is in this set.
\n

ReactionFilter #

\n

Used for both reaction_created and reaction_deleted.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
fidsVec<u64>The reactor's FID is in this set.
target_fidsVec<u64>The author of the cast being reacted to is in this set.
target_cast_hashesVec<String>The specific cast hash(es) being reacted to.
\n

UserUpdatedFilter #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
fidsVec<u64>Only match updates to these FIDs.
\n

UserCreatedFilter #

\n

No filter fields — fires for every new FID registration seen on-chain.

\n

Size caps #

\n\n

Fields accepted but not enforced at dispatch #

\n

A handful of filter fields (root_parent_urls, embeds regex, embedded_cast_*) are parsed and validated at create time but not used to filter events when they're dispatched. They require cross-message lookups that aren't on the dispatcher's hot path today. Setting them is harmless — events still fire — but they won't narrow your stream. If you need a tighter filter than the enforced fields can express, filter in your receiver and ignore the overdelivered events.

\n

Examples #

\n

Every new cast network-wide #

\n
{ \"cast_created\": {} }\n
\n

Casts authored by a specific FID #

\n
{ \"cast_created\": { \"author_fids\": [3] } }\n
\n

Casts that mention your FID OR are replies to you #

\n
{\n  \"cast_created\": {\n    \"mentioned_fids\": [12345],\n    \"parent_author_fids\": [12345]\n  }\n}\n
\n

Caveat: that filter uses AND semantics across fields, so it matches casts that both mention and reply to you. To OR the two conditions, register two webhooks (or two subscriptions on the same webhook) and dedupe on the receiver side.

\n

Likes on your casts #

\n
{ \"reaction_created\": { \"target_fids\": [12345] } }\n
\n

New follows where you are the target #

\n
{ \"follow_created\": { \"target_fids\": [12345] } }\n
\n

Keyword filter #

\n
{ \"cast_created\": { \"text\": \"(?i)\\\\b(hypersnap|farcaster)\\\\b\" } }\n
\n

(?i) is supported; (?=...) / (?!...) lookaround is not.

", + "contentHtml": "

Subscription filters #

\n

A WebhookSubscription object selects which Farcaster events you want delivered. The shape mirrors common Farcaster v2 contracts so existing filters port verbatim.

\n

Top-level shape #

\n
{\n  \"cast_created\":    { /* CastFilter */ },\n  \"cast_deleted\":    { /* CastFilter */ },\n  \"user_created\":    { },\n  \"user_updated\":    { \"fids\": [12345] },\n  \"follow_created\":  { \"fids\": [], \"target_fids\": [] },\n  \"follow_deleted\":  { \"fids\": [], \"target_fids\": [] },\n  \"reaction_created\":{ \"fids\": [], \"target_fids\": [], \"target_cast_hashes\": [] },\n  \"reaction_deleted\":{ \"fids\": [], \"target_fids\": [], \"target_cast_hashes\": [] }\n}\n
\n

At least one event must be present. Creating a webhook with an empty subscription returns 400.

\n

Any filter object can be omitted or included as {} to subscribe to everything of that event type.

\n

CastFilter #

\n

Used for both cast_created and cast_deleted.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
author_fidsVec<u64>Cast author FID is in this set.
exclude_author_fidsVec<u64>Cast author FID is not in this set.
mentioned_fidsVec<u64>Cast mentions any FID in this set.
parent_urlsVec<String>Cast is a reply under one of these parent URLs (channel scoping).
root_parent_urlsVec<String>Root-of-thread parent URL matches. (Accepted at create time; not enforced at dispatch.)
parent_hashesVec<String>Cast is a reply to a cast with one of these hashes.
parent_author_fidsVec<u64>Cast is a reply to a cast authored by one of these FIDs.
textOption<String>Regex. If set, cast text must match. Compiled at create time with the linear-time regex crate; lookaround is rejected.
embedsOption<String>Regex. Applied against embed URLs. (Accepted at create time; not enforced at dispatch.)
embedded_cast_author_fidsVec<u64>(Accepted; not enforced at dispatch.)
embedded_cast_hashesVec<String>(Accepted; not enforced at dispatch.)
\n

All supplied fields AND together. If you set both author_fids and mentioned_fids, the cast must match both. Arrays OR within themselves — author_fids: [3, 5] matches a cast by FID 3 or 5.

\n

An empty array means \"no restriction on this field\", not \"nothing matches\".

\n

FollowFilter #

\n

Used for both follow_created and follow_deleted.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
fidsVec<u64>The follower's FID is in this set.
target_fidsVec<u64>The target (the one being followed) is in this set.
\n

ReactionFilter #

\n

Used for both reaction_created and reaction_deleted.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
fidsVec<u64>The reactor's FID is in this set.
target_fidsVec<u64>The author of the cast being reacted to is in this set.
target_cast_hashesVec<String>The specific cast hash(es) being reacted to.
\n

UserUpdatedFilter #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeMeaning
fidsVec<u64>Only match updates to these FIDs.
\n

UserCreatedFilter #

\n

No filter fields — fires for every new FID registration seen on-chain.

\n

Size caps #

\n\n

Fields accepted but not enforced at dispatch #

\n

A handful of filter fields (root_parent_urls, embeds regex, embedded_cast_*) are parsed and validated at create time but not used to filter events when they're dispatched. They require cross-message lookups that aren't on the dispatcher's hot path today. Setting them is harmless — events still fire — but they won't narrow your stream. If you need a tighter filter than the enforced fields can express, filter in your receiver and ignore the overdelivered events.

\n

Examples #

\n

Every new cast network-wide #

\n
{ \"cast_created\": {} }\n
\n

Casts authored by a specific FID #

\n
{ \"cast_created\": { \"author_fids\": [3] } }\n
\n

Casts that mention your FID OR are replies to you #

\n
{\n  \"cast_created\": {\n    \"mentioned_fids\": [12345],\n    \"parent_author_fids\": [12345]\n  }\n}\n
\n

Caveat: that filter uses AND semantics across fields, so it matches casts that both mention and reply to you. To OR the two conditions, register two webhooks (or two subscriptions on the same webhook) and dedupe on the receiver side.

\n

Likes on your casts #

\n
{ \"reaction_created\": { \"target_fids\": [12345] } }\n
\n

New follows where you are the target #

\n
{ \"follow_created\": { \"target_fids\": [12345] } }\n
\n

Keyword filter #

\n
{ \"cast_created\": { \"text\": \"(?i)\\\\b(hypersnap|farcaster)\\\\b\" } }\n
\n

(?i) is supported; (?=...) / (?!...) lookaround is not.

", "contentMarkdown": "# Subscription filters\n\nA `WebhookSubscription` object selects which Farcaster events you want delivered. The shape mirrors common Farcaster v2 contracts so existing filters port verbatim.\n\n## Top-level shape\n\n```json\n{\n \"cast_created\": { /* CastFilter */ },\n \"cast_deleted\": { /* CastFilter */ },\n \"user_created\": { },\n \"user_updated\": { \"fids\": [12345] },\n \"follow_created\": { \"fids\": [], \"target_fids\": [] },\n \"follow_deleted\": { \"fids\": [], \"target_fids\": [] },\n \"reaction_created\":{ \"fids\": [], \"target_fids\": [], \"target_cast_hashes\": [] },\n \"reaction_deleted\":{ \"fids\": [], \"target_fids\": [], \"target_cast_hashes\": [] }\n}\n```\n\n**At least one event must be present.** Creating a webhook with an empty subscription returns `400`.\n\nAny filter object can be omitted or included as `{}` to subscribe to everything of that event type.\n\n## CastFilter\n\nUsed for both `cast_created` and `cast_deleted`.\n\n| Field | Type | Meaning |\n|---|---|---|\n| `author_fids` | `Vec` | Cast author FID is in this set. |\n| `exclude_author_fids` | `Vec` | Cast author FID is **not** in this set. |\n| `mentioned_fids` | `Vec` | Cast mentions any FID in this set. |\n| `parent_urls` | `Vec` | Cast is a reply under one of these parent URLs (channel scoping). |\n| `root_parent_urls` | `Vec` | Root-of-thread parent URL matches. (Accepted at create time; not enforced at dispatch.) |\n| `parent_hashes` | `Vec` | Cast is a reply to a cast with one of these hashes. |\n| `parent_author_fids` | `Vec` | Cast is a reply to a cast authored by one of these FIDs. |\n| `text` | `Option` | Regex. If set, cast `text` must match. Compiled at create time with the linear-time `regex` crate; lookaround is rejected. |\n| `embeds` | `Option` | Regex. Applied against embed URLs. (Accepted at create time; not enforced at dispatch.) |\n| `embedded_cast_author_fids` | `Vec` | (Accepted; not enforced at dispatch.) |\n| `embedded_cast_hashes` | `Vec` | (Accepted; not enforced at dispatch.) |\n\n**All supplied fields AND together.** If you set both `author_fids` and `mentioned_fids`, the cast must match both. Arrays OR within themselves — `author_fids: [3, 5]` matches a cast by FID 3 *or* 5.\n\nAn empty array means \"no restriction on this field\", not \"nothing matches\".\n\n## FollowFilter\n\nUsed for both `follow_created` and `follow_deleted`.\n\n| Field | Type | Meaning |\n|---|---|---|\n| `fids` | `Vec` | The follower's FID is in this set. |\n| `target_fids` | `Vec` | The target (the one being followed) is in this set. |\n\n## ReactionFilter\n\nUsed for both `reaction_created` and `reaction_deleted`.\n\n| Field | Type | Meaning |\n|---|---|---|\n| `fids` | `Vec` | The reactor's FID is in this set. |\n| `target_fids` | `Vec` | The author of the cast being reacted to is in this set. |\n| `target_cast_hashes` | `Vec` | The specific cast hash(es) being reacted to. |\n\n## UserUpdatedFilter\n\n| Field | Type | Meaning |\n|---|---|---|\n| `fids` | `Vec` | Only match updates to these FIDs. |\n\n## UserCreatedFilter\n\nNo filter fields — fires for every new FID registration seen on-chain.\n\n## Size caps\n\n- Every array field is capped at **1024** entries. Exceeding the cap rejects the create/update with `400`.\n- Regex patterns (`text`, `embeds`) must compile under Rust's `regex` crate (linear time, no backtracking). Lookaround is rejected. Alternation, character classes, and quantifiers all work.\n\n## Fields accepted but not enforced at dispatch\n\nA handful of filter fields (`root_parent_urls`, `embeds` regex, `embedded_cast_*`) are parsed and validated at create time but not used to filter events when they're dispatched. They require cross-message lookups that aren't on the dispatcher's hot path today. Setting them is harmless — events still fire — but they won't narrow your stream. If you need a tighter filter than the enforced fields can express, filter in your receiver and ignore the overdelivered events.\n\n## Examples\n\n### Every new cast network-wide\n\n```json\n{ \"cast_created\": {} }\n```\n\n### Casts authored by a specific FID\n\n```json\n{ \"cast_created\": { \"author_fids\": [3] } }\n```\n\n### Casts that mention your FID OR are replies to you\n\n```json\n{\n \"cast_created\": {\n \"mentioned_fids\": [12345],\n \"parent_author_fids\": [12345]\n }\n}\n```\n\n**Caveat:** that filter uses AND semantics across fields, so it matches casts that both mention *and* reply to you. To OR the two conditions, register two webhooks (or two subscriptions on the same webhook) and dedupe on the receiver side.\n\n### Likes on your casts\n\n```json\n{ \"reaction_created\": { \"target_fids\": [12345] } }\n```\n\n### New follows where you are the target\n\n```json\n{ \"follow_created\": { \"target_fids\": [12345] } }\n```\n\n### Keyword filter\n\n```json\n{ \"cast_created\": { \"text\": \"(?i)\\\\b(hypersnap|farcaster)\\\\b\" } }\n```\n\n`(?i)` is supported; `(?=...)` / `(?!...)` lookaround is not.\n", "toc": [ { @@ -1436,7 +1436,7 @@ "sourcePath": "src/reference/webhooks/delivery.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/webhooks/delivery.md", "slug": "reference/webhooks/delivery", - "contentHtml": "

Delivery contract #

\n

When an event matches a webhook's subscription, Hypersnap POSTs a JSON envelope to the webhook's target_url. This page describes exactly what your receiver should expect and how to verify it.

\n

Request shape #

\n

Method: POST

\n

Headers

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
HeaderValue
Content-Typeapplication/json
X-Hypersnap-Signature<hex(hmac_sha512(secret, raw_body))> (header name is operator-configurable; this is the default)
\n

Body

\n
{\n  \"created_at\": 1712345678,\n  \"type\": \"cast.created\",\n  \"data\": { /* event payload — see Event schemas */ }\n}\n
\n

The type field tells you which event schema applies. The data field carries the event payload — see Event schemas for the per-type shapes.

\n

Verifying the signature #

\n

The signature is hmac_sha512(secret, raw_body), hex-encoded. raw_body is the literal bytes of the HTTP body, not a re-serialized representation — verify before JSON-parsing.

\n

Node.js #

\n
import crypto from \"crypto\";\n\nfunction verify(req, secret) {\n  const received = req.headers[\"x-hypersnap-signature\"];\n  const expected = crypto\n    .createHmac(\"sha512\", secret)\n    .update(req.rawBody) // the literal bytes, NOT JSON.stringify(req.body)\n    .digest(\"hex\");\n  return crypto.timingSafeEqual(\n    Buffer.from(received, \"hex\"),\n    Buffer.from(expected, \"hex\"),\n  );\n}\n
\n

Python #

\n
import hmac, hashlib\n\ndef verify(raw_body: bytes, received_hex: str, secret: str) -> bool:\n    expected = hmac.new(\n        secret.encode(), raw_body, hashlib.sha512\n    ).hexdigest()\n    return hmac.compare_digest(expected, received_hex)\n
\n

Rust #

\n
use hmac::{Hmac, Mac};\nuse sha2::Sha512;\n\nfn verify(raw_body: &[u8], received_hex: &str, secret: &str) -> bool {\n    let mut mac = Hmac::<Sha512>::new_from_slice(secret.as_bytes()).unwrap();\n    mac.update(raw_body);\n    let expected = hex::encode(mac.finalize().into_bytes());\n    // subtle::ConstantTimeEq in production.\n    expected == received_hex\n}\n
\n

Which secret signs a delivery #

\n

The secret used for a given delivery is the most recent non-expired entry in the webhook's secrets array. During a rotation grace window you may see two valid secrets in the list at once. Your receiver should:

\n
    \n
  1. Keep a set of accepted secrets, not just one.
  2. \n
  3. Try each in turn until one verifies.
  4. \n
  5. Drop a secret from the accepted set after its expires_at passes.
  6. \n
\n

This way a secret rotation never loses a delivery.

\n

Response codes Hypersnap expects #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Your responseHypersnap's interpretation
2xxDelivered. Counted as webhooks.delivery.succeeded.
4xxTerminal failure. No retries. Counted as webhooks.delivery.failed_4xx. Your webhook is responsible for fixing whatever's wrong — a 4xx usually means a bug in the config (wrong URL path, auth) or a permanent rejection.
5xxTransient failure. Retried with exponential backoff up to retry_max_attempts. Counted as webhooks.delivery.failed_5xx.
Timeout / network errorTransient failure. Retried. Counted as webhooks.delivery.failed_network.
\n

Default timeout on a single attempt: 10 seconds (delivery_timeout_secs).

\n

Default retry schedule: 5 attempts total, starting at 500ms and doubling (retry_initial_backoff_ms × 2^attempt). Failures in between live on a persistent RocksDB-backed retry queue so they survive restarts.

\n

Rate limiting #

\n

Each webhook has a rate limit enforced by the dispatcher:

\n
    \n
  • Default: 1000 events per 60 seconds, per webhook.
  • \n
  • Events above the limit are dropped, not queued. Dropped events are counted in the webhooks.delivery.rate_limited metric.
  • \n
\n

If you expect high-volume streams, narrow your subscription filters so fewer events match, or ask your operator to raise the per-webhook limit.

\n

Delivery guarantees #

\n
    \n
  • At-least-once. Transient failures + retries can replay the same (event, attempt) pair. Dedupe on the data field's natural key (cast hash, reaction unique tuple, etc.).
  • \n
  • Not strictly ordered. Two events for the same cast can arrive in either order if one is retried while the other succeeds first pass. If ordering matters, attach server timestamps from the event payload.
  • \n
  • Transient failure ≠ lost. 5xx/network failures go on the retry queue. You get them eventually.
  • \n
  • 4xx = lost. If you reply 4xx, the event is permanently dropped. Reserve 4xx for cases where retrying would be pointless.
  • \n
\n

Receiver checklist #

\n
    \n
  • Verify every delivery's HMAC before trusting the body.
  • \n
  • Accept multiple active secrets during rotation grace windows.
  • \n
  • Return 2xx as soon as you've durably enqueued the event; don't hold the connection open for downstream work.
  • \n
  • Dedupe on a natural key (cast hash, reaction tuple).
  • \n
  • Return 5xx on transient failures so Hypersnap can retry.
  • \n
  • Reject with 4xx only when there's nothing to be done.
  • \n
  • Keep your receiver's p95 latency under delivery_timeout_secs.
  • \n
", + "contentHtml": "

Delivery contract #

\n

When an event matches a webhook's subscription, Hypersnap POSTs a JSON envelope to the webhook's target_url. This page describes exactly what your receiver should expect and how to verify it.

\n

Request shape #

\n

Method: POST

\n

Headers

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
HeaderValue
Content-Typeapplication/json
X-Hypersnap-Signature<hex(hmac_sha512(secret, raw_body))> (header name is operator-configurable; this is the default)
\n

Body

\n
{\n  \"created_at\": 1712345678,\n  \"type\": \"cast.created\",\n  \"data\": { /* event payload — see Event schemas */ }\n}\n
\n

The type field tells you which event schema applies. The data field carries the event payload — see Event schemas for the per-type shapes.

\n

Verifying the signature #

\n

The signature is hmac_sha512(secret, raw_body), hex-encoded. raw_body is the literal bytes of the HTTP body, not a re-serialized representation — verify before JSON-parsing.

\n

Node.js #

\n
import crypto from \"crypto\";\n\nfunction verify(req, secret) {\n  const received = req.headers[\"x-hypersnap-signature\"];\n  const expected = crypto\n    .createHmac(\"sha512\", secret)\n    .update(req.rawBody) // the literal bytes, NOT JSON.stringify(req.body)\n    .digest(\"hex\");\n  return crypto.timingSafeEqual(\n    Buffer.from(received, \"hex\"),\n    Buffer.from(expected, \"hex\"),\n  );\n}\n
\n

Python #

\n
import hmac, hashlib\n\ndef verify(raw_body: bytes, received_hex: str, secret: str) -> bool:\n    expected = hmac.new(\n        secret.encode(), raw_body, hashlib.sha512\n    ).hexdigest()\n    return hmac.compare_digest(expected, received_hex)\n
\n

Rust #

\n
use hmac::{Hmac, Mac};\nuse sha2::Sha512;\n\nfn verify(raw_body: &[u8], received_hex: &str, secret: &str) -> bool {\n    let mut mac = Hmac::<Sha512>::new_from_slice(secret.as_bytes()).unwrap();\n    mac.update(raw_body);\n    let expected = hex::encode(mac.finalize().into_bytes());\n    // subtle::ConstantTimeEq in production.\n    expected == received_hex\n}\n
\n

Which secret signs a delivery #

\n

The secret used for a given delivery is the most recent non-expired entry in the webhook's secrets array. During a rotation grace window you may see two valid secrets in the list at once. Your receiver should:

\n
    \n
  1. Keep a set of accepted secrets, not just one.
  2. \n
  3. Try each in turn until one verifies.
  4. \n
  5. Drop a secret from the accepted set after its expires_at passes.
  6. \n
\n

This way a secret rotation never loses a delivery.

\n

Response codes Hypersnap expects #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Your responseHypersnap's interpretation
2xxDelivered. Counted as webhooks.delivery.succeeded.
4xxTerminal failure. No retries. Counted as webhooks.delivery.failed_4xx. Your webhook is responsible for fixing whatever's wrong — a 4xx usually means a bug in the config (wrong URL path, auth) or a permanent rejection.
5xxTransient failure. Retried with exponential backoff up to retry_max_attempts. Counted as webhooks.delivery.failed_5xx.
Timeout / network errorTransient failure. Retried. Counted as webhooks.delivery.failed_network.
\n

Default timeout on a single attempt: 10 seconds (delivery_timeout_secs).

\n

Default retry schedule: 5 attempts total, starting at 500ms and doubling (retry_initial_backoff_ms × 2^attempt). Failures in between live on a persistent RocksDB-backed retry queue so they survive restarts.

\n

Rate limiting #

\n

Each webhook has a rate limit enforced by the dispatcher:

\n
    \n
  • Default: 1000 events per 60 seconds, per webhook.
  • \n
  • Events above the limit are dropped, not queued. Dropped events are counted in the webhooks.delivery.rate_limited metric.
  • \n
\n

If you expect high-volume streams, narrow your subscription filters so fewer events match, or ask your operator to raise the per-webhook limit.

\n

Delivery guarantees #

\n
    \n
  • At-least-once. Transient failures + retries can replay the same (event, attempt) pair. Dedupe on the data field's natural key (cast hash, reaction unique tuple, etc.).
  • \n
  • Not strictly ordered. Two events for the same cast can arrive in either order if one is retried while the other succeeds first pass. If ordering matters, attach server timestamps from the event payload.
  • \n
  • Transient failure ≠ lost. 5xx/network failures go on the retry queue. You get them eventually.
  • \n
  • 4xx = lost. If you reply 4xx, the event is permanently dropped. Reserve 4xx for cases where retrying would be pointless.
  • \n
\n

Receiver checklist #

\n
    \n
  • Verify every delivery's HMAC before trusting the body.
  • \n
  • Accept multiple active secrets during rotation grace windows.
  • \n
  • Return 2xx as soon as you've durably enqueued the event; don't hold the connection open for downstream work.
  • \n
  • Dedupe on a natural key (cast hash, reaction tuple).
  • \n
  • Return 5xx on transient failures so Hypersnap can retry.
  • \n
  • Reject with 4xx only when there's nothing to be done.
  • \n
  • Keep your receiver's p95 latency under delivery_timeout_secs.
  • \n
", "contentMarkdown": "# Delivery contract\n\nWhen an event matches a webhook's subscription, Hypersnap POSTs a JSON envelope to the webhook's `target_url`. This page describes exactly what your receiver should expect and how to verify it.\n\n## Request shape\n\n**Method:** `POST`\n\n**Headers**\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `X-Hypersnap-Signature` | `` (header name is operator-configurable; this is the default) |\n\n**Body**\n\n```json\n{\n \"created_at\": 1712345678,\n \"type\": \"cast.created\",\n \"data\": { /* event payload — see Event schemas */ }\n}\n```\n\nThe `type` field tells you which event schema applies. The `data` field carries the event payload — see [Event schemas](./events.md) for the per-type shapes.\n\n## Verifying the signature\n\nThe signature is `hmac_sha512(secret, raw_body)`, hex-encoded. `raw_body` is the *literal bytes* of the HTTP body, not a re-serialized representation — verify before JSON-parsing.\n\n### Node.js\n\n```javascript\nimport crypto from \"crypto\";\n\nfunction verify(req, secret) {\n const received = req.headers[\"x-hypersnap-signature\"];\n const expected = crypto\n .createHmac(\"sha512\", secret)\n .update(req.rawBody) // the literal bytes, NOT JSON.stringify(req.body)\n .digest(\"hex\");\n return crypto.timingSafeEqual(\n Buffer.from(received, \"hex\"),\n Buffer.from(expected, \"hex\"),\n );\n}\n```\n\n### Python\n\n```python\nimport hmac, hashlib\n\ndef verify(raw_body: bytes, received_hex: str, secret: str) -> bool:\n expected = hmac.new(\n secret.encode(), raw_body, hashlib.sha512\n ).hexdigest()\n return hmac.compare_digest(expected, received_hex)\n```\n\n### Rust\n\n```rust,ignore\nuse hmac::{Hmac, Mac};\nuse sha2::Sha512;\n\nfn verify(raw_body: &[u8], received_hex: &str, secret: &str) -> bool {\n let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap();\n mac.update(raw_body);\n let expected = hex::encode(mac.finalize().into_bytes());\n // subtle::ConstantTimeEq in production.\n expected == received_hex\n}\n```\n\n## Which secret signs a delivery\n\nThe secret used for a given delivery is the **most recent non-expired entry** in the webhook's `secrets` array. During a rotation grace window you may see two valid secrets in the list at once. Your receiver should:\n\n1. Keep a set of accepted secrets, not just one.\n2. Try each in turn until one verifies.\n3. Drop a secret from the accepted set after its `expires_at` passes.\n\nThis way a secret rotation never loses a delivery.\n\n## Response codes Hypersnap expects\n\n| Your response | Hypersnap's interpretation |\n|---|---|\n| `2xx` | Delivered. Counted as `webhooks.delivery.succeeded`. |\n| `4xx` | Terminal failure. No retries. Counted as `webhooks.delivery.failed_4xx`. Your webhook is responsible for fixing whatever's wrong — a 4xx usually means a bug in the config (wrong URL path, auth) or a permanent rejection. |\n| `5xx` | Transient failure. Retried with exponential backoff up to `retry_max_attempts`. Counted as `webhooks.delivery.failed_5xx`. |\n| Timeout / network error | Transient failure. Retried. Counted as `webhooks.delivery.failed_network`. |\n\n**Default timeout** on a single attempt: **10 seconds** (`delivery_timeout_secs`).\n\n**Default retry schedule**: 5 attempts total, starting at 500ms and doubling (`retry_initial_backoff_ms × 2^attempt`). Failures in between live on a persistent RocksDB-backed retry queue so they survive restarts.\n\n## Rate limiting\n\nEach webhook has a rate limit enforced by the dispatcher:\n\n- Default: **1000 events per 60 seconds**, per webhook.\n- Events above the limit are **dropped**, not queued. Dropped events are counted in the `webhooks.delivery.rate_limited` metric.\n\nIf you expect high-volume streams, narrow your subscription filters so fewer events match, or ask your operator to raise the per-webhook limit.\n\n## Delivery guarantees\n\n- **At-least-once.** Transient failures + retries can replay the same `(event, attempt)` pair. Dedupe on the `data` field's natural key (cast hash, reaction unique tuple, etc.).\n- **Not strictly ordered.** Two events for the same cast can arrive in either order if one is retried while the other succeeds first pass. If ordering matters, attach server timestamps from the event payload.\n- **Transient failure ≠ lost.** 5xx/network failures go on the retry queue. You get them eventually.\n- **4xx = lost.** If you reply 4xx, the event is permanently dropped. Reserve 4xx for cases where retrying would be pointless.\n\n## Receiver checklist\n\n- [ ] Verify every delivery's HMAC before trusting the body.\n- [ ] Accept multiple active secrets during rotation grace windows.\n- [ ] Return `2xx` as soon as you've durably enqueued the event; don't hold the connection open for downstream work.\n- [ ] Dedupe on a natural key (cast hash, reaction tuple).\n- [ ] Return `5xx` on transient failures so Hypersnap can retry.\n- [ ] Reject with `4xx` only when there's nothing to be done.\n- [ ] Keep your receiver's p95 latency under `delivery_timeout_secs`.\n", "toc": [ { @@ -1497,7 +1497,7 @@ "sourcePath": "src/reference/webhooks/events.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/webhooks/events.md", "slug": "reference/webhooks/events", - "contentHtml": "

Event schemas #

\n

The data field of a delivered webhook envelope carries a type-specific payload. The type field in the envelope tells you which schema applies:

\n
{\n  \"created_at\": 1712345678,\n  \"type\": \"cast.created\",\n  \"data\": { /* see below */ }\n}\n
\n

Hypersnap fires the following event types. Field shapes mirror common Farcaster v2 contracts so existing client models deserialize directly.

\n
\n

cast.created #

\n

Emitted when a new cast is ingested by the node.

\n
{\n  \"type\": \"cast.created\",\n  \"data\": {\n    \"cast\": {\n      \"hash\": \"0x...\",\n      \"author\": { /* User */ },\n      \"text\": \"hello world\",\n      \"timestamp\": 1712345678,\n      \"parent_hash\": null,\n      \"parent_url\": null,\n      \"root_parent_url\": null,\n      \"embeds\": [],\n      \"mentioned_profiles\": [],\n      \"reactions\": { \"likes_count\": 0, \"recasts_count\": 0 },\n      \"replies\": { \"count\": 0 }\n    }\n  }\n}\n
\n

cast.deleted #

\n

Emitted when a cast delete message is applied.

\n
{\n  \"type\": \"cast.deleted\",\n  \"data\": {\n    \"cast\": { /* Same cast shape as cast.created */ }\n  }\n}\n
\n

user.created #

\n

Emitted on an IdRegistry register event seen on-chain. No filter fields.

\n
{\n  \"type\": \"user.created\",\n  \"data\": {\n    \"user\": { \"fid\": 12345, \"username\": null, \"...\" : \"...\" }\n  }\n}\n
\n

user.updated #

\n

Emitted when user-data (pfp, bio, display name, url, username) changes.

\n
{\n  \"type\": \"user.updated\",\n  \"data\": {\n    \"user\": { /* User (updated fields filled) */ }\n  }\n}\n
\n

follow.created #

\n

Emitted when a link_add message with type follow is applied.

\n
{\n  \"type\": \"follow.created\",\n  \"data\": {\n    \"follower\": { /* User */ },\n    \"target\":   { /* User */ }\n  }\n}\n
\n

follow.deleted #

\n

Emitted on link_remove for a follow link.

\n
{\n  \"type\": \"follow.deleted\",\n  \"data\": {\n    \"follower\": { /* User */ },\n    \"target\":   { /* User */ }\n  }\n}\n
\n

reaction.created #

\n

Emitted when a like or recast is applied.

\n
{\n  \"type\": \"reaction.created\",\n  \"data\": {\n    \"reaction_type\": \"like\",\n    \"user\":  { /* User */ },\n    \"cast\":  { /* Cast */ }\n  }\n}\n
\n

reaction_type is either \"like\" or \"recast\".

\n

reaction.deleted #

\n

Same shape as reaction.created, emitted when a reaction is removed.

\n
\n

Dedupe keys #

\n

For at-least-once delivery, dedupe on the natural key of each event type:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EventDedupe key
cast.created / cast.deleteddata.cast.hash
user.createddata.user.fid
user.updated(data.user.fid, received_at) or compare fields against last-known state
follow.created / follow.deleted(data.follower.fid, data.target.fid)
reaction.created / reaction.deleted(data.user.fid, data.cast.hash, data.reaction_type)
\n

Forward compatibility #

\n
    \n
  • New optional fields can appear on existing event payloads. Don't error on unknown fields.
  • \n
  • New event types can be added. Subscribe only to the types you know; ignore unfamiliar type values your code doesn't recognize.
  • \n
  • Event field semantics won't change for an existing type.
  • \n
", + "contentHtml": "

Event schemas #

\n

The data field of a delivered webhook envelope carries a type-specific payload. The type field in the envelope tells you which schema applies:

\n
{\n  \"created_at\": 1712345678,\n  \"type\": \"cast.created\",\n  \"data\": { /* see below */ }\n}\n
\n

Hypersnap fires the following event types. Field shapes mirror common Farcaster v2 contracts so existing client models deserialize directly.

\n
\n

cast.created #

\n

Emitted when a new cast is ingested by the node.

\n
{\n  \"type\": \"cast.created\",\n  \"data\": {\n    \"cast\": {\n      \"hash\": \"0x...\",\n      \"author\": { /* User */ },\n      \"text\": \"hello world\",\n      \"timestamp\": 1712345678,\n      \"parent_hash\": null,\n      \"parent_url\": null,\n      \"root_parent_url\": null,\n      \"embeds\": [],\n      \"mentioned_profiles\": [],\n      \"reactions\": { \"likes_count\": 0, \"recasts_count\": 0 },\n      \"replies\": { \"count\": 0 }\n    }\n  }\n}\n
\n

cast.deleted #

\n

Emitted when a cast delete message is applied.

\n
{\n  \"type\": \"cast.deleted\",\n  \"data\": {\n    \"cast\": { /* Same cast shape as cast.created */ }\n  }\n}\n
\n

user.created #

\n

Emitted on an IdRegistry register event seen on-chain. No filter fields.

\n
{\n  \"type\": \"user.created\",\n  \"data\": {\n    \"user\": { \"fid\": 12345, \"username\": null, \"...\" : \"...\" }\n  }\n}\n
\n

user.updated #

\n

Emitted when user-data (pfp, bio, display name, url, username) changes.

\n
{\n  \"type\": \"user.updated\",\n  \"data\": {\n    \"user\": { /* User (updated fields filled) */ }\n  }\n}\n
\n

follow.created #

\n

Emitted when a link_add message with type follow is applied.

\n
{\n  \"type\": \"follow.created\",\n  \"data\": {\n    \"follower\": { /* User */ },\n    \"target\":   { /* User */ }\n  }\n}\n
\n

follow.deleted #

\n

Emitted on link_remove for a follow link.

\n
{\n  \"type\": \"follow.deleted\",\n  \"data\": {\n    \"follower\": { /* User */ },\n    \"target\":   { /* User */ }\n  }\n}\n
\n

reaction.created #

\n

Emitted when a like or recast is applied.

\n
{\n  \"type\": \"reaction.created\",\n  \"data\": {\n    \"reaction_type\": \"like\",\n    \"user\":  { /* User */ },\n    \"cast\":  { /* Cast */ }\n  }\n}\n
\n

reaction_type is either \"like\" or \"recast\".

\n

reaction.deleted #

\n

Same shape as reaction.created, emitted when a reaction is removed.

\n
\n

Dedupe keys #

\n

For at-least-once delivery, dedupe on the natural key of each event type:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EventDedupe key
cast.created / cast.deleteddata.cast.hash
user.createddata.user.fid
user.updated(data.user.fid, received_at) or compare fields against last-known state
follow.created / follow.deleted(data.follower.fid, data.target.fid)
reaction.created / reaction.deleted(data.user.fid, data.cast.hash, data.reaction_type)
\n

Forward compatibility #

\n
    \n
  • New optional fields can appear on existing event payloads. Don't error on unknown fields.
  • \n
  • New event types can be added. Subscribe only to the types you know; ignore unfamiliar type values your code doesn't recognize.
  • \n
  • Event field semantics won't change for an existing type.
  • \n
", "contentMarkdown": "# Event schemas\n\nThe `data` field of a delivered webhook envelope carries a type-specific payload. The `type` field in the envelope tells you which schema applies:\n\n```json\n{\n \"created_at\": 1712345678,\n \"type\": \"cast.created\",\n \"data\": { /* see below */ }\n}\n```\n\nHypersnap fires the following event types. Field shapes mirror common Farcaster v2 contracts so existing client models deserialize directly.\n\n---\n\n## cast.created\n\nEmitted when a new cast is ingested by the node.\n\n```json\n{\n \"type\": \"cast.created\",\n \"data\": {\n \"cast\": {\n \"hash\": \"0x...\",\n \"author\": { /* User */ },\n \"text\": \"hello world\",\n \"timestamp\": 1712345678,\n \"parent_hash\": null,\n \"parent_url\": null,\n \"root_parent_url\": null,\n \"embeds\": [],\n \"mentioned_profiles\": [],\n \"reactions\": { \"likes_count\": 0, \"recasts_count\": 0 },\n \"replies\": { \"count\": 0 }\n }\n }\n}\n```\n\n## cast.deleted\n\nEmitted when a cast delete message is applied.\n\n```json\n{\n \"type\": \"cast.deleted\",\n \"data\": {\n \"cast\": { /* Same cast shape as cast.created */ }\n }\n}\n```\n\n## user.created\n\nEmitted on an `IdRegistry` register event seen on-chain. No filter fields.\n\n```json\n{\n \"type\": \"user.created\",\n \"data\": {\n \"user\": { \"fid\": 12345, \"username\": null, \"...\" : \"...\" }\n }\n}\n```\n\n## user.updated\n\nEmitted when user-data (pfp, bio, display name, url, username) changes.\n\n```json\n{\n \"type\": \"user.updated\",\n \"data\": {\n \"user\": { /* User (updated fields filled) */ }\n }\n}\n```\n\n## follow.created\n\nEmitted when a `link_add` message with type `follow` is applied.\n\n```json\n{\n \"type\": \"follow.created\",\n \"data\": {\n \"follower\": { /* User */ },\n \"target\": { /* User */ }\n }\n}\n```\n\n## follow.deleted\n\nEmitted on `link_remove` for a follow link.\n\n```json\n{\n \"type\": \"follow.deleted\",\n \"data\": {\n \"follower\": { /* User */ },\n \"target\": { /* User */ }\n }\n}\n```\n\n## reaction.created\n\nEmitted when a like or recast is applied.\n\n```json\n{\n \"type\": \"reaction.created\",\n \"data\": {\n \"reaction_type\": \"like\",\n \"user\": { /* User */ },\n \"cast\": { /* Cast */ }\n }\n}\n```\n\n`reaction_type` is either `\"like\"` or `\"recast\"`.\n\n## reaction.deleted\n\nSame shape as `reaction.created`, emitted when a reaction is removed.\n\n---\n\n## Dedupe keys\n\nFor at-least-once delivery, dedupe on the natural key of each event type:\n\n| Event | Dedupe key |\n|---|---|\n| `cast.created` / `cast.deleted` | `data.cast.hash` |\n| `user.created` | `data.user.fid` |\n| `user.updated` | `(data.user.fid, received_at)` or compare fields against last-known state |\n| `follow.created` / `follow.deleted` | `(data.follower.fid, data.target.fid)` |\n| `reaction.created` / `reaction.deleted` | `(data.user.fid, data.cast.hash, data.reaction_type)` |\n\n## Forward compatibility\n\n- New optional fields can appear on existing event payloads. Don't error on unknown fields.\n- New event types can be added. Subscribe only to the types you know; ignore unfamiliar `type` values your code doesn't recognize.\n- Event field semantics won't change for an existing `type`.\n", "toc": [ { @@ -1558,7 +1558,7 @@ "sourcePath": "src/reference/miniapps/index.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/miniapps/index.md", "slug": "reference/miniapps", - "contentHtml": "

Mini-app notifications #

\n

Hypersnap is a multi-tenant proxy for Farcaster Mini App notifications. Any Farcaster mini app can register with a Hypersnap node, receive the upstream token events, and send push notifications back to its users — using the exact wire contract the Farcaster Mini App spec defines, so clients (Warpcast, etc.) work against Hypersnap with no changes.

\n

Who should use this #

\n
    \n
  • Mini-app developers who want push notifications on new content, social activity, or in-app events.
  • \n
  • Anyone who has implemented the upstream Farcaster Mini App notification flow against a different proxy and wants to switch.
  • \n
\n

Three moving parts #

\n
    \n
  1. Register your app. Sign a management request with your FID's custody key to create a mini-app record. Hypersnap assigns a random 16-character base58 app_id and returns a send secret. → Registering a mini app
  2. \n
  3. Receive token events. Farcaster clients POST JFS-signed events to /v2/farcaster/frame/webhook/<app_id> when users add your app or toggle notifications. Hypersnap verifies + stores them. You don't need to implement this endpoint yourself — point the client at Hypersnap and you're done. → Client token webhook
  4. \n
  5. Send notifications. POST a payload to /v2/farcaster/frame/notifications/<app_id> with your send secret. Hypersnap looks up the enabled tokens for your app, groups them by notification URL, and POSTs to each client in batches. → Sending notifications
  6. \n
\n

Relationship to the rest of the API #

\n

This is its own pipeline, entirely separate from:

\n\n

Authentication summary #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SurfaceAuth
/v2/farcaster/frame/app/*EIP-712 custody signature
/v2/farcaster/frame/webhook/<app_id>JFS from a Farcaster client
/v2/farcaster/frame/notifications/<app_id>Per-app x-api-key: <send_secret>
\n

The send secret is returned at app-creation time and can be rotated via the signed management API. Treat it as an API key and keep it server-side.

", + "contentHtml": "

Mini-app notifications #

\n

Hypersnap is a multi-tenant proxy for Farcaster Mini App notifications. Any Farcaster mini app can register with a Hypersnap node, receive the upstream token events, and send push notifications back to its users — using the exact wire contract the Farcaster Mini App spec defines, so clients (Warpcast, etc.) work against Hypersnap with no changes.

\n

Who should use this #

\n
    \n
  • Mini-app developers who want push notifications on new content, social activity, or in-app events.
  • \n
  • Anyone who has implemented the upstream Farcaster Mini App notification flow against a different proxy and wants to switch.
  • \n
\n

Three moving parts #

\n
    \n
  1. Register your app. Sign a management request with your FID's custody key to create a mini-app record. Hypersnap assigns a random 16-character base58 app_id and returns a send secret. → Registering a mini app
  2. \n
  3. Receive token events. Farcaster clients POST JFS-signed events to /v2/farcaster/frame/webhook/<app_id> when users add your app or toggle notifications. Hypersnap verifies + stores them. You don't need to implement this endpoint yourself — point the client at Hypersnap and you're done. → Client token webhook
  4. \n
  5. Send notifications. POST a payload to /v2/farcaster/frame/notifications/<app_id> with your send secret. Hypersnap looks up the enabled tokens for your app, groups them by notification URL, and POSTs to each client in batches. → Sending notifications
  6. \n
\n

Relationship to the rest of the API #

\n

This is its own pipeline, entirely separate from:

\n\n

Authentication summary #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SurfaceAuth
/v2/farcaster/frame/app/*EIP-712 custody signature
/v2/farcaster/frame/webhook/<app_id>JFS from a Farcaster client
/v2/farcaster/frame/notifications/<app_id>Per-app x-api-key: <send_secret>
\n

The send secret is returned at app-creation time and can be rotated via the signed management API. Treat it as an API key and keep it server-side.

", "contentMarkdown": "# Mini-app notifications\n\nHypersnap is a multi-tenant proxy for [Farcaster Mini App notifications](https://miniapps.farcaster.xyz/docs/specification). Any Farcaster mini app can register with a Hypersnap node, receive the upstream token events, and send push notifications back to its users — using the exact wire contract the Farcaster Mini App spec defines, so clients (Warpcast, etc.) work against Hypersnap with no changes.\n\n## Who should use this\n\n- Mini-app developers who want push notifications on new content, social activity, or in-app events.\n- Anyone who has implemented the upstream Farcaster Mini App notification flow against a different proxy and wants to switch.\n\n## Three moving parts\n\n1. **Register your app.** Sign a management request with your FID's custody key to create a mini-app record. Hypersnap assigns a random 16-character base58 `app_id` and returns a send secret. → [Registering a mini app](./registering.md)\n2. **Receive token events.** Farcaster clients POST JFS-signed events to `/v2/farcaster/frame/webhook/` when users add your app or toggle notifications. Hypersnap verifies + stores them. You don't need to implement this endpoint yourself — point the client at Hypersnap and you're done. → [Client token webhook](./token-webhook.md)\n3. **Send notifications.** POST a payload to `/v2/farcaster/frame/notifications/` with your send secret. Hypersnap looks up the enabled tokens for your app, groups them by notification URL, and POSTs to each client in batches. → [Sending notifications](./sending.md)\n\n## Relationship to the rest of the API\n\nThis is its own pipeline, entirely separate from:\n\n- **[Read API](../reads/index.md)** — unauthenticated GETs.\n- **[Webhooks](../webhooks/index.md)** — outbound event streams to your servers.\n- **[User notifications](../reads/notifications.md)** — the per-user in-app \"mentions/replies\" feed.\n\n## Authentication summary\n\n| Surface | Auth |\n|---|---|\n| `/v2/farcaster/frame/app/*` | [EIP-712 custody signature](../../concepts/authentication.md) |\n| `/v2/farcaster/frame/webhook/` | [JFS from a Farcaster client](../../concepts/jfs.md) |\n| `/v2/farcaster/frame/notifications/` | Per-app `x-api-key: ` |\n\nThe send secret is returned at app-creation time and can be rotated via the signed management API. Treat it as an API key and keep it server-side.\n", "toc": [ { @@ -1589,7 +1589,7 @@ "sourcePath": "src/reference/miniapps/registering.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/miniapps/registering.md", "slug": "reference/miniapps/registering", - "contentHtml": "

Registering a mini app #

\n

All endpoints in this section are under /v2/farcaster/frame/app/ and require an EIP-712 signature from your FID's custody key. Max body: 32 KB.

\n

The server assigns a random 16-character base58 app_id (~93 bits of entropy) and a fresh send secret at create time. These are what you use in the URL paths for the token webhook and send endpoints.

\n
\n

POST /v2/farcaster/frame/app/ — create #

\n

X-Hypersnap-Op: app.create

\n

Request body (CreateAppRequest):

\n
{\n  \"name\": \"my mini app\",\n  \"app_url\": \"https://miniapp.example.com\",\n  \"description\": \"optional\",\n  \"signer_fid_allowlist\": [12345, 67890]\n}\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeNotes
namestring1–128 chars. Human-readable, shown in management responses.
app_urlstringCanonical mini-app URL. SSRF-checked at create time. Hypersnap never POSTs to this URL itself — it's informational.
descriptionstring (optional)Free-form.
signer_fid_allowlistVec<u64> (optional)FIDs whose Farcaster signing keys are permitted to submit JFS token events for this app. Empty = any active signer is accepted. Capped at 1024 entries.
\n

Response (AppResponse):

\n
{\n  \"app\": {\n    \"app_id\": \"3Hq9ZgK2p4vNfWxR\",\n    \"owner_fid\": 12345,\n    \"name\": \"my mini app\",\n    \"app_url\": \"https://miniapp.example.com\",\n    \"description\": \"optional\",\n    \"signer_fid_allowlist\": [],\n    \"send_secrets\": [\n      {\n        \"uid\": \"uuid\",\n        \"value\": \"<64-char hex send secret>\",\n        \"expires_at\": null,\n        \"created_at\": 1712345678\n      }\n    ],\n    \"created_at\": 1712345678,\n    \"updated_at\": 1712345678\n  }\n}\n
\n

Save app.app_id and app.send_secrets[0].value. You'll need:

\n
    \n
  • app_id in the URL path for the token webhook (/v2/farcaster/frame/webhook/<app_id>) and the send endpoint (/v2/farcaster/frame/notifications/<app_id>).
  • \n
  • send_secrets[0].value as the x-api-key header when you call the send endpoint.
  • \n
\n

Errors

\n
    \n
  • 400 — invalid name / app_url / SSRF-blocked URL / allowlist too big.
  • \n
  • 401 — signature / auth failure.
  • \n
  • 429 — per-FID app cap hit.
  • \n
\n
\n
\n

GET /v2/farcaster/frame/app/ — lookup #

\n

X-Hypersnap-Op: app.read

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
app_idstringyes
\n

Response — same AppResponse shape. 403 if the app belongs to a different FID; 404 if not found.

\n
\n
\n

GET /v2/farcaster/frame/app/list — list #

\n

X-Hypersnap-Op: app.read

\n

Query — none.

\n

Response

\n
{ \"apps\": [ { /* RegisteredApp */ } ] }\n
\n

Returns every mini app owned by the signing FID.

\n
\n
\n

PUT /v2/farcaster/frame/app/ — update #

\n

X-Hypersnap-Op: app.update

\n

Request body (UpdateAppRequest):

\n
{\n  \"app_id\": \"3Hq9ZgK2p4vNfWxR\",\n  \"name\": \"optional new name\",\n  \"app_url\": \"https://new.example.com\",\n  \"description\": \"optional\",\n  \"signer_fid_allowlist\": [12345]\n}\n
\n

Only supplied fields change. Passing signer_fid_allowlist replaces the whole list.

\n
\n
\n

DELETE /v2/farcaster/frame/app/ — delete #

\n

X-Hypersnap-Op: app.delete

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
app_idstringyes
\n

Response

\n
{ \"deleted\": true }\n
\n

After deletion, both the send endpoint and the client token webhook return 404 for this app_id. Previously registered notification tokens become unreachable (the token store still holds them but nothing reads them).

\n
\n
\n

POST /v2/farcaster/frame/app/secret/rotate — rotate send secret #

\n

X-Hypersnap-Op: app.rotate_secret

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
app_idstringyes
\n

Response — the full AppResponse. send_secrets has one new entry appended (the newest) and previously-active secrets have expires_at set to now + secret_grace_period_secs (default 24h).

\n

How to use rotation:

\n
    \n
  1. Call /secret/rotate.
  2. \n
  3. Read the new secret from send_secrets[-1].value.
  4. \n
  5. Deploy the new secret to your backend.
  6. \n
  7. The old secret keeps working until the grace window passes, so you don't have a downtime window during the rollout.
  8. \n
\n
", + "contentHtml": "

Registering a mini app #

\n

All endpoints in this section are under /v2/farcaster/frame/app/ and require an EIP-712 signature from your FID's custody key. Max body: 32 KB.

\n

The server assigns a random 16-character base58 app_id (~93 bits of entropy) and a fresh send secret at create time. These are what you use in the URL paths for the token webhook and send endpoints.

\n
\n

POST /v2/farcaster/frame/app/ — create #

\n

X-Hypersnap-Op: app.create

\n

Request body (CreateAppRequest):

\n
{\n  \"name\": \"my mini app\",\n  \"app_url\": \"https://miniapp.example.com\",\n  \"description\": \"optional\",\n  \"signer_fid_allowlist\": [12345, 67890]\n}\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeNotes
namestring1–128 chars. Human-readable, shown in management responses.
app_urlstringCanonical mini-app URL. SSRF-checked at create time. Hypersnap never POSTs to this URL itself — it's informational.
descriptionstring (optional)Free-form.
signer_fid_allowlistVec<u64> (optional)FIDs whose Farcaster signing keys are permitted to submit JFS token events for this app. Empty = any active signer is accepted. Capped at 1024 entries.
\n

Response (AppResponse):

\n
{\n  \"app\": {\n    \"app_id\": \"3Hq9ZgK2p4vNfWxR\",\n    \"owner_fid\": 12345,\n    \"name\": \"my mini app\",\n    \"app_url\": \"https://miniapp.example.com\",\n    \"description\": \"optional\",\n    \"signer_fid_allowlist\": [],\n    \"send_secrets\": [\n      {\n        \"uid\": \"uuid\",\n        \"value\": \"<64-char hex send secret>\",\n        \"expires_at\": null,\n        \"created_at\": 1712345678\n      }\n    ],\n    \"created_at\": 1712345678,\n    \"updated_at\": 1712345678\n  }\n}\n
\n

Save app.app_id and app.send_secrets[0].value. You'll need:

\n
    \n
  • app_id in the URL path for the token webhook (/v2/farcaster/frame/webhook/<app_id>) and the send endpoint (/v2/farcaster/frame/notifications/<app_id>).
  • \n
  • send_secrets[0].value as the x-api-key header when you call the send endpoint.
  • \n
\n

Errors

\n
    \n
  • 400 — invalid name / app_url / SSRF-blocked URL / allowlist too big.
  • \n
  • 401 — signature / auth failure.
  • \n
  • 429 — per-FID app cap hit.
  • \n
\n
\n
\n

GET /v2/farcaster/frame/app/ — lookup #

\n

X-Hypersnap-Op: app.read

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
app_idstringyes
\n

Response — same AppResponse shape. 403 if the app belongs to a different FID; 404 if not found.

\n
\n
\n

GET /v2/farcaster/frame/app/list — list #

\n

X-Hypersnap-Op: app.read

\n

Query — none.

\n

Response

\n
{ \"apps\": [ { /* RegisteredApp */ } ] }\n
\n

Returns every mini app owned by the signing FID.

\n
\n
\n

PUT /v2/farcaster/frame/app/ — update #

\n

X-Hypersnap-Op: app.update

\n

Request body (UpdateAppRequest):

\n
{\n  \"app_id\": \"3Hq9ZgK2p4vNfWxR\",\n  \"name\": \"optional new name\",\n  \"app_url\": \"https://new.example.com\",\n  \"description\": \"optional\",\n  \"signer_fid_allowlist\": [12345]\n}\n
\n

Only supplied fields change. Passing signer_fid_allowlist replaces the whole list.

\n
\n
\n

DELETE /v2/farcaster/frame/app/ — delete #

\n

X-Hypersnap-Op: app.delete

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
app_idstringyes
\n

Response

\n
{ \"deleted\": true }\n
\n

After deletion, both the send endpoint and the client token webhook return 404 for this app_id. Previously registered notification tokens become unreachable (the token store still holds them but nothing reads them).

\n
\n
\n

POST /v2/farcaster/frame/app/secret/rotate — rotate send secret #

\n

X-Hypersnap-Op: app.rotate_secret

\n

Query

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
NameTypeRequired
app_idstringyes
\n

Response — the full AppResponse. send_secrets has one new entry appended (the newest) and previously-active secrets have expires_at set to now + secret_grace_period_secs (default 24h).

\n

How to use rotation:

\n
    \n
  1. Call /secret/rotate.
  2. \n
  3. Read the new secret from send_secrets[-1].value.
  4. \n
  5. Deploy the new secret to your backend.
  6. \n
  7. The old secret keeps working until the grace window passes, so you don't have a downtime window during the rollout.
  8. \n
\n
", "contentMarkdown": "# Registering a mini app\n\nAll endpoints in this section are under `/v2/farcaster/frame/app/` and require an [EIP-712 signature](../../concepts/authentication.md) from your FID's custody key. Max body: **32 KB**.\n\nThe server assigns a random 16-character base58 `app_id` (~93 bits of entropy) and a fresh send secret at create time. These are what you use in the URL paths for the token webhook and send endpoints.\n\n---\n\n## POST /v2/farcaster/frame/app/ — create\n\n`X-Hypersnap-Op`: `app.create`\n\n**Request body** (`CreateAppRequest`):\n\n```json\n{\n \"name\": \"my mini app\",\n \"app_url\": \"https://miniapp.example.com\",\n \"description\": \"optional\",\n \"signer_fid_allowlist\": [12345, 67890]\n}\n```\n\n| Field | Type | Notes |\n|---|---|---|\n| `name` | string | 1–128 chars. Human-readable, shown in management responses. |\n| `app_url` | string | Canonical mini-app URL. SSRF-checked at create time. Hypersnap never POSTs to this URL itself — it's informational. |\n| `description` | string (optional) | Free-form. |\n| `signer_fid_allowlist` | `Vec` (optional) | FIDs whose Farcaster signing keys are permitted to submit JFS token events for this app. Empty = any active signer is accepted. Capped at 1024 entries. |\n\n**Response** (`AppResponse`):\n\n```json\n{\n \"app\": {\n \"app_id\": \"3Hq9ZgK2p4vNfWxR\",\n \"owner_fid\": 12345,\n \"name\": \"my mini app\",\n \"app_url\": \"https://miniapp.example.com\",\n \"description\": \"optional\",\n \"signer_fid_allowlist\": [],\n \"send_secrets\": [\n {\n \"uid\": \"uuid\",\n \"value\": \"<64-char hex send secret>\",\n \"expires_at\": null,\n \"created_at\": 1712345678\n }\n ],\n \"created_at\": 1712345678,\n \"updated_at\": 1712345678\n }\n}\n```\n\n**Save `app.app_id` and `app.send_secrets[0].value`.** You'll need:\n\n- `app_id` in the URL path for the token webhook (`/v2/farcaster/frame/webhook/`) and the send endpoint (`/v2/farcaster/frame/notifications/`).\n- `send_secrets[0].value` as the `x-api-key` header when you call the send endpoint.\n\n**Errors**\n\n- `400` — invalid name / app_url / SSRF-blocked URL / allowlist too big.\n- `401` — signature / auth failure.\n- `429` — per-FID app cap hit.\n\n
\n\n---\n\n## GET /v2/farcaster/frame/app/ — lookup\n\n`X-Hypersnap-Op`: `app.read`\n\n**Query**\n\n| Name | Type | Required |\n|---|---|---|\n| `app_id` | string | yes |\n\n**Response** — same `AppResponse` shape. `403` if the app belongs to a different FID; `404` if not found.\n\n
\n\n---\n\n## GET /v2/farcaster/frame/app/list — list\n\n`X-Hypersnap-Op`: `app.read`\n\n**Query** — none.\n\n**Response**\n\n```json\n{ \"apps\": [ { /* RegisteredApp */ } ] }\n```\n\nReturns every mini app owned by the signing FID.\n\n
\n\n---\n\n## PUT /v2/farcaster/frame/app/ — update\n\n`X-Hypersnap-Op`: `app.update`\n\n**Request body** (`UpdateAppRequest`):\n\n```json\n{\n \"app_id\": \"3Hq9ZgK2p4vNfWxR\",\n \"name\": \"optional new name\",\n \"app_url\": \"https://new.example.com\",\n \"description\": \"optional\",\n \"signer_fid_allowlist\": [12345]\n}\n```\n\nOnly supplied fields change. Passing `signer_fid_allowlist` replaces the whole list.\n\n
\n\n---\n\n## DELETE /v2/farcaster/frame/app/ — delete\n\n`X-Hypersnap-Op`: `app.delete`\n\n**Query**\n\n| Name | Type | Required |\n|---|---|---|\n| `app_id` | string | yes |\n\n**Response**\n\n```json\n{ \"deleted\": true }\n```\n\nAfter deletion, both the send endpoint and the client token webhook return `404` for this `app_id`. Previously registered notification tokens become unreachable (the token store still holds them but nothing reads them).\n\n
\n\n---\n\n## POST /v2/farcaster/frame/app/secret/rotate — rotate send secret\n\n`X-Hypersnap-Op`: `app.rotate_secret`\n\n**Query**\n\n| Name | Type | Required |\n|---|---|---|\n| `app_id` | string | yes |\n\n**Response** — the full `AppResponse`. `send_secrets` has one new entry appended (the newest) and previously-active secrets have `expires_at` set to `now + secret_grace_period_secs` (default 24h).\n\n**How to use rotation:**\n\n1. Call `/secret/rotate`.\n2. Read the new secret from `send_secrets[-1].value`.\n3. Deploy the new secret to your backend.\n4. The old secret keeps working until the grace window passes, so you don't have a downtime window during the rollout.\n\n
\n", "toc": [ { @@ -1630,7 +1630,7 @@ "sourcePath": "src/reference/miniapps/token-webhook.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/miniapps/token-webhook.md", "slug": "reference/miniapps/token-webhook", - "contentHtml": "

Client token webhook #

\n

You almost never call this endpoint yourself — Farcaster clients (Warpcast, etc.) POST to it when a user adds, removes, enables, or disables your mini app.

\n

This page is a reference for:

\n
    \n
  • Understanding what Hypersnap does with those events.
  • \n
  • Debugging cases where notifications aren't reaching users (did the event arrive?).
  • \n
\n

URL #

\n
POST /v2/farcaster/frame/webhook/<app_id>\n
\n

<app_id> is the 16-character base58 string Hypersnap gave you at app-creation time. You embed this URL in your mini app's manifest so Farcaster clients know where to POST.

\n

Request body — JFS envelope #

\n
{\n  \"header\":    \"<base64url>\",\n  \"payload\":   \"<base64url JSON>\",\n  \"signature\": \"<base64url>\"\n}\n
\n

This is a standard JSON Farcaster Signature envelope. The decoded payload is one of:

\n
{ \"event\": \"miniapp_added\",         \"notificationDetails\": { \"url\": \"https://...\", \"token\": \"...\" } }\n{ \"event\": \"miniapp_removed\" }\n{ \"event\": \"notifications_enabled\", \"notificationDetails\": { \"url\": \"https://...\", \"token\": \"...\" } }\n{ \"event\": \"notifications_disabled\" }\n
\n

The four events map to state transitions on the user's relationship with your mini app:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EventWhat Hypersnap does
miniapp_added (with notificationDetails)Upserts an enabled token for (fid, app_id).
miniapp_added (without notificationDetails)Records the add without a token (notifications opt-out).
miniapp_removedDeletes all tokens for (fid, app_id).
notifications_enabledUpserts / re-enables a token.
notifications_disabledMarks the token disabled but does not delete it.
\n

Verification rules #

\n

Hypersnap applies these checks before touching the token store:

\n
    \n
  1. Base64url decode the three envelope fields.
  2. \n
  3. Ed25519 verify the signature against the key in the JFS header.
  4. \n
  5. Active signer check — look up the signer set for the claimed FID from the on-chain KeyRegistry. The signing key must be currently active.
  6. \n
  7. Signer allowlist — if the mini-app record has a non-empty signer_fid_allowlist, the signer FID must be in it.
  8. \n
\n

If any step fails, Hypersnap returns 401 and does not apply the event.

\n

Response shape #

\n

Successful ack:

\n
{ \"success\": true }\n
\n

Failure:

\n
{ \"success\": false, \"message\": \"...\" }\n
\n

Clients retry 5xx and give up on 4xx — same contract your own receiver would get, except here Hypersnap is the receiver and you're the beneficiary.

\n

What you need to implement #

\n

Nothing, as long as you're proxying through Hypersnap. Put the /v2/farcaster/frame/webhook/<app_id> URL in your mini app's manifest:

\n
{\n  \"name\": \"my mini app\",\n  \"icon_url\": \"https://...\",\n  \"home_url\": \"https://miniapp.example.com\",\n  \"webhook_url\": \"https://haatz.quilibrium.com/v2/farcaster/frame/webhook/3Hq9ZgK2p4vNfWxR\"\n}\n
\n

When users add your app, Farcaster clients POST to that URL, Hypersnap stores the token, and a subsequent call to your send endpoint will reach them.

\n

Debugging #

\n

If a user added your app but isn't receiving notifications:

\n
    \n
  1. Check that the event arrived. Hypersnap logs ingest failures at the hypersnap::notifications tracing target. A failed verification logs why (bad signature, inactive signer, not in allowlist).
  2. \n
  3. Check the app exists. Call GET /v2/farcaster/frame/app/?app_id=<id> (signed) and confirm it returns 200.
  4. \n
  5. Check the signer allowlist. If you set signer_fid_allowlist, the user's Farcaster signer FID needs to be in it. An overly narrow allowlist is the most common mistake.
  6. \n
  7. Check you're pointed at the right app_id. The <app_id> in the webhook URL and the <app_id> in your send endpoint calls must match.
  8. \n
", + "contentHtml": "

Client token webhook #

\n

You almost never call this endpoint yourself — Farcaster clients (Warpcast, etc.) POST to it when a user adds, removes, enables, or disables your mini app.

\n

This page is a reference for:

\n
    \n
  • Understanding what Hypersnap does with those events.
  • \n
  • Debugging cases where notifications aren't reaching users (did the event arrive?).
  • \n
\n

URL #

\n
POST /v2/farcaster/frame/webhook/<app_id>\n
\n

<app_id> is the 16-character base58 string Hypersnap gave you at app-creation time. You embed this URL in your mini app's manifest so Farcaster clients know where to POST.

\n

Request body — JFS envelope #

\n
{\n  \"header\":    \"<base64url>\",\n  \"payload\":   \"<base64url JSON>\",\n  \"signature\": \"<base64url>\"\n}\n
\n

This is a standard JSON Farcaster Signature envelope. The decoded payload is one of:

\n
{ \"event\": \"miniapp_added\",         \"notificationDetails\": { \"url\": \"https://...\", \"token\": \"...\" } }\n{ \"event\": \"miniapp_removed\" }\n{ \"event\": \"notifications_enabled\", \"notificationDetails\": { \"url\": \"https://...\", \"token\": \"...\" } }\n{ \"event\": \"notifications_disabled\" }\n
\n

The four events map to state transitions on the user's relationship with your mini app:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EventWhat Hypersnap does
miniapp_added (with notificationDetails)Upserts an enabled token for (fid, app_id).
miniapp_added (without notificationDetails)Records the add without a token (notifications opt-out).
miniapp_removedDeletes all tokens for (fid, app_id).
notifications_enabledUpserts / re-enables a token.
notifications_disabledMarks the token disabled but does not delete it.
\n

Verification rules #

\n

Hypersnap applies these checks before touching the token store:

\n
    \n
  1. Base64url decode the three envelope fields.
  2. \n
  3. Ed25519 verify the signature against the key in the JFS header.
  4. \n
  5. Active signer check — look up the signer set for the claimed FID from the on-chain KeyRegistry. The signing key must be currently active.
  6. \n
  7. Signer allowlist — if the mini-app record has a non-empty signer_fid_allowlist, the signer FID must be in it.
  8. \n
\n

If any step fails, Hypersnap returns 401 and does not apply the event.

\n

Response shape #

\n

Successful ack:

\n
{ \"success\": true }\n
\n

Failure:

\n
{ \"success\": false, \"message\": \"...\" }\n
\n

Clients retry 5xx and give up on 4xx — same contract your own receiver would get, except here Hypersnap is the receiver and you're the beneficiary.

\n

What you need to implement #

\n

Nothing, as long as you're proxying through Hypersnap. Put the /v2/farcaster/frame/webhook/<app_id> URL in your mini app's manifest:

\n
{\n  \"name\": \"my mini app\",\n  \"icon_url\": \"https://...\",\n  \"home_url\": \"https://miniapp.example.com\",\n  \"webhook_url\": \"https://haatz.quilibrium.com/v2/farcaster/frame/webhook/3Hq9ZgK2p4vNfWxR\"\n}\n
\n

When users add your app, Farcaster clients POST to that URL, Hypersnap stores the token, and a subsequent call to your send endpoint will reach them.

\n

Debugging #

\n

If a user added your app but isn't receiving notifications:

\n
    \n
  1. Check that the event arrived. Hypersnap logs ingest failures at the hypersnap::notifications tracing target. A failed verification logs why (bad signature, inactive signer, not in allowlist).
  2. \n
  3. Check the app exists. Call GET /v2/farcaster/frame/app/?app_id=<id> (signed) and confirm it returns 200.
  4. \n
  5. Check the signer allowlist. If you set signer_fid_allowlist, the user's Farcaster signer FID needs to be in it. An overly narrow allowlist is the most common mistake.
  6. \n
  7. Check you're pointed at the right app_id. The <app_id> in the webhook URL and the <app_id> in your send endpoint calls must match.
  8. \n
", "contentMarkdown": "# Client token webhook\n\nYou almost never call this endpoint yourself — **Farcaster clients** (Warpcast, etc.) POST to it when a user adds, removes, enables, or disables your mini app.\n\nThis page is a reference for:\n\n- Understanding what Hypersnap does with those events.\n- Debugging cases where notifications aren't reaching users (did the event arrive?).\n\n## URL\n\n```\nPOST /v2/farcaster/frame/webhook/\n```\n\n`` is the 16-character base58 string Hypersnap gave you at app-creation time. You embed this URL in your mini app's manifest so Farcaster clients know where to POST.\n\n## Request body — JFS envelope\n\n```json\n{\n \"header\": \"\",\n \"payload\": \"\",\n \"signature\": \"\"\n}\n```\n\nThis is a standard [JSON Farcaster Signature envelope](../../concepts/jfs.md). The decoded `payload` is one of:\n\n```json\n{ \"event\": \"miniapp_added\", \"notificationDetails\": { \"url\": \"https://...\", \"token\": \"...\" } }\n{ \"event\": \"miniapp_removed\" }\n{ \"event\": \"notifications_enabled\", \"notificationDetails\": { \"url\": \"https://...\", \"token\": \"...\" } }\n{ \"event\": \"notifications_disabled\" }\n```\n\nThe four events map to state transitions on the user's relationship with your mini app:\n\n| Event | What Hypersnap does |\n|---|---|\n| `miniapp_added` (with `notificationDetails`) | Upserts an enabled token for `(fid, app_id)`. |\n| `miniapp_added` (without `notificationDetails`) | Records the add without a token (notifications opt-out). |\n| `miniapp_removed` | Deletes all tokens for `(fid, app_id)`. |\n| `notifications_enabled` | Upserts / re-enables a token. |\n| `notifications_disabled` | Marks the token disabled but does not delete it. |\n\n## Verification rules\n\nHypersnap applies these checks before touching the token store:\n\n1. **Base64url decode** the three envelope fields.\n2. **Ed25519 verify** the signature against the `key` in the JFS header.\n3. **Active signer check** — look up the signer set for the claimed FID from the on-chain `KeyRegistry`. The signing key must be currently active.\n4. **Signer allowlist** — if the mini-app record has a non-empty `signer_fid_allowlist`, the signer FID must be in it.\n\nIf any step fails, Hypersnap returns `401` and does not apply the event.\n\n## Response shape\n\nSuccessful ack:\n\n```json\n{ \"success\": true }\n```\n\nFailure:\n\n```json\n{ \"success\": false, \"message\": \"...\" }\n```\n\nClients retry `5xx` and give up on `4xx` — same contract your own receiver would get, except here Hypersnap is the receiver and you're the beneficiary.\n\n## What you need to implement\n\nNothing, as long as you're proxying through Hypersnap. Put the `/v2/farcaster/frame/webhook/` URL in your mini app's manifest:\n\n```json\n{\n \"name\": \"my mini app\",\n \"icon_url\": \"https://...\",\n \"home_url\": \"https://miniapp.example.com\",\n \"webhook_url\": \"https://haatz.quilibrium.com/v2/farcaster/frame/webhook/3Hq9ZgK2p4vNfWxR\"\n}\n```\n\nWhen users add your app, Farcaster clients POST to that URL, Hypersnap stores the token, and a subsequent call to your send endpoint will reach them.\n\n## Debugging\n\nIf a user added your app but isn't receiving notifications:\n\n1. **Check that the event arrived.** Hypersnap logs ingest failures at the `hypersnap::notifications` tracing target. A failed verification logs why (bad signature, inactive signer, not in allowlist).\n2. **Check the app exists.** Call `GET /v2/farcaster/frame/app/?app_id=` (signed) and confirm it returns `200`.\n3. **Check the signer allowlist.** If you set `signer_fid_allowlist`, the user's Farcaster signer FID needs to be in it. An overly narrow allowlist is the most common mistake.\n4. **Check you're pointed at the right app_id.** The `` in the webhook URL and the `` in your send endpoint calls must match.\n", "toc": [ { @@ -1671,7 +1671,7 @@ "sourcePath": "src/reference/miniapps/sending.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/reference/miniapps/sending.md", "slug": "reference/miniapps/sending", - "contentHtml": "

Sending notifications #

\n
POST /v2/farcaster/frame/notifications/<app_id>\n
\n

This is the endpoint you call from your backend when you want to push a notification to your mini app's users.

\n

Auth: x-api-key: <send_secret>. The value must match the most recently created unexpired entry in your app's send_secrets array. See Registering a mini app for how to obtain and rotate the secret.

\n

Request body #

\n
{\n  \"notification\": {\n    \"title\":      \"string (≤32 chars)\",\n    \"body\":       \"string (≤128 chars)\",\n    \"target_url\": \"https://miniapp.example.com/path/in/app\",\n    \"uuid\":       \"optional UUID string\"\n  },\n  \"target_fids\":   [12345, 67890],\n  \"exclude_fids\":  [],\n  \"following_fid\": null,\n  \"minimum_user_score\": null,\n  \"near_location\":     null\n}\n
\n

notification #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeRequiredNotes
titlestringyes≤ 32 characters (spec limit).
bodystringyes≤ 128 characters (spec limit).
target_urlstringyes≤ 256 characters. Must be the same domain as your mini-app's app_url.
uuidstringnoA UUID you supply. Becomes the notificationId on the per-client fan-out. Enables (fid, notificationId) dedupe within the 24h window. Generate a random UUID per logical notification.
\n

Targeting #

\n
    \n
  • target_fids — explicit list of FIDs to deliver to. Empty = all enabled FIDs for this app.
  • \n
  • exclude_fids — FIDs to drop from the computed recipient set after filtering.
  • \n
  • following_fid — if set, only deliver to FIDs that follow this FID (useful for \"notify your followers when you post\").
  • \n
  • minimum_user_score — accepted for forward compatibility. No local user-score signal exists yet; Hypersnap parses but does not enforce this field.
  • \n
  • near_location — accepted for forward compatibility. Not enforced.
  • \n
\n

Response #

\n
{\n  \"campaign_id\":         \"uuid\",\n  \"success_count\":       42,\n  \"failure_count\":       1,\n  \"not_attempted_count\": 0,\n  \"retryable_fids\":      [12345]\n}\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldMeaning
campaign_idServer-assigned UUID for this send. Log it alongside your internal request id so you can trace deliveries.
success_countTokens the downstream Farcaster client confirmed success on.
failure_countTokens that returned failure (invalid / rate-limited).
not_attempted_countTokens that were deduped out by (fid, notificationId) dedupe or filtered by exclude_fids / following_fid.
retryable_fidsFIDs whose tokens came back as rateLimitedTokens from the client. Safe to retry later.
\n

What Hypersnap does on the wire #

\n
    \n
  1. Looks up the mini app by <app_id>.
  2. \n
  3. Resolves the recipient FIDs (explicit target_fids or all enabled tokens) and applies exclude_fids / following_fid filters.
  4. \n
  5. Dedupes (fid, notificationId) pairs against the 24-hour LRU.
  6. \n
  7. Groups remaining tokens by their notification_url (each Farcaster client has its own URL).
  8. \n
  9. POSTs to each URL in batches of ≤ 100 tokens, in parallel up to send_concurrency.
  10. \n
  11. Aggregates the per-client responses into the SendNotificationResult above.
  12. \n
\n

The per-client POST body is the Mini App spec contract:

\n
{\n  \"notificationId\": \"string (≤128)\",\n  \"title\":          \"string (≤32)\",\n  \"body\":           \"string (≤128)\",\n  \"targetUrl\":      \"string (≤1024, same domain)\",\n  \"tokens\":         [\"token-a\", \"token-b\"]\n}\n
\n

And the client response is interpreted as:

\n
    \n
  • successfulTokens → counted in success_count.
  • \n
  • invalidTokens → permanently deleted from Hypersnap's token store.
  • \n
  • rateLimitedTokens → the owning FIDs are returned in retryable_fids.
  • \n
\n

Rate limits (per spec) #

\n
    \n
  • Per token: 1 notification per 30 seconds, 100 per day. Enforced by the Farcaster client, surfaced via rate_limited_tokens.
  • \n
  • (fid, notificationId) dedupe: 24 hours. Enforced by Hypersnap.
  • \n
\n

Try it #

\n
\n

Examples #

\n

Send to one user #

\n
curl -X POST \\\n  -H \"x-api-key: $SEND_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"notification\": {\n      \"title\": \"You got a reply\",\n      \"body\":  \"alice: loved your take on this\",\n      \"target_url\": \"https://miniapp.example.com/thread/0xabc\",\n      \"uuid\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n    },\n    \"target_fids\": [12345]\n  }' \\\n  https://haatz.quilibrium.com/v2/farcaster/frame/notifications/3Hq9ZgK2p4vNfWxR\n
\n

Broadcast to everyone who has enabled notifications #

\n
curl -X POST \\\n  -H \"x-api-key: $SEND_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"notification\": {\n      \"title\": \"New season drop\",\n      \"body\":  \"Season 5 just opened — tap to play\",\n      \"target_url\": \"https://miniapp.example.com/season/5\",\n      \"uuid\": \"b2f63f8a-...\"\n    },\n    \"target_fids\": []\n  }' \\\n  https://haatz.quilibrium.com/v2/farcaster/frame/notifications/3Hq9ZgK2p4vNfWxR\n
\n

Handle retryable FIDs #

\n

If the response comes back with retryable_fids: [12345], schedule a retry 30+ seconds later with a fresh uuid (or keep the same one — the dedupe key (fid, notificationId) guarantees the same logical notification still won't arrive twice).

", + "contentHtml": "

Sending notifications #

\n
POST /v2/farcaster/frame/notifications/<app_id>\n
\n

This is the endpoint you call from your backend when you want to push a notification to your mini app's users.

\n

Auth: x-api-key: <send_secret>. The value must match the most recently created unexpired entry in your app's send_secrets array. See Registering a mini app for how to obtain and rotate the secret.

\n

Request body #

\n
{\n  \"notification\": {\n    \"title\":      \"string (≤32 chars)\",\n    \"body\":       \"string (≤128 chars)\",\n    \"target_url\": \"https://miniapp.example.com/path/in/app\",\n    \"uuid\":       \"optional UUID string\"\n  },\n  \"target_fids\":   [12345, 67890],\n  \"exclude_fids\":  [],\n  \"following_fid\": null,\n  \"minimum_user_score\": null,\n  \"near_location\":     null\n}\n
\n

notification #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldTypeRequiredNotes
titlestringyes≤ 32 characters (spec limit).
bodystringyes≤ 128 characters (spec limit).
target_urlstringyes≤ 256 characters. Must be the same domain as your mini-app's app_url.
uuidstringnoA UUID you supply. Becomes the notificationId on the per-client fan-out. Enables (fid, notificationId) dedupe within the 24h window. Generate a random UUID per logical notification.
\n

Targeting #

\n
    \n
  • target_fids — explicit list of FIDs to deliver to. Empty = all enabled FIDs for this app.
  • \n
  • exclude_fids — FIDs to drop from the computed recipient set after filtering.
  • \n
  • following_fid — if set, only deliver to FIDs that follow this FID (useful for \"notify your followers when you post\").
  • \n
  • minimum_user_score — accepted for forward compatibility. No local user-score signal exists yet; Hypersnap parses but does not enforce this field.
  • \n
  • near_location — accepted for forward compatibility. Not enforced.
  • \n
\n

Response #

\n
{\n  \"campaign_id\":         \"uuid\",\n  \"success_count\":       42,\n  \"failure_count\":       1,\n  \"not_attempted_count\": 0,\n  \"retryable_fids\":      [12345]\n}\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FieldMeaning
campaign_idServer-assigned UUID for this send. Log it alongside your internal request id so you can trace deliveries.
success_countTokens the downstream Farcaster client confirmed success on.
failure_countTokens that returned failure (invalid / rate-limited).
not_attempted_countTokens that were deduped out by (fid, notificationId) dedupe or filtered by exclude_fids / following_fid.
retryable_fidsFIDs whose tokens came back as rateLimitedTokens from the client. Safe to retry later.
\n

What Hypersnap does on the wire #

\n
    \n
  1. Looks up the mini app by <app_id>.
  2. \n
  3. Resolves the recipient FIDs (explicit target_fids or all enabled tokens) and applies exclude_fids / following_fid filters.
  4. \n
  5. Dedupes (fid, notificationId) pairs against the 24-hour LRU.
  6. \n
  7. Groups remaining tokens by their notification_url (each Farcaster client has its own URL).
  8. \n
  9. POSTs to each URL in batches of ≤ 100 tokens, in parallel up to send_concurrency.
  10. \n
  11. Aggregates the per-client responses into the SendNotificationResult above.
  12. \n
\n

The per-client POST body is the Mini App spec contract:

\n
{\n  \"notificationId\": \"string (≤128)\",\n  \"title\":          \"string (≤32)\",\n  \"body\":           \"string (≤128)\",\n  \"targetUrl\":      \"string (≤1024, same domain)\",\n  \"tokens\":         [\"token-a\", \"token-b\"]\n}\n
\n

And the client response is interpreted as:

\n
    \n
  • successfulTokens → counted in success_count.
  • \n
  • invalidTokens → permanently deleted from Hypersnap's token store.
  • \n
  • rateLimitedTokens → the owning FIDs are returned in retryable_fids.
  • \n
\n

Rate limits (per spec) #

\n
    \n
  • Per token: 1 notification per 30 seconds, 100 per day. Enforced by the Farcaster client, surfaced via rate_limited_tokens.
  • \n
  • (fid, notificationId) dedupe: 24 hours. Enforced by Hypersnap.
  • \n
\n

Try it #

\n
\n

Examples #

\n

Send to one user #

\n
curl -X POST \\\n  -H \"x-api-key: $SEND_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"notification\": {\n      \"title\": \"You got a reply\",\n      \"body\":  \"alice: loved your take on this\",\n      \"target_url\": \"https://miniapp.example.com/thread/0xabc\",\n      \"uuid\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n    },\n    \"target_fids\": [12345]\n  }' \\\n  https://haatz.quilibrium.com/v2/farcaster/frame/notifications/3Hq9ZgK2p4vNfWxR\n
\n

Broadcast to everyone who has enabled notifications #

\n
curl -X POST \\\n  -H \"x-api-key: $SEND_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"notification\": {\n      \"title\": \"New season drop\",\n      \"body\":  \"Season 5 just opened — tap to play\",\n      \"target_url\": \"https://miniapp.example.com/season/5\",\n      \"uuid\": \"b2f63f8a-...\"\n    },\n    \"target_fids\": []\n  }' \\\n  https://haatz.quilibrium.com/v2/farcaster/frame/notifications/3Hq9ZgK2p4vNfWxR\n
\n

Handle retryable FIDs #

\n

If the response comes back with retryable_fids: [12345], schedule a retry 30+ seconds later with a fresh uuid (or keep the same one — the dedupe key (fid, notificationId) guarantees the same logical notification still won't arrive twice).

", "contentMarkdown": "# Sending notifications\n\n```\nPOST /v2/farcaster/frame/notifications/\n```\n\nThis is the endpoint you call from your backend when you want to push a notification to your mini app's users.\n\n**Auth:** `x-api-key: `. The value must match the most recently created unexpired entry in your app's `send_secrets` array. See [Registering a mini app](./registering.md) for how to obtain and rotate the secret.\n\n## Request body\n\n```json\n{\n \"notification\": {\n \"title\": \"string (≤32 chars)\",\n \"body\": \"string (≤128 chars)\",\n \"target_url\": \"https://miniapp.example.com/path/in/app\",\n \"uuid\": \"optional UUID string\"\n },\n \"target_fids\": [12345, 67890],\n \"exclude_fids\": [],\n \"following_fid\": null,\n \"minimum_user_score\": null,\n \"near_location\": null\n}\n```\n\n### notification\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `title` | string | yes | ≤ 32 characters (spec limit). |\n| `body` | string | yes | ≤ 128 characters (spec limit). |\n| `target_url` | string | yes | ≤ 256 characters. Must be the same domain as your mini-app's `app_url`. |\n| `uuid` | string | no | A UUID you supply. Becomes the `notificationId` on the per-client fan-out. Enables `(fid, notificationId)` dedupe within the 24h window. Generate a random UUID per logical notification. |\n\n### Targeting\n\n- `target_fids` — explicit list of FIDs to deliver to. Empty = all enabled FIDs for this app.\n- `exclude_fids` — FIDs to drop from the computed recipient set after filtering.\n- `following_fid` — if set, only deliver to FIDs that follow this FID (useful for \"notify your followers when you post\").\n- `minimum_user_score` — accepted for forward compatibility. No local user-score signal exists yet; Hypersnap parses but does not enforce this field.\n- `near_location` — accepted for forward compatibility. Not enforced.\n\n## Response\n\n```json\n{\n \"campaign_id\": \"uuid\",\n \"success_count\": 42,\n \"failure_count\": 1,\n \"not_attempted_count\": 0,\n \"retryable_fids\": [12345]\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `campaign_id` | Server-assigned UUID for this send. Log it alongside your internal request id so you can trace deliveries. |\n| `success_count` | Tokens the downstream Farcaster client confirmed success on. |\n| `failure_count` | Tokens that returned failure (invalid / rate-limited). |\n| `not_attempted_count` | Tokens that were deduped out by `(fid, notificationId)` dedupe or filtered by `exclude_fids` / `following_fid`. |\n| `retryable_fids` | FIDs whose tokens came back as `rateLimitedTokens` from the client. Safe to retry later. |\n\n## What Hypersnap does on the wire\n\n1. Looks up the mini app by ``.\n2. Resolves the recipient FIDs (explicit `target_fids` or all enabled tokens) and applies `exclude_fids` / `following_fid` filters.\n3. Dedupes `(fid, notificationId)` pairs against the 24-hour LRU.\n4. Groups remaining tokens by their `notification_url` (each Farcaster client has its own URL).\n5. POSTs to each URL in batches of ≤ 100 tokens, in parallel up to `send_concurrency`.\n6. Aggregates the per-client responses into the `SendNotificationResult` above.\n\nThe per-client POST body is the Mini App spec contract:\n\n```json\n{\n \"notificationId\": \"string (≤128)\",\n \"title\": \"string (≤32)\",\n \"body\": \"string (≤128)\",\n \"targetUrl\": \"string (≤1024, same domain)\",\n \"tokens\": [\"token-a\", \"token-b\"]\n}\n```\n\nAnd the client response is interpreted as:\n\n- `successfulTokens` → counted in `success_count`.\n- `invalidTokens` → permanently deleted from Hypersnap's token store.\n- `rateLimitedTokens` → the owning FIDs are returned in `retryable_fids`.\n\n## Rate limits (per spec)\n\n- **Per token:** 1 notification per 30 seconds, 100 per day. Enforced by the Farcaster client, surfaced via `rate_limited_tokens`.\n- **`(fid, notificationId)` dedupe:** 24 hours. Enforced by Hypersnap.\n\n## Try it\n\n
\n\n## Examples\n\n### Send to one user\n\n```bash\ncurl -X POST \\\n -H \"x-api-key: $SEND_SECRET\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"notification\": {\n \"title\": \"You got a reply\",\n \"body\": \"alice: loved your take on this\",\n \"target_url\": \"https://miniapp.example.com/thread/0xabc\",\n \"uuid\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n },\n \"target_fids\": [12345]\n }' \\\n https://haatz.quilibrium.com/v2/farcaster/frame/notifications/3Hq9ZgK2p4vNfWxR\n```\n\n### Broadcast to everyone who has enabled notifications\n\n```bash\ncurl -X POST \\\n -H \"x-api-key: $SEND_SECRET\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"notification\": {\n \"title\": \"New season drop\",\n \"body\": \"Season 5 just opened — tap to play\",\n \"target_url\": \"https://miniapp.example.com/season/5\",\n \"uuid\": \"b2f63f8a-...\"\n },\n \"target_fids\": []\n }' \\\n https://haatz.quilibrium.com/v2/farcaster/frame/notifications/3Hq9ZgK2p4vNfWxR\n```\n\n### Handle retryable FIDs\n\nIf the response comes back with `retryable_fids: [12345]`, schedule a retry 30+ seconds later with a fresh `uuid` (or keep the same one — the dedupe key `(fid, notificationId)` guarantees the same logical notification still won't arrive twice).\n", "toc": [ { @@ -1808,7 +1808,7 @@ "sourcePath": "src/guides/build-an-agent.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/guides/build-an-agent.md", "slug": "guides/build-an-agent", - "contentHtml": "

Build an agent #

\n

An \"agent\" for the purposes of this guide is anything that reacts to Farcaster activity in realtime — a bot that replies to mentions, a moderator that watches a channel for abuse, an analytics pipeline that indexes a slice of the firehose, a notification service that tells Discord when someone mentions your DAO.

\n

The core primitive is a webhook subscription.

\n

1. Decide what you want to hear about #

\n

Write out what events your agent needs, as plain English:

\n
\n

\"I want to know about every cast that mentions FID 12345, and every reply to any cast authored by FID 12345.\"

\n
\n

Translate to a WebhookSubscription:

\n
{\n  \"cast_created\": {\n    \"mentioned_fids\": [12345],\n    \"parent_author_fids\": [12345]\n  }\n}\n
\n

Gotcha: fields inside a filter AND together. The filter above matches casts that both mention and reply to you. To OR the two conditions, register two webhooks, or register one very broad webhook and filter in your receiver.

\n

See Subscription filters for every available field.

\n

2. Stand up a receiver #

\n

Your receiver is a public HTTPS endpoint that accepts POST with a JSON body. Minimal Node.js example:

\n
import express from \"express\";\nimport crypto  from \"crypto\";\n\nconst app = express();\n\n// Preserve raw bytes for HMAC verification.\napp.use(express.raw({ type: \"application/json\", limit: \"1mb\" }));\n\nconst SECRET = process.env.WEBHOOK_SECRET;\n\napp.post(\"/hook\", (req, res) => {\n  const received = req.header(\"x-hypersnap-signature\");\n  const expected = crypto\n    .createHmac(\"sha512\", SECRET)\n    .update(req.body) // raw bytes\n    .digest(\"hex\");\n\n  if (!crypto.timingSafeEqual(Buffer.from(received, \"hex\"), Buffer.from(expected, \"hex\"))) {\n    return res.status(401).end();\n  }\n\n  const event = JSON.parse(req.body.toString(\"utf8\"));\n  // Durably enqueue before ACKing.\n  void enqueue(event);\n  res.status(200).end();\n});\n\napp.listen(8080);\n
\n

The crucial pieces:

\n
    \n
  • Raw bytes for HMAC. Verify before JSON.parse. If you re-stringify the body, the hash won't match.
  • \n
  • Enqueue, then ACK. Put the event on your own durable queue, then return 200. Don't hold the connection open for downstream work.
  • \n
  • Return 5xx on enqueue failure so Hypersnap retries. Return 4xx only if the message is malformed in a way that will never succeed.
  • \n
\n

See Receive webhooks for the deep version of this.

\n

3. Register the webhook #

\n

Sign an EIP-712 request as your FID's custody key:

\n
import { ethers } from \"ethers\";\nimport { randomBytes } from \"crypto\";\n\nconst wallet = new ethers.Wallet(process.env.CUSTODY_KEY);\n\nconst body = JSON.stringify({\n  name: \"my agent\",\n  url:  \"https://receiver.example.com/hook\",\n  subscription: {\n    cast_created: { mentioned_fids: [12345] },\n  },\n});\n\nconst signedAt = Math.floor(Date.now() / 1000);\nconst nonce    = \"0x\" + randomBytes(32).toString(\"hex\");\nconst reqHash  = ethers.keccak256(ethers.toUtf8Bytes(body));\n\nconst sig = await wallet.signTypedData(\n  { name: \"Hypersnap\", version: \"1\", chainId: 10 },\n  { HypersnapSignedOp: [\n      { name: \"op\",          type: \"string\"  },\n      { name: \"fid\",         type: \"uint64\"  },\n      { name: \"signedAt\",    type: \"uint256\" },\n      { name: \"nonce\",       type: \"bytes32\" },\n      { name: \"requestHash\", type: \"bytes32\" },\n  ]},\n  { op: \"webhook.create\", fid: 12345n, signedAt: BigInt(signedAt), nonce, requestHash: reqHash },\n);\n\nconst resp = await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\":          \"application/json\",\n    \"X-Hypersnap-Fid\":       \"12345\",\n    \"X-Hypersnap-Op\":        \"webhook.create\",\n    \"X-Hypersnap-Signed-At\": String(signedAt),\n    \"X-Hypersnap-Nonce\":     nonce,\n    \"X-Hypersnap-Signature\": sig,\n  },\n  body,\n});\n\nconst { webhook } = await resp.json();\nconsole.log(\"Save this signing secret:\", webhook.secrets[0].value);\n
\n

See Sign an EIP-712 request for Python and Rust.

\n

4. Build your reaction logic #

\n

Inside your enqueue path, process the event. Events have a typed schema — see Event schemas.

\n
async function process(event) {\n  if (event.type === \"cast.created\") {\n    const cast = event.data.cast;\n    if (cast.text.includes(\"@myagent\")) {\n      await reply(cast.hash, \"hi, I'm a bot!\");\n    }\n  }\n}\n
\n

5. Dedupe #

\n

Hypersnap delivers events at-least-once. Retries or network races can send the same logical event twice. Dedupe on the natural key:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EventDedupe key
cast.created / cast.deleteddata.cast.hash
follow.*(follower.fid, target.fid)
reaction.*(user.fid, cast.hash, reaction_type)
\n

A cheap implementation: a Redis SET with a 1-hour TTL.

\n

6. Rotate secrets #

\n

If your signing secret leaks, rotate:

\n
await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/secret/rotate?webhook_id=\" + id, {\n  method: \"POST\",\n  headers: eip712Headers(\"webhook.rotate_secret\", \"\" /* empty body */),\n});\n
\n

Response contains the new secrets array. During the grace window, deliveries might be signed with either the old or new key, so maintain a set of accepted secrets on your receiver and drop the old one when its expires_at passes.

\n

7. When your agent needs to call reads too #

\n

Realtime events tell you that something happened. If you need context (who is this author? what's the parent of this cast? does this user follow me?), use the Read API from the same process.

\n

Operational notes #

\n
    \n
  • Observability. Log (event.type, dedupe_key, attempt_count) for every delivery. When things go wrong, you want to see whether an event was never delivered, delivered but failed locally, or delivered many times due to retries.
  • \n
  • Backpressure. If your processing is slow, 1000 events/minute (the default per-webhook rate limit) will still build up a backlog. Put a queue between the HTTP handler and your processor.
  • \n
  • Cold starts. When you first register a webhook, events start flowing within seconds. You do not get historical backfill — earlier events are not replayed. If you need history, do a one-time walk over the read API to seed state.
  • \n
", + "contentHtml": "

Build an agent #

\n

An \"agent\" for the purposes of this guide is anything that reacts to Farcaster activity in realtime — a bot that replies to mentions, a moderator that watches a channel for abuse, an analytics pipeline that indexes a slice of the firehose, a notification service that tells Discord when someone mentions your DAO.

\n

The core primitive is a webhook subscription.

\n

1. Decide what you want to hear about #

\n

Write out what events your agent needs, as plain English:

\n
\n

\"I want to know about every cast that mentions FID 12345, and every reply to any cast authored by FID 12345.\"

\n
\n

Translate to a WebhookSubscription:

\n
{\n  \"cast_created\": {\n    \"mentioned_fids\": [12345],\n    \"parent_author_fids\": [12345]\n  }\n}\n
\n

Gotcha: fields inside a filter AND together. The filter above matches casts that both mention and reply to you. To OR the two conditions, register two webhooks, or register one very broad webhook and filter in your receiver.

\n

See Subscription filters for every available field.

\n

2. Stand up a receiver #

\n

Your receiver is a public HTTPS endpoint that accepts POST with a JSON body. Minimal Node.js example:

\n
import express from \"express\";\nimport crypto  from \"crypto\";\n\nconst app = express();\n\n// Preserve raw bytes for HMAC verification.\napp.use(express.raw({ type: \"application/json\", limit: \"1mb\" }));\n\nconst SECRET = process.env.WEBHOOK_SECRET;\n\napp.post(\"/hook\", (req, res) => {\n  const received = req.header(\"x-hypersnap-signature\");\n  const expected = crypto\n    .createHmac(\"sha512\", SECRET)\n    .update(req.body) // raw bytes\n    .digest(\"hex\");\n\n  if (!crypto.timingSafeEqual(Buffer.from(received, \"hex\"), Buffer.from(expected, \"hex\"))) {\n    return res.status(401).end();\n  }\n\n  const event = JSON.parse(req.body.toString(\"utf8\"));\n  // Durably enqueue before ACKing.\n  void enqueue(event);\n  res.status(200).end();\n});\n\napp.listen(8080);\n
\n

The crucial pieces:

\n
    \n
  • Raw bytes for HMAC. Verify before JSON.parse. If you re-stringify the body, the hash won't match.
  • \n
  • Enqueue, then ACK. Put the event on your own durable queue, then return 200. Don't hold the connection open for downstream work.
  • \n
  • Return 5xx on enqueue failure so Hypersnap retries. Return 4xx only if the message is malformed in a way that will never succeed.
  • \n
\n

See Receive webhooks for the deep version of this.

\n

3. Register the webhook #

\n

Sign an EIP-712 request as your FID's custody key:

\n
import { ethers } from \"ethers\";\nimport { randomBytes } from \"crypto\";\n\nconst wallet = new ethers.Wallet(process.env.CUSTODY_KEY);\n\nconst body = JSON.stringify({\n  name: \"my agent\",\n  url:  \"https://receiver.example.com/hook\",\n  subscription: {\n    cast_created: { mentioned_fids: [12345] },\n  },\n});\n\nconst signedAt = Math.floor(Date.now() / 1000);\nconst nonce    = \"0x\" + randomBytes(32).toString(\"hex\");\nconst reqHash  = ethers.keccak256(ethers.toUtf8Bytes(body));\n\nconst sig = await wallet.signTypedData(\n  { name: \"Hypersnap\", version: \"1\", chainId: 10 },\n  { HypersnapSignedOp: [\n      { name: \"op\",          type: \"string\"  },\n      { name: \"fid\",         type: \"uint64\"  },\n      { name: \"signedAt\",    type: \"uint256\" },\n      { name: \"nonce\",       type: \"bytes32\" },\n      { name: \"requestHash\", type: \"bytes32\" },\n  ]},\n  { op: \"webhook.create\", fid: 12345n, signedAt: BigInt(signedAt), nonce, requestHash: reqHash },\n);\n\nconst resp = await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\":          \"application/json\",\n    \"X-Hypersnap-Fid\":       \"12345\",\n    \"X-Hypersnap-Op\":        \"webhook.create\",\n    \"X-Hypersnap-Signed-At\": String(signedAt),\n    \"X-Hypersnap-Nonce\":     nonce,\n    \"X-Hypersnap-Signature\": sig,\n  },\n  body,\n});\n\nconst { webhook } = await resp.json();\nconsole.log(\"Save this signing secret:\", webhook.secrets[0].value);\n
\n

See Sign an EIP-712 request for Python and Rust.

\n

4. Build your reaction logic #

\n

Inside your enqueue path, process the event. Events have a typed schema — see Event schemas.

\n
async function process(event) {\n  if (event.type === \"cast.created\") {\n    const cast = event.data.cast;\n    if (cast.text.includes(\"@myagent\")) {\n      await reply(cast.hash, \"hi, I'm a bot!\");\n    }\n  }\n}\n
\n

5. Dedupe #

\n

Hypersnap delivers events at-least-once. Retries or network races can send the same logical event twice. Dedupe on the natural key:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
EventDedupe key
cast.created / cast.deleteddata.cast.hash
follow.*(follower.fid, target.fid)
reaction.*(user.fid, cast.hash, reaction_type)
\n

A cheap implementation: a Redis SET with a 1-hour TTL.

\n

6. Rotate secrets #

\n

If your signing secret leaks, rotate:

\n
await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/secret/rotate?webhook_id=\" + id, {\n  method: \"POST\",\n  headers: eip712Headers(\"webhook.rotate_secret\", \"\" /* empty body */),\n});\n
\n

Response contains the new secrets array. During the grace window, deliveries might be signed with either the old or new key, so maintain a set of accepted secrets on your receiver and drop the old one when its expires_at passes.

\n

7. When your agent needs to call reads too #

\n

Realtime events tell you that something happened. If you need context (who is this author? what's the parent of this cast? does this user follow me?), use the Read API from the same process.

\n

Operational notes #

\n
    \n
  • Observability. Log (event.type, dedupe_key, attempt_count) for every delivery. When things go wrong, you want to see whether an event was never delivered, delivered but failed locally, or delivered many times due to retries.
  • \n
  • Backpressure. If your processing is slow, 1000 events/minute (the default per-webhook rate limit) will still build up a backlog. Put a queue between the HTTP handler and your processor.
  • \n
  • Cold starts. When you first register a webhook, events start flowing within seconds. You do not get historical backfill — earlier events are not replayed. If you need history, do a one-time walk over the read API to seed state.
  • \n
", "contentMarkdown": "# Build an agent\n\nAn \"agent\" for the purposes of this guide is anything that reacts to Farcaster activity in realtime — a bot that replies to mentions, a moderator that watches a channel for abuse, an analytics pipeline that indexes a slice of the firehose, a notification service that tells Discord when someone mentions your DAO.\n\nThe core primitive is a **webhook subscription**.\n\n## 1. Decide what you want to hear about\n\nWrite out what events your agent needs, as plain English:\n\n> \"I want to know about every cast that mentions FID 12345, and every reply to any cast authored by FID 12345.\"\n\nTranslate to a `WebhookSubscription`:\n\n```json\n{\n \"cast_created\": {\n \"mentioned_fids\": [12345],\n \"parent_author_fids\": [12345]\n }\n}\n```\n\n**Gotcha:** fields inside a filter AND together. The filter above matches casts that both mention *and* reply to you. To OR the two conditions, register two webhooks, or register one very broad webhook and filter in your receiver.\n\nSee [Subscription filters](../reference/webhooks/filters.md) for every available field.\n\n## 2. Stand up a receiver\n\nYour receiver is a public HTTPS endpoint that accepts `POST` with a JSON body. Minimal Node.js example:\n\n```javascript\nimport express from \"express\";\nimport crypto from \"crypto\";\n\nconst app = express();\n\n// Preserve raw bytes for HMAC verification.\napp.use(express.raw({ type: \"application/json\", limit: \"1mb\" }));\n\nconst SECRET = process.env.WEBHOOK_SECRET;\n\napp.post(\"/hook\", (req, res) => {\n const received = req.header(\"x-hypersnap-signature\");\n const expected = crypto\n .createHmac(\"sha512\", SECRET)\n .update(req.body) // raw bytes\n .digest(\"hex\");\n\n if (!crypto.timingSafeEqual(Buffer.from(received, \"hex\"), Buffer.from(expected, \"hex\"))) {\n return res.status(401).end();\n }\n\n const event = JSON.parse(req.body.toString(\"utf8\"));\n // Durably enqueue before ACKing.\n void enqueue(event);\n res.status(200).end();\n});\n\napp.listen(8080);\n```\n\nThe crucial pieces:\n\n- **Raw bytes for HMAC.** Verify *before* `JSON.parse`. If you re-stringify the body, the hash won't match.\n- **Enqueue, then ACK.** Put the event on your own durable queue, then return `200`. Don't hold the connection open for downstream work.\n- **Return `5xx` on enqueue failure** so Hypersnap retries. Return `4xx` only if the message is malformed in a way that will never succeed.\n\nSee [Receive webhooks](./receive-webhooks.md) for the deep version of this.\n\n## 3. Register the webhook\n\nSign an EIP-712 request as your FID's custody key:\n\n```javascript\nimport { ethers } from \"ethers\";\nimport { randomBytes } from \"crypto\";\n\nconst wallet = new ethers.Wallet(process.env.CUSTODY_KEY);\n\nconst body = JSON.stringify({\n name: \"my agent\",\n url: \"https://receiver.example.com/hook\",\n subscription: {\n cast_created: { mentioned_fids: [12345] },\n },\n});\n\nconst signedAt = Math.floor(Date.now() / 1000);\nconst nonce = \"0x\" + randomBytes(32).toString(\"hex\");\nconst reqHash = ethers.keccak256(ethers.toUtf8Bytes(body));\n\nconst sig = await wallet.signTypedData(\n { name: \"Hypersnap\", version: \"1\", chainId: 10 },\n { HypersnapSignedOp: [\n { name: \"op\", type: \"string\" },\n { name: \"fid\", type: \"uint64\" },\n { name: \"signedAt\", type: \"uint256\" },\n { name: \"nonce\", type: \"bytes32\" },\n { name: \"requestHash\", type: \"bytes32\" },\n ]},\n { op: \"webhook.create\", fid: 12345n, signedAt: BigInt(signedAt), nonce, requestHash: reqHash },\n);\n\nconst resp = await fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Hypersnap-Fid\": \"12345\",\n \"X-Hypersnap-Op\": \"webhook.create\",\n \"X-Hypersnap-Signed-At\": String(signedAt),\n \"X-Hypersnap-Nonce\": nonce,\n \"X-Hypersnap-Signature\": sig,\n },\n body,\n});\n\nconst { webhook } = await resp.json();\nconsole.log(\"Save this signing secret:\", webhook.secrets[0].value);\n```\n\nSee [Sign an EIP-712 request](./sign-eip712.md) for Python and Rust.\n\n## 4. Build your reaction logic\n\nInside your enqueue path, process the event. Events have a typed schema — see [Event schemas](../reference/webhooks/events.md).\n\n```javascript\nasync function process(event) {\n if (event.type === \"cast.created\") {\n const cast = event.data.cast;\n if (cast.text.includes(\"@myagent\")) {\n await reply(cast.hash, \"hi, I'm a bot!\");\n }\n }\n}\n```\n\n## 5. Dedupe\n\nHypersnap delivers events **at-least-once**. Retries or network races can send the same logical event twice. Dedupe on the natural key:\n\n| Event | Dedupe key |\n|---|---|\n| `cast.created` / `cast.deleted` | `data.cast.hash` |\n| `follow.*` | `(follower.fid, target.fid)` |\n| `reaction.*` | `(user.fid, cast.hash, reaction_type)` |\n\nA cheap implementation: a Redis SET with a 1-hour TTL.\n\n## 6. Rotate secrets\n\nIf your signing secret leaks, rotate:\n\n```javascript\nawait fetch(\"https://haatz.quilibrium.com/v2/farcaster/webhook/secret/rotate?webhook_id=\" + id, {\n method: \"POST\",\n headers: eip712Headers(\"webhook.rotate_secret\", \"\" /* empty body */),\n});\n```\n\nResponse contains the new secrets array. During the grace window, deliveries might be signed with either the old or new key, so maintain a set of accepted secrets on your receiver and drop the old one when its `expires_at` passes.\n\n## 7. When your agent needs to call reads too\n\nRealtime events tell you *that* something happened. If you need context (who is this author? what's the parent of this cast? does this user follow me?), use the [Read API](../reference/reads/index.md) from the same process.\n\n## Operational notes\n\n- **Observability.** Log `(event.type, dedupe_key, attempt_count)` for every delivery. When things go wrong, you want to see whether an event was never delivered, delivered but failed locally, or delivered many times due to retries.\n- **Backpressure.** If your processing is slow, 1000 events/minute (the default per-webhook rate limit) will still build up a backlog. Put a queue between the HTTP handler and your processor.\n- **Cold starts.** When you first register a webhook, events start flowing within seconds. You do not get historical backfill — earlier events are not replayed. If you need history, do a one-time walk over the read API to seed state.\n", "toc": [ { @@ -1910,7 +1910,7 @@ "sourcePath": "src/guides/receive-webhooks.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/guides/receive-webhooks.md", "slug": "guides/receive-webhooks", - "contentHtml": "

Receive webhooks #

\n

A deep-dive on running a production webhook receiver for Hypersnap. Covers signature verification, rotation, dedupe, retries, and backpressure.

\n

Minimum viable receiver #

\n
import express from \"express\";\nimport crypto  from \"crypto\";\n\nconst app = express();\napp.use(express.raw({ type: \"application/json\", limit: \"1mb\" }));\n\nconst SECRETS = new Set([process.env.WEBHOOK_SECRET]);\n\napp.post(\"/hook\", (req, res) => {\n  const receivedHex = req.header(\"x-hypersnap-signature\");\n  if (!verifyAny(req.body, receivedHex, SECRETS)) {\n    return res.status(401).end();\n  }\n  const event = JSON.parse(req.body.toString(\"utf8\"));\n  void handle(event);\n  res.status(200).end();\n});\n\nfunction verifyAny(body, receivedHex, secrets) {\n  const recv = Buffer.from(receivedHex, \"hex\");\n  for (const s of secrets) {\n    const exp = crypto.createHmac(\"sha512\", s).update(body).digest();\n    if (recv.length === exp.length && crypto.timingSafeEqual(recv, exp)) {\n      return true;\n    }\n  }\n  return false;\n}\n\napp.listen(8080);\n
\n

The rest of this guide is what you do on top of this scaffold to make it robust.

\n

Signature verification #

\n

Rule one: verify before parsing. HMAC is computed over the raw bytes. If you parse JSON and re-serialize it, the bytes change (whitespace, key ordering) and your hash won't match the one in the header.

\n

Express: use express.raw. Koa/fastify: look up the raw-body option on your framework. Lambda: use the isBase64Encoded + body as-received.

\n

Rule two: use constant-time comparison. Never == on hex strings. Use crypto.timingSafeEqual / hmac.compare_digest / subtle::ConstantTimeEq.

\n

Rule three: accept multiple secrets during rotation. See below.

\n

Handling secret rotation #

\n

When you rotate a webhook's secret, Hypersnap keeps the old one valid for a 24-hour grace window (configurable by your node operator). During that window, deliveries can be signed with either secret depending on timing. Your receiver needs to:

\n
    \n
  1. Maintain an accepted-secrets set. Start with the single active secret.
  2. \n
  3. When you rotate, add the new one to the set.
  4. \n
  5. Try every secret when verifying, return 401 only if none match.
  6. \n
  7. Drop the old secret from the set once its expires_at has passed.
  8. \n
\n

In code:

\n
const SECRETS = new Map(); // secret_value -> expires_at (null = active, never expires)\n\nasync function refreshSecrets() {\n  const resp = await signedFetch(\"GET\", \"/v2/farcaster/webhook/?webhook_id=\" + ID);\n  const webhook = (await resp.json()).webhook;\n  const now = Math.floor(Date.now() / 1000);\n  SECRETS.clear();\n  for (const s of webhook.secrets) {\n    if (s.expires_at === null || s.expires_at > now) {\n      SECRETS.set(s.value, s.expires_at);\n    }\n  }\n}\n\n// Refresh on boot and every few hours.\nawait refreshSecrets();\nsetInterval(refreshSecrets, 60 * 60 * 1000);\n
\n

Dedupe #

\n

Deliveries are at-least-once. Retries, transient failures, and network races can replay the same logical event. You need an idempotency layer.

\n

Cheap version — Redis:

\n
async function handle(event) {\n  const key = dedupeKey(event);\n  const first = await redis.set(`wh:${key}`, \"1\", \"EX\", 3600, \"NX\");\n  if (!first) return; // already processed\n  await process(event);\n}\n\nfunction dedupeKey(event) {\n  switch (event.type) {\n    case \"cast.created\":\n    case \"cast.deleted\":\n      return `c:${event.data.cast.hash}`;\n    case \"follow.created\":\n    case \"follow.deleted\":\n      return `f:${event.data.follower.fid}:${event.data.target.fid}`;\n    case \"reaction.created\":\n    case \"reaction.deleted\":\n      return `r:${event.data.user.fid}:${event.data.cast.hash}:${event.data.reaction_type}`;\n    default:\n      return `x:${JSON.stringify(event.data)}`;\n  }\n}\n
\n

A 1-hour TTL is enough for retries (the retry queue times out well before that).

\n

Return codes #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
You returnHypersnap's behavior
2xxDelivered.
4xxTerminal failure. No retries.
5xxRetried up to retry_max_attempts (default 5) with exponential backoff.
Timeout / connection errorRetried as above.
\n

Return 2xx as fast as possible. Enqueue the event to your local processing queue and ACK. Don't do downstream work on the HTTP handler's stack — you'll eat into the 10-second delivery_timeout_secs default and cause timeouts.

\n

Backpressure #

\n

At the default default_rate_limit = 1000 events / 60 seconds, you can get a sustained load of ~17 events/sec per webhook. That's small, but peaky — a hot thread can burst higher than average.

\n
    \n
  • Put a queue between the HTTP handler and your processor.
  • \n
  • Monitor queue depth.
  • \n
  • If the queue is backing up, your receiver should start returning 5xx so Hypersnap requeues instead of dropping events.
  • \n
\n

Observability #

\n

Log every delivery with:

\n
    \n
  • event.type
  • \n
  • Dedupe key
  • \n
  • Whether it was a first-sight or dedupe hit
  • \n
  • Processing outcome
  • \n
  • HTTP status returned
  • \n
\n

With those fields you can answer every support question (\"did this event ever arrive?\", \"how many times?\", \"what did we do with it?\").

\n

Hypersnap emits per-webhook metrics at webhooks.delivery.* — ask your operator for the dashboard link so you can compare delivery counts against your local receiver stats.

\n

Testing #

\n
    \n
  • Signature verification: unit-test with a known secret and a known body.
  • \n
  • Rotation: seed your receiver with two secrets and confirm it accepts deliveries signed by either.
  • \n
  • Retries: return 500 for a delivery and confirm Hypersnap retries with the expected backoff.
  • \n
  • Dedupe: feed the same event twice; confirm your downstream processing ran exactly once.
  • \n
\n

Checklist #

\n
    \n
  • Raw-body middleware preserves bytes for HMAC.
  • \n
  • timingSafeEqual for signature compare.
  • \n
  • Accept multiple secrets for rotation.
  • \n
  • Dedupe on natural keys.
  • \n
  • Return 2xx after durable enqueue, not after processing.
  • \n
  • Return 5xx on transient enqueue failure.
  • \n
  • Return 4xx only when retry is pointless.
  • \n
  • Log (type, dedupe_key, first_sight, status) on every request.
  • \n
", + "contentHtml": "

Receive webhooks #

\n

A deep-dive on running a production webhook receiver for Hypersnap. Covers signature verification, rotation, dedupe, retries, and backpressure.

\n

Minimum viable receiver #

\n
import express from \"express\";\nimport crypto  from \"crypto\";\n\nconst app = express();\napp.use(express.raw({ type: \"application/json\", limit: \"1mb\" }));\n\nconst SECRETS = new Set([process.env.WEBHOOK_SECRET]);\n\napp.post(\"/hook\", (req, res) => {\n  const receivedHex = req.header(\"x-hypersnap-signature\");\n  if (!verifyAny(req.body, receivedHex, SECRETS)) {\n    return res.status(401).end();\n  }\n  const event = JSON.parse(req.body.toString(\"utf8\"));\n  void handle(event);\n  res.status(200).end();\n});\n\nfunction verifyAny(body, receivedHex, secrets) {\n  const recv = Buffer.from(receivedHex, \"hex\");\n  for (const s of secrets) {\n    const exp = crypto.createHmac(\"sha512\", s).update(body).digest();\n    if (recv.length === exp.length && crypto.timingSafeEqual(recv, exp)) {\n      return true;\n    }\n  }\n  return false;\n}\n\napp.listen(8080);\n
\n

The rest of this guide is what you do on top of this scaffold to make it robust.

\n

Signature verification #

\n

Rule one: verify before parsing. HMAC is computed over the raw bytes. If you parse JSON and re-serialize it, the bytes change (whitespace, key ordering) and your hash won't match the one in the header.

\n

Express: use express.raw. Koa/fastify: look up the raw-body option on your framework. Lambda: use the isBase64Encoded + body as-received.

\n

Rule two: use constant-time comparison. Never == on hex strings. Use crypto.timingSafeEqual / hmac.compare_digest / subtle::ConstantTimeEq.

\n

Rule three: accept multiple secrets during rotation. See below.

\n

Handling secret rotation #

\n

When you rotate a webhook's secret, Hypersnap keeps the old one valid for a 24-hour grace window (configurable by your node operator). During that window, deliveries can be signed with either secret depending on timing. Your receiver needs to:

\n
    \n
  1. Maintain an accepted-secrets set. Start with the single active secret.
  2. \n
  3. When you rotate, add the new one to the set.
  4. \n
  5. Try every secret when verifying, return 401 only if none match.
  6. \n
  7. Drop the old secret from the set once its expires_at has passed.
  8. \n
\n

In code:

\n
const SECRETS = new Map(); // secret_value -> expires_at (null = active, never expires)\n\nasync function refreshSecrets() {\n  const resp = await signedFetch(\"GET\", \"/v2/farcaster/webhook/?webhook_id=\" + ID);\n  const webhook = (await resp.json()).webhook;\n  const now = Math.floor(Date.now() / 1000);\n  SECRETS.clear();\n  for (const s of webhook.secrets) {\n    if (s.expires_at === null || s.expires_at > now) {\n      SECRETS.set(s.value, s.expires_at);\n    }\n  }\n}\n\n// Refresh on boot and every few hours.\nawait refreshSecrets();\nsetInterval(refreshSecrets, 60 * 60 * 1000);\n
\n

Dedupe #

\n

Deliveries are at-least-once. Retries, transient failures, and network races can replay the same logical event. You need an idempotency layer.

\n

Cheap version — Redis:

\n
async function handle(event) {\n  const key = dedupeKey(event);\n  const first = await redis.set(`wh:${key}`, \"1\", \"EX\", 3600, \"NX\");\n  if (!first) return; // already processed\n  await process(event);\n}\n\nfunction dedupeKey(event) {\n  switch (event.type) {\n    case \"cast.created\":\n    case \"cast.deleted\":\n      return `c:${event.data.cast.hash}`;\n    case \"follow.created\":\n    case \"follow.deleted\":\n      return `f:${event.data.follower.fid}:${event.data.target.fid}`;\n    case \"reaction.created\":\n    case \"reaction.deleted\":\n      return `r:${event.data.user.fid}:${event.data.cast.hash}:${event.data.reaction_type}`;\n    default:\n      return `x:${JSON.stringify(event.data)}`;\n  }\n}\n
\n

A 1-hour TTL is enough for retries (the retry queue times out well before that).

\n

Return codes #

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
You returnHypersnap's behavior
2xxDelivered.
4xxTerminal failure. No retries.
5xxRetried up to retry_max_attempts (default 5) with exponential backoff.
Timeout / connection errorRetried as above.
\n

Return 2xx as fast as possible. Enqueue the event to your local processing queue and ACK. Don't do downstream work on the HTTP handler's stack — you'll eat into the 10-second delivery_timeout_secs default and cause timeouts.

\n

Backpressure #

\n

At the default default_rate_limit = 1000 events / 60 seconds, you can get a sustained load of ~17 events/sec per webhook. That's small, but peaky — a hot thread can burst higher than average.

\n
    \n
  • Put a queue between the HTTP handler and your processor.
  • \n
  • Monitor queue depth.
  • \n
  • If the queue is backing up, your receiver should start returning 5xx so Hypersnap requeues instead of dropping events.
  • \n
\n

Observability #

\n

Log every delivery with:

\n
    \n
  • event.type
  • \n
  • Dedupe key
  • \n
  • Whether it was a first-sight or dedupe hit
  • \n
  • Processing outcome
  • \n
  • HTTP status returned
  • \n
\n

With those fields you can answer every support question (\"did this event ever arrive?\", \"how many times?\", \"what did we do with it?\").

\n

Hypersnap emits per-webhook metrics at webhooks.delivery.* — ask your operator for the dashboard link so you can compare delivery counts against your local receiver stats.

\n

Testing #

\n
    \n
  • Signature verification: unit-test with a known secret and a known body.
  • \n
  • Rotation: seed your receiver with two secrets and confirm it accepts deliveries signed by either.
  • \n
  • Retries: return 500 for a delivery and confirm Hypersnap retries with the expected backoff.
  • \n
  • Dedupe: feed the same event twice; confirm your downstream processing ran exactly once.
  • \n
\n

Checklist #

\n
    \n
  • Raw-body middleware preserves bytes for HMAC.
  • \n
  • timingSafeEqual for signature compare.
  • \n
  • Accept multiple secrets for rotation.
  • \n
  • Dedupe on natural keys.
  • \n
  • Return 2xx after durable enqueue, not after processing.
  • \n
  • Return 5xx on transient enqueue failure.
  • \n
  • Return 4xx only when retry is pointless.
  • \n
  • Log (type, dedupe_key, first_sight, status) on every request.
  • \n
", "contentMarkdown": "# Receive webhooks\n\nA deep-dive on running a production webhook receiver for Hypersnap. Covers signature verification, rotation, dedupe, retries, and backpressure.\n\n## Minimum viable receiver\n\n```javascript\nimport express from \"express\";\nimport crypto from \"crypto\";\n\nconst app = express();\napp.use(express.raw({ type: \"application/json\", limit: \"1mb\" }));\n\nconst SECRETS = new Set([process.env.WEBHOOK_SECRET]);\n\napp.post(\"/hook\", (req, res) => {\n const receivedHex = req.header(\"x-hypersnap-signature\");\n if (!verifyAny(req.body, receivedHex, SECRETS)) {\n return res.status(401).end();\n }\n const event = JSON.parse(req.body.toString(\"utf8\"));\n void handle(event);\n res.status(200).end();\n});\n\nfunction verifyAny(body, receivedHex, secrets) {\n const recv = Buffer.from(receivedHex, \"hex\");\n for (const s of secrets) {\n const exp = crypto.createHmac(\"sha512\", s).update(body).digest();\n if (recv.length === exp.length && crypto.timingSafeEqual(recv, exp)) {\n return true;\n }\n }\n return false;\n}\n\napp.listen(8080);\n```\n\nThe rest of this guide is what you do on top of this scaffold to make it robust.\n\n## Signature verification\n\n**Rule one: verify before parsing.** HMAC is computed over the raw bytes. If you parse JSON and re-serialize it, the bytes change (whitespace, key ordering) and your hash won't match the one in the header.\n\nExpress: use `express.raw`. Koa/fastify: look up the raw-body option on your framework. Lambda: use the `isBase64Encoded` + `body` as-received.\n\n**Rule two: use constant-time comparison.** Never `==` on hex strings. Use `crypto.timingSafeEqual` / `hmac.compare_digest` / `subtle::ConstantTimeEq`.\n\n**Rule three: accept multiple secrets during rotation.** See below.\n\n## Handling secret rotation\n\nWhen you rotate a webhook's secret, Hypersnap keeps the old one valid for a 24-hour grace window (configurable by your node operator). During that window, deliveries can be signed with either secret depending on timing. Your receiver needs to:\n\n1. **Maintain an accepted-secrets set.** Start with the single active secret.\n2. **When you rotate**, add the new one to the set.\n3. **Try every secret** when verifying, return `401` only if none match.\n4. **Drop the old secret** from the set once its `expires_at` has passed.\n\nIn code:\n\n```javascript\nconst SECRETS = new Map(); // secret_value -> expires_at (null = active, never expires)\n\nasync function refreshSecrets() {\n const resp = await signedFetch(\"GET\", \"/v2/farcaster/webhook/?webhook_id=\" + ID);\n const webhook = (await resp.json()).webhook;\n const now = Math.floor(Date.now() / 1000);\n SECRETS.clear();\n for (const s of webhook.secrets) {\n if (s.expires_at === null || s.expires_at > now) {\n SECRETS.set(s.value, s.expires_at);\n }\n }\n}\n\n// Refresh on boot and every few hours.\nawait refreshSecrets();\nsetInterval(refreshSecrets, 60 * 60 * 1000);\n```\n\n## Dedupe\n\nDeliveries are **at-least-once**. Retries, transient failures, and network races can replay the same logical event. You need an idempotency layer.\n\nCheap version — Redis:\n\n```javascript\nasync function handle(event) {\n const key = dedupeKey(event);\n const first = await redis.set(`wh:${key}`, \"1\", \"EX\", 3600, \"NX\");\n if (!first) return; // already processed\n await process(event);\n}\n\nfunction dedupeKey(event) {\n switch (event.type) {\n case \"cast.created\":\n case \"cast.deleted\":\n return `c:${event.data.cast.hash}`;\n case \"follow.created\":\n case \"follow.deleted\":\n return `f:${event.data.follower.fid}:${event.data.target.fid}`;\n case \"reaction.created\":\n case \"reaction.deleted\":\n return `r:${event.data.user.fid}:${event.data.cast.hash}:${event.data.reaction_type}`;\n default:\n return `x:${JSON.stringify(event.data)}`;\n }\n}\n```\n\nA 1-hour TTL is enough for retries (the retry queue times out well before that).\n\n## Return codes\n\n| You return | Hypersnap's behavior |\n|---|---|\n| `2xx` | Delivered. |\n| `4xx` | Terminal failure. No retries. |\n| `5xx` | Retried up to `retry_max_attempts` (default 5) with exponential backoff. |\n| Timeout / connection error | Retried as above. |\n\n**Return `2xx` as fast as possible.** Enqueue the event to your local processing queue and ACK. Don't do downstream work on the HTTP handler's stack — you'll eat into the 10-second `delivery_timeout_secs` default and cause timeouts.\n\n## Backpressure\n\nAt the default `default_rate_limit = 1000 events / 60 seconds`, you can get a sustained load of ~17 events/sec per webhook. That's small, but peaky — a hot thread can burst higher than average.\n\n- Put a queue between the HTTP handler and your processor.\n- Monitor queue depth.\n- If the queue is backing up, your receiver should start returning `5xx` so Hypersnap requeues instead of dropping events.\n\n## Observability\n\nLog every delivery with:\n\n- `event.type`\n- Dedupe key\n- Whether it was a first-sight or dedupe hit\n- Processing outcome\n- HTTP status returned\n\nWith those fields you can answer every support question (\"did this event ever arrive?\", \"how many times?\", \"what did we do with it?\").\n\nHypersnap emits per-webhook metrics at `webhooks.delivery.*` — ask your operator for the dashboard link so you can compare delivery counts against your local receiver stats.\n\n## Testing\n\n- **Signature verification:** unit-test with a known secret and a known body.\n- **Rotation:** seed your receiver with two secrets and confirm it accepts deliveries signed by either.\n- **Retries:** return `500` for a delivery and confirm Hypersnap retries with the expected backoff.\n- **Dedupe:** feed the same event twice; confirm your downstream processing ran exactly once.\n\n## Checklist\n\n- [ ] Raw-body middleware preserves bytes for HMAC.\n- [ ] `timingSafeEqual` for signature compare.\n- [ ] Accept multiple secrets for rotation.\n- [ ] Dedupe on natural keys.\n- [ ] Return `2xx` after durable enqueue, not after processing.\n- [ ] Return `5xx` on transient enqueue failure.\n- [ ] Return `4xx` only when retry is pointless.\n- [ ] Log `(type, dedupe_key, first_sight, status)` on every request.\n", "toc": [ { @@ -2002,7 +2002,7 @@ "sourcePath": "src/guides/run-as-miniapp.md", "githubUrl": "https://github.com/farcasterorg/hypersnap-docs-web/blob/master/src/guides/run-as-miniapp.md", "slug": "guides/run-as-miniapp", - "contentHtml": "

Run the docs as a Farcaster mini app #

\n

This docs site is static HTML + JS. Point a Farcaster mini-app manifest at the hosted URL and it becomes a usable mini app — wallet already connected (via the client's embedded wallet), FID already known, and the API playground can sign management requests without any extra UI.

\n

Manifest #

\n

Create manifest.json at the root of wherever you're hosting the docs:

\n
{\n  \"name\":        \"Hypersnap docs\",\n  \"icon_url\":    \"https://hypersnap-docs.qstorage.quilibrium.com/favicon-8114d1fc.png\",\n  \"home_url\":    \"https://hypersnap-docs.qstorage.quilibrium.com/playground.html\",\n  \"webhook_url\": \"https://haatz.quilibrium.com/v2/farcaster/frame/webhook/REPLACE-WITH-APP-ID\"\n}\n
\n

The manifest targets the playground page as the home — that's the part developers will want to use. The webhook_url is there so the Farcaster client can POST token events; it can point at any valid Hypersnap app_id, including one you registered for the docs itself (so the docs can push notifications back to devs who star it).

\n

Detection inside the playground #

\n

The playground's Connect-wallet flow walks four detection paths in order and uses the first one that yields an EIP-1193 provider:

\n
    \n
  1. Farcaster mini-app SDK, async APIwindow.sdk.wallet.getEthereumProvider() if the global exposes the new async getter.
  2. \n
  3. Farcaster mini-app SDK, legacy sync propertywindow.sdk.wallet.ethProvider or window.farcasterMiniApp.ethProvider for older clients.
  4. \n
  5. EIP-6963 multi-wallet discovery — listens for eip6963:announceProvider events and picks the first announced provider. This is how modern extensions (MetaMask, Rabby, Coinbase Wallet) expose themselves without stomping on window.ethereum.
  6. \n
  7. Generic injected EIP-1193window.ethereum (or the first entry of window.ethereum.providers when multiple wallets have injected).
  8. \n
\n

All four paths converge on the same request({ method: \"eth_signTypedData_v4\", … }) call. The source badge next to the connected address tells you which path was used (\"Farcaster mini-app\", \"MetaMask\", \"window.ethereum\", etc.).

\n

If none of the paths produce a provider, the Connect button surfaces a detailed error listing everything it tried — so you can tell whether your Farcaster client failed to inject the SDK, or the page is open in a plain browser with no wallet installed.

\n

What works, what doesn't #

\n
    \n
  • Public reads — work identically inside or outside a mini app. No wallet needed.
  • \n
  • Signed management calls — work if the client's embedded wallet (or injected wallet) controls the custody address for the FID you set. If not, the signature will recover to a different address and Hypersnap returns 401.
  • \n
  • Send-secret endpoint — works. You still have to paste the per-app secret into the field; the mini-app container has no way to hand it to the docs.
  • \n
\n

Hosting notes #

\n
    \n
  • Serve the book/ directory from a CORS-friendly static host. GitHub Pages, Cloudflare Pages, Netlify, and direct S3 all work.
  • \n
  • The built site is fully self-contained — no runtime fetches to an API other than the Hypersnap host the user configured. That means your CSP can be strict: connect-src 'self' https://*.quilibrium.com is enough unless you want to allow arbitrary hostnames in the Host field.
  • \n
  • There is no build-time server-side rendering to worry about.
  • \n
\n

Authoring new try-it panels #

\n

If you want to extend the playground with an endpoint that isn't already covered, the markup is:

\n
<div class=\"try-it\"\n     data-method=\"GET\"\n     data-path=\"/v2/farcaster/...\"\n     data-title=\"optional label\"\n     data-auth=\"none|signed|send-key\"\n     data-op=\"webhook.read\"\n     data-fields=\"name|kind|type|placeholder|default|required; ...\"\n     data-body-template='{\"optional\": \"pre-filled body\"}'></div>\n
\n

Where:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
AttributeMeaning
data-methodHTTP method.
data-pathPath template. Use {param} for path parameters — they get replaced from fields with kind=path.
data-titleHuman-readable label shown in the panel's summary row.
data-authnone (default for unsigned reads), signed (EIP-712 — implied when data-op is set), or send-key (per-app x-api-key).
data-opOp string for signed mode. One of webhook.create / webhook.read / etc. See Signed operations.
data-fieldsSemi-colon-separated field specs. Each field is `name
data-body-templateA starter JSON body shown in the body textarea. Users can edit before clicking Run.
\n

The playground JS hydrates every .try-it block it finds, so you can drop panels anywhere in the docs — they'll work in-page alongside the reference prose.

", + "contentHtml": "

Run the docs as a Farcaster mini app #

\n

This docs site is static HTML + JS. Point a Farcaster mini-app manifest at the hosted URL and it becomes a usable mini app — wallet already connected (via the client's embedded wallet), FID already known, and the API playground can sign management requests without any extra UI.

\n

Manifest #

\n

Create manifest.json at the root of wherever you're hosting the docs:

\n
{\n  \"name\":        \"Hypersnap docs\",\n  \"icon_url\":    \"https://hypersnap-docs.qstorage.quilibrium.com/favicon-8114d1fc.png\",\n  \"home_url\":    \"https://hypersnap-docs.qstorage.quilibrium.com/playground.html\",\n  \"webhook_url\": \"https://haatz.quilibrium.com/v2/farcaster/frame/webhook/REPLACE-WITH-APP-ID\"\n}\n
\n

The manifest targets the playground page as the home — that's the part developers will want to use. The webhook_url is there so the Farcaster client can POST token events; it can point at any valid Hypersnap app_id, including one you registered for the docs itself (so the docs can push notifications back to devs who star it).

\n

Detection inside the playground #

\n

The playground's Connect-wallet flow walks four detection paths in order and uses the first one that yields an EIP-1193 provider:

\n
    \n
  1. Farcaster mini-app SDK, async APIwindow.sdk.wallet.getEthereumProvider() if the global exposes the new async getter.
  2. \n
  3. Farcaster mini-app SDK, legacy sync propertywindow.sdk.wallet.ethProvider or window.farcasterMiniApp.ethProvider for older clients.
  4. \n
  5. EIP-6963 multi-wallet discovery — listens for eip6963:announceProvider events and picks the first announced provider. This is how modern extensions (MetaMask, Rabby, Coinbase Wallet) expose themselves without stomping on window.ethereum.
  6. \n
  7. Generic injected EIP-1193window.ethereum (or the first entry of window.ethereum.providers when multiple wallets have injected).
  8. \n
\n

All four paths converge on the same request({ method: \"eth_signTypedData_v4\", … }) call. The source badge next to the connected address tells you which path was used (\"Farcaster mini-app\", \"MetaMask\", \"window.ethereum\", etc.).

\n

If none of the paths produce a provider, the Connect button surfaces a detailed error listing everything it tried — so you can tell whether your Farcaster client failed to inject the SDK, or the page is open in a plain browser with no wallet installed.

\n

What works, what doesn't #

\n
    \n
  • Public reads — work identically inside or outside a mini app. No wallet needed.
  • \n
  • Signed management calls — work if the client's embedded wallet (or injected wallet) controls the custody address for the FID you set. If not, the signature will recover to a different address and Hypersnap returns 401.
  • \n
  • Send-secret endpoint — works. You still have to paste the per-app secret into the field; the mini-app container has no way to hand it to the docs.
  • \n
\n

Hosting notes #

\n
    \n
  • Serve the book/ directory from a CORS-friendly static host. GitHub Pages, Cloudflare Pages, Netlify, and direct S3 all work.
  • \n
  • The built site is fully self-contained — no runtime fetches to an API other than the Hypersnap host the user configured. That means your CSP can be strict: connect-src 'self' https://*.quilibrium.com is enough unless you want to allow arbitrary hostnames in the Host field.
  • \n
  • There is no build-time server-side rendering to worry about.
  • \n
\n

Authoring new try-it panels #

\n

If you want to extend the playground with an endpoint that isn't already covered, the markup is:

\n
<div class=\"try-it\"\n     data-method=\"GET\"\n     data-path=\"/v2/farcaster/...\"\n     data-title=\"optional label\"\n     data-auth=\"none|signed|send-key\"\n     data-op=\"webhook.read\"\n     data-fields=\"name|kind|type|placeholder|default|required; ...\"\n     data-body-template='{\"optional\": \"pre-filled body\"}'></div>\n
\n

Where:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
AttributeMeaning
data-methodHTTP method.
data-pathPath template. Use {param} for path parameters — they get replaced from fields with kind=path.
data-titleHuman-readable label shown in the panel's summary row.
data-authnone (default for unsigned reads), signed (EIP-712 — implied when data-op is set), or send-key (per-app x-api-key).
data-opOp string for signed mode. One of webhook.create / webhook.read / etc. See Signed operations.
data-fieldsSemi-colon-separated field specs. Each field is `name
data-body-templateA starter JSON body shown in the body textarea. Users can edit before clicking Run.
\n

The playground JS hydrates every .try-it block it finds, so you can drop panels anywhere in the docs — they'll work in-page alongside the reference prose.

", "contentMarkdown": "# Run the docs as a Farcaster mini app\n\nThis docs site is static HTML + JS. Point a Farcaster mini-app manifest at the hosted URL and it becomes a usable mini app — wallet already connected (via the client's embedded wallet), FID already known, and the [API playground](../playground.md) can sign management requests without any extra UI.\n\n## Manifest\n\nCreate `manifest.json` at the root of wherever you're hosting the docs:\n\n```json\n{\n \"name\": \"Hypersnap docs\",\n \"icon_url\": \"https://hypersnap-docs.qstorage.quilibrium.com/favicon-8114d1fc.png\",\n \"home_url\": \"https://hypersnap-docs.qstorage.quilibrium.com/playground.html\",\n \"webhook_url\": \"https://haatz.quilibrium.com/v2/farcaster/frame/webhook/REPLACE-WITH-APP-ID\"\n}\n```\n\nThe manifest targets the playground page as the home — that's the part developers will want to use. The `webhook_url` is there so the Farcaster client can POST token events; it can point at any valid Hypersnap `app_id`, including one you registered for the docs itself (so the docs can push notifications back to devs who star it).\n\n## Detection inside the playground\n\nThe playground's Connect-wallet flow walks four detection paths in order and uses the first one that yields an EIP-1193 provider:\n\n1. **Farcaster mini-app SDK, async API** — `window.sdk.wallet.getEthereumProvider()` if the global exposes the new async getter.\n2. **Farcaster mini-app SDK, legacy sync property** — `window.sdk.wallet.ethProvider` or `window.farcasterMiniApp.ethProvider` for older clients.\n3. **EIP-6963 multi-wallet discovery** — listens for `eip6963:announceProvider` events and picks the first announced provider. This is how modern extensions (MetaMask, Rabby, Coinbase Wallet) expose themselves without stomping on `window.ethereum`.\n4. **Generic injected EIP-1193** — `window.ethereum` (or the first entry of `window.ethereum.providers` when multiple wallets have injected).\n\nAll four paths converge on the same `request({ method: \"eth_signTypedData_v4\", … })` call. The `source` badge next to the connected address tells you which path was used (\"Farcaster mini-app\", \"MetaMask\", \"window.ethereum\", etc.).\n\nIf none of the paths produce a provider, the Connect button surfaces a detailed error listing everything it tried — so you can tell whether your Farcaster client failed to inject the SDK, or the page is open in a plain browser with no wallet installed.\n\n## What works, what doesn't\n\n- **Public reads** — work identically inside or outside a mini app. No wallet needed.\n- **Signed management calls** — work if the client's embedded wallet (or injected wallet) controls the custody address for the FID you set. If not, the signature will recover to a different address and Hypersnap returns `401`.\n- **Send-secret endpoint** — works. You still have to paste the per-app secret into the field; the mini-app container has no way to hand it to the docs.\n\n## Hosting notes\n\n- Serve the `book/` directory from a CORS-friendly static host. GitHub Pages, Cloudflare Pages, Netlify, and direct S3 all work.\n- The built site is fully self-contained — no runtime fetches to an API other than the Hypersnap host the user configured. That means your CSP can be strict: `connect-src 'self' https://*.quilibrium.com` is enough unless you want to allow arbitrary hostnames in the Host field.\n- There is no build-time server-side rendering to worry about.\n\n## Authoring new try-it panels\n\nIf you want to extend the playground with an endpoint that isn't already covered, the markup is:\n\n```html\n
\n```\n\nWhere:\n\n| Attribute | Meaning |\n|---|---|\n| `data-method` | HTTP method. |\n| `data-path` | Path template. Use `{param}` for path parameters — they get replaced from fields with `kind=path`. |\n| `data-title` | Human-readable label shown in the panel's summary row. |\n| `data-auth` | `none` (default for unsigned reads), `signed` (EIP-712 — implied when `data-op` is set), or `send-key` (per-app `x-api-key`). |\n| `data-op` | Op string for `signed` mode. One of `webhook.create` / `webhook.read` / etc. See [Signed operations](../concepts/authentication.md). |\n| `data-fields` | Semi-colon-separated field specs. Each field is `name|kind|type|placeholder|default|required` with `kind` in `{query, path, body, header}`. |\n| `data-body-template` | A starter JSON body shown in the body textarea. Users can edit before clicking Run. |\n\nThe playground JS hydrates every `.try-it` block it finds, so you can drop panels anywhere in the docs — they'll work in-page alongside the reference prose.\n", "toc": [ {