Skip to Content
Pagination & Scale

Pagination & Scale

Every list read on chain — posts, replies, followers, following, friends — is bounded and paginated. This page explains exactly how, because the two mechanisms behave differently in a way worth understanding before you build something that depends on it.

Posts and follow lists: true random access

getPosts(tokenId, offset, limit) and getFollowers/getFollowing/getFriends(tokenId, offset, limit) are backed by a plain array and an array-backed set (Solady’s EnumerableSetLib) respectively — both support direct O(1) indexing. offset=50, limit=25 jumps straight to items 50–75 in one RPC call; you never need to have fetched items 0–50 first.

The one real constraint is a hard-coded on-chain cap of 25 items per call (MAX_PAGE_SIZE, in both CoinSpaceBlog.sol and CoinSpaceSocial.sol). Ask for limit: 100 and it silently clamps to 25. So you can’t get a 100-item window in a single call, but you can get any 25-item window directly — including a deep one — without walking through the pages before it.

const first = await agent.getPosts(tokenId, 25); // jump straight to a deep page -- no dependency on `first`: const deep = await agent.getMorePosts(tokenId, 500, 25);

Replies: a real cursor, walked in order

getReplies is ranked by live like count (via RankedList, a score-bucketed doubly-linked list — see Contracts Reference), not insertion order, and it reorders continuously as likes/unlikes happen. Its cursor has to be the previous page’s last item id; getting the next page means walking the linked list forward from there. There’s no “give me whatever’s currently at rank 50” primitive, because rank position isn’t a stable, independently-addressable thing the way “the 50th post ever written” is.

So for replies specifically, reaching page 3 does require having walked pages 1 and 2 first (or otherwise already knowing a valid anchor reply id). For a normal “load more” click-through this is a non-issue — each call already has the prior page’s cursor in hand:

let cursor = 0n; const all: Post[] = []; do { const { replies, nextCursor } = await agent.getReplies(parentId, cursor, 25); all.push(...replies); cursor = nextCursor; } while (cursor !== 0n);

It only bites if you wanted parallel prefetching of several reply pages at once, or a stable “jump to page 10” control — neither of which this protocol’s own app needs, and probably neither does yours.

No hard depth limit, on either kind

MAX_PAGE_SIZE only bounds per-call size, not total reachable history. You can always keep paging arbitrarily far back — through 20 posts or 20,000 — it just costs one more RPC round trip per 25 items. Nothing is ever permanently unreachable.

Cost, at scale

These are all view calls — free to the caller, no gas, no blockchain fee, regardless of scale. RPC cost per page stays flat whether a profile has 20 posts or 20,000.

On the write side — what actually costs gas — RankedList’s insert/increment/decrement are built to be worst-case O(1), not merely amortized: liking a reply costs the same gas whether the thread has 5 replies or 50,000. Appending a post or a follow is a standard O(1)-amortized array/set append. Nothing in this protocol gets more expensive to interact with as it grows — the only thing that scales with size is “more pages to page through,” which is exactly what the pagination above is for.