Skip to content

Call your node over HTTP from your own code, with no paid data API.

Read every Farcaster message from your own node over plain HTTP, for an app, a bot or an analytics job, with no paid data API. Start here before the recipes: the base URL, the message format, Farcaster time, paging and errors.

The app’s address is also the API’s address. Caddy, set up by step 3, passes every /v1/ request to the node:

Terminal window
export NODE=https://<your node>

Port 3381, the node’s own HTTP port, stays closed to the internet. From the server itself, or through an SSH tunnel (ssh -N -L 3381:127.0.0.1:3381 root@$IP), http://127.0.0.1:3381 works too.

There is no API key and no sign-in: anyone who knows the address can read public Farcaster data from it. Caddy blocks only the node’s admin pages (/v1/mesh).

Terminal window
curl -s "$NODE/v1/info" | jq '{version, dbStats, shards: [.shardInfos[] | {shardId, blockDelay}]}'
Response (trimmed)
{
"version": "0.14.3",
"dbStats": { "numMessages": 914247971, "numFidRegistrations": 3355185, "approxSize": 557895406561 },
"shards": [
{ "shardId": 0, "blockDelay": 1 },
{ "shardId": 1, "blockDelay": 1 },
{ "shardId": 2, "blockDelay": 0 }
]
}

What this returns: the node’s version, how many messages it stores, and how many seconds each shard trails the newest block. Single digits everywhere means your reads are current.

The JavaScript examples use the global fetch in Node.js 18 or later and in browsers. GET responses carry Access-Control-Allow-Origin: *, so a page on another origin can read from your node. POST is different; see Post a cast.

Most endpoints return Farcaster messages. Each one has the same envelope, and data.type says which body it carries:

One cast from GET /v1/castsByFid (trimmed)
{
"data": {
"type": "MESSAGE_TYPE_CAST_ADD",
"fid": 37,
"timestamp": 182827068,
"network": "FARCASTER_NETWORK_MAINNET",
"castAddBody": {
"text": "One mechanism for proof of human is vouching. …",
"parentCastId": { "fid": 3355261, "hash": "0xec42f9dde49d50dca69034c62bc890f37903b521" },
"mentions": [],
"mentionsPositions": [],
"embeds": []
}
},
"hash": "0xc87cfa68811e39167f5e38264a16c67b99dd6ff1",
"signer": "0x0766980c52e24fda5fb1784d430d94f3555ca3f71f63f0c0411849401c63ee5d"
}
  • data.fid is the author’s account number. hash identifies the message; with the FID it identifies a cast.
  • signer is the app key that signed it. Filter casts by app turns it into an app name.
  • Hashes, keys and addresses are 0x hex. Signatures are base64. So is castRemoveBody.targetHash, even though it is a hash.
  • Enum values come as strings, such as MESSAGE_TYPE_CAST_ADD and REACTION_TYPE_LIKE.
  • The body field matches the type: castAddBody, reactionBody, linkBody, userDataBody. A list endpoint can mix types: castsByFid also returns MESSAGE_TYPE_CAST_REMOVE messages, so check data.type before you read castAddBody.

Field-by-field details: Message reference.

data.timestamp counts seconds since 2021-01-01 00:00:00 UTC, not since 1970. Add 1609459200 to get Unix time.

Terminal window
date -u -d @$((1609459200 + 182789250)) +%Y-%m-%dT%H:%M:%SZ
Output
2026-10-17T14:47:30.000Z
182822400

List endpoints take three query parameters and return a nextPageToken:

ParameterMeaning
pageSizeMessages per page. Always set it: without it, castsByFid?fid=3 returned 10,000 casts in one response.
reversereverse=true returns newest first. It must be the word true or false; reverse=1 gets HTTP 400.
pageTokenThe nextPageToken from the previous response. Leave it out for the first page.
Terminal window
# First page: the 2 newest casts by FID 37
curl -s "$NODE/v1/castsByFid?fid=37&pageSize=2&reverse=true" | jq '{n: (.messages | length), nextPageToken}'
# Next page: pass nextPageToken back as pageToken
TOKEN=$(curl -s "$NODE/v1/castsByFid?fid=37&pageSize=2&reverse=true" | jq -r .nextPageToken)
curl -s "$NODE/v1/castsByFid?fid=37&pageSize=2&reverse=true&pageToken=$TOKEN" | jq '[.messages[].data.timestamp]'
Output
{ "n": 2, "nextPageToken": "AwAAACUBCuVU8goF1cgycmWFyXMTt3RTGbTtHSXO" }
[182796605, 182796543]
FID 37 has 788 casts on this node

Two kinds of endpoint page differently:

  • One account (castsByFid, linksByFid, reactionsByFid, userDataByFid): the account lives on one shard. A page holds up to pageSize messages, and the last page returns an empty nextPageToken.
  • Many accounts (castsByParent, reactionsByCast, linksByTargetFid): the node asks every account shard and joins the answers, so a page can hold up to pageSize per shard. pageSize=2 returned 4 replies. castsByParent also never returns an empty token: its last page carries one null per shard, and sending that token starts over from the first page. Read the replies to a cast shows how to stop.
What you sentHTTPBody
A bad or missing parameter400Plain text, such as Invalid query parameters: missing field `fid`
A valid lookup that finds nothing400JSON {"error": "Failed to get cast", "error_detail": "…NotFound…"}
A path that does not exist404Plain text Not Found

A missing cast or username is a 400, not a 404. To tell it apart from a malformed request, check that the body is JSON and that error_detail contains NotFound.

Two errors
curl -s -w ' [HTTP %{http_code}]\n' "$NODE/v1/castsByFid?fid=37&reverse=1"
curl -s -w ' [HTTP %{http_code}]\n' "$NODE/v1/castById?fid=37&hash=0x0000000000000000000000000000000000000000"
Output (trimmed)
Invalid query parameters: provided string was not `true` or `false` [HTTP 400]
{"error":"Failed to get cast","error_detail":"status: NotFound, message: \"cast not found\", …"} [HTTP 400]

The node’s HTTP server has no rate limit, and the Caddy setup from step 3 adds none. The limit is your server: a heavy job competes with the node for CPU and disk. Run large scans with pageSize of a few hundred, a few requests in parallel, and from the server itself (http://127.0.0.1:3381) when you can. If you share the address, set a rate limit in Caddy.

  • Recipes: look up users, read casts and feeds, follow replies and reactions, filter by app, watch for new messages and post.
  • Reuse the client code: the RunFarcaster app as a reference implementation, with the files to copy into your own project.