@lsp-indexer/node
The foundational package — provides low-level GraphQL fetch functions, parsers, query key factories,
env helpers, and the subscription client. Both @lsp-indexer/react and @lsp-indexer/next are built
on top of this.
npm install @lsp-indexer/nodeEnvironment Helpers
The package provides URL helpers that read environment variables at runtime:
import { getClientUrl, getServerUrl, getClientWsUrl, getServerWsUrl } from '@lsp-indexer/node';
// Client-side (reads NEXT_PUBLIC_INDEXER_URL)
const clientUrl = getClientUrl();
// Server-side (reads INDEXER_URL, falls back to NEXT_PUBLIC_INDEXER_URL)
const serverUrl = getServerUrl();
// WebSocket URLs (reads WS vars, falls back to HTTP URL with wss://)
const clientWs = getClientWsUrl();
const serverWs = getServerWsUrl();| Function | Env Var | Fallback |
|---|---|---|
getClientUrl() | NEXT_PUBLIC_INDEXER_URL | throws |
getServerUrl() | INDEXER_URL | NEXT_PUBLIC_INDEXER_URL |
getClientWsUrl() | NEXT_PUBLIC_INDEXER_WS_URL | derived from getClientUrl() |
getServerWsUrl() | INDEXER_WS_URL | derived from getServerUrl() |
All functions validate URLs and throw IndexerError with category CONFIGURATION on invalid input.
Fetch Functions
Every domain has a pair of fetch functions — one for single entities, one for lists:
import { fetchProfile, fetchProfiles, getClientUrl } from '@lsp-indexer/node';
// Fetch a single profile
const profile = await fetchProfile(getClientUrl(), { address: '0x...' });
// Fetch a paginated list
const { profiles, totalCount } = await fetchProfiles(getClientUrl(), {
filter: { name: 'vitalik' },
sort: { field: 'name', direction: 'asc' },
limit: 10,
offset: 0,
});Available fetch functions
| Domain | Single | List |
|---|---|---|
| Profiles | fetchProfile | fetchProfiles |
| Digital Assets | fetchDigitalAsset | fetchDigitalAssets |
| NFTs | fetchNft | fetchNfts |
| Owned Assets | fetchOwnedAsset | fetchOwnedAssets |
| Owned Tokens | fetchOwnedToken | fetchOwnedTokens |
| Creators | — | fetchCreators |
| Issued Assets | — | fetchIssuedAssets |
| Follows | — | fetchFollows, fetchMutualFollows, fetchMutualFollowers, fetchFollowedByMyFollows |
| Encrypted Assets | — | fetchEncryptedAssets, fetchEncryptedAssetsBatch |
| Data Changed | fetchLatestDataChangedEvent | fetchDataChangedEvents |
| Token ID Data Changed | fetchLatestTokenIdDataChangedEvent | fetchTokenIdDataChangedEvents |
| Universal Receiver | — | fetchUniversalReceiverEvents |
| Collection Attributes | — | fetchCollectionAttributes |
Additional: fetchFollowCount, fetchIsFollowing.
Additional: fetchEncryptedAssetsBatch — batch-fetches multiple encrypted assets by (address, contentId, revision) tuples.
Mutual follow queries are intersection queries across the follow graph:
fetchMutualFollows(url, { addressA, addressB, sort?, limit?, offset?, include? })— profiles that bothaddressAandaddressBfollowfetchMutualFollowers(url, { addressA, addressB, sort?, limit?, offset?, include? })— profiles that follow bothaddressAandaddressBfetchFollowedByMyFollows(url, { myAddress, targetAddress, sort?, limit?, offset?, include? })— profiles thatmyAddressfollows and that also followtargetAddress
All three accept sort, limit, offset, and include options and return
{ profiles, totalCount } — the same shape as fetchProfiles.
Batch Encrypted Asset Fetch
fetchEncryptedAssetsBatch fetches multiple encrypted assets by (address, contentId, revision) tuples in a single Hasura query.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
tuples | EncryptedAssetBatchTuple[] | Yes | Array of { address: string, contentId: string, revision: number } to fetch |
include | EncryptedAssetInclude | No | Narrow which related fields are returned — full TypeScript inference |
Usage
import { fetchEncryptedAssetsBatch, getClientUrl } from '@lsp-indexer/node';
const { encryptedAssets } = await fetchEncryptedAssetsBatch(getClientUrl(), {
tuples: [
{ address: '0xAssetAddress1', contentId: 'content-1', revision: 1 },
{ address: '0xAssetAddress2', contentId: 'content-2', revision: 0 },
],
include: { encryption: true },
});
// encryptedAssets → EncryptedAsset[] (one per matched tuple)Empty tuples short-circuits — no network call is made and encryptedAssets returns [].
If no tuples match, encryptedAssets returns [] — no error is thrown.
Address matching is case-insensitive. Duplicate tuples are not deduplicated — pass unique tuples.
The return shape is { encryptedAssets: P[] } (no totalCount).
Collection Attributes
fetchCollectionAttributes fetches distinct {key, value} attribute pairs and the total NFT count for a collection address.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
collectionAddress | string | Yes | Contract address of the NFT collection to query |
Usage
import { fetchCollectionAttributes, getClientUrl } from '@lsp-indexer/node';
const { attributes, totalCount } = await fetchCollectionAttributes(getClientUrl(), {
collectionAddress: '0xCollectionAddress',
});
// attributes → CollectionAttribute[] — distinct { key, value, type } pairs
// totalCount → number — total NFTs in the collectionEach CollectionAttribute has key (string), value (string), and type (string | null).
Uses distinct_on: [key, value] to deduplicate across all NFTs in the collection.
Address matching is case-insensitive.
Include Fields (Partial Selects)
All fetch functions accept an include parameter to control which related data is returned.
This reduces payload size and improves performance:
import { fetchProfile, getClientUrl } from '@lsp-indexer/node';
// Only fetch the profile name and owned assets
const profile = await fetchProfile(getClientUrl(), {
address: '0x...',
include: {
ownedAssets: true,
issuedAssets: false,
creators: false,
},
});
// profile.ownedAssets → OwnedAsset[]
// profile.issuedAssets → undefined (not included)The return type narrows automatically based on which fields you include — full TypeScript inference.
NFT Include Fields
NFTs support additional include fields for chillwhales-specific game data:
| Field | Type | Description |
|---|---|---|
score | number | null | Chillwhales score |
rank | number | null | Rank within the collection by score |
chillClaimed | boolean | null | Whether the CHILL token has been claimed |
orbsClaimed | boolean | null | Whether the ORBS token has been claimed |
level | number | null | Current level (chillwhales game mechanic) |
cooldownExpiry | number | null | Cooldown expiry as unix epoch |
faction | string | null | Faction membership (chillwhales game mechanic) |
import { fetchNfts, getClientUrl } from '@lsp-indexer/node';
const { nfts } = await fetchNfts(getClientUrl(), {
filter: { collectionAddress: '0x...' },
include: { score: true, rank: true, chillClaimed: true, faction: true },
});
// nfts[0].score → number | null
// nfts[0].rank → number | nullNFT Filter Fields
NFTs support additional filter fields for chillwhales game state:
| Field | Type | Description |
|---|---|---|
chillClaimed | boolean | Filter by CHILL claimed status |
orbsClaimed | boolean | Filter by ORBS claimed status |
maxLevel | number | Filter NFTs at or below this level |
cooldownExpiryBefore | number | Filter NFTs whose cooldown expires before this unix timestamp |
NFT Sort Fields
The NftSortField enum now includes score for ranking NFTs by their chillwhales score:
import { fetchNfts, getClientUrl } from '@lsp-indexer/node';
const { nfts } = await fetchNfts(getClientUrl(), {
filter: { collectionAddress: '0x...' },
sort: { field: 'score', direction: 'desc', nulls: 'last' },
include: { score: true, rank: true },
});Available NftSortField values: newest, oldest, tokenId, formattedTokenId, score.
Query Key Factories
For React Query cache management, every domain exports a key factory:
import {
profileKeys,
digitalAssetKeys,
encryptedAssetKeys,
collectionAttributeKeys,
} from '@lsp-indexer/node';
profileKeys.all; // ['profiles']
profileKeys.lists(); // ['profiles', 'list']
profileKeys.list(filter, sort, limit, offset, include); // ['profiles', 'list', { filter, sort, ... }]
profileKeys.details(); // ['profiles', 'detail']
profileKeys.detail(address, include); // ['profiles', 'detail', { address, include }]
// Batch key factories
encryptedAssetKeys.batch(tuples, include); // ['encryptedAssets', 'batch', tuples, include]
// Collection attribute key factories
collectionAttributeKeys.all; // ['collection-attributes']
collectionAttributeKeys.lists(); // ['collection-attributes', 'list']
collectionAttributeKeys.list(collectionAddress); // ['collection-attributes', 'list', collectionAddress]These are used internally by the React and Next.js hooks, but you can also use them directly for manual cache invalidation:
import { useQueryClient } from '@tanstack/react-query';
import { profileKeys } from '@lsp-indexer/node';
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: profileKeys.lists() });Parsers
Raw Hasura responses are parsed into typed domain objects. Parsers handle:
- Nested relationship mapping
- Null safety for optional fields
- Include-aware partial selects (omits fields not in
include)
import { parseProfile, parseProfiles } from '@lsp-indexer/node';
// Usually called internally by fetch functions, but available for custom use
const profile = parseProfile(rawHasuraResponse);Subscription Client
Low-level WebSocket subscription client for real-time data. Manages connection state,
reconnection detection, and multiple independent subscriptions via useSyncExternalStore.
import { SubscriptionClient } from '@lsp-indexer/node';
// Connection is lazy — no explicit connect() needed
const client = new SubscriptionClient('ws://localhost:8080/v1/graphql');
// Create a subscription instance (used internally by hooks)
const instance = client.createSubscription(config, options);
// Access reactive state
instance.data; // TParsed[] | null — null until first data received
instance.error; // unknown
instance.isSubscribed; // boolean
// Subscribe to state changes (useSyncExternalStore pattern)
const unsubscribe = instance.subscribe(() => {
console.log('State changed:', instance.data);
});In practice, you'll use the higher-level subscription hooks (useProfileSubscription, etc.)
from @lsp-indexer/react instead of the raw client.
Error Handling
All errors are wrapped in IndexerError with structured metadata:
import { IndexerError } from '@lsp-indexer/node';
try {
const url = getClientUrl();
} catch (err) {
if (err instanceof IndexerError) {
console.log(err.category); // 'CONFIGURATION'
console.log(err.code); // 'MISSING_ENV_VAR'
console.log(err.message); // 'NEXT_PUBLIC_INDEXER_URL is not set...'
}
}| Category | Codes |
|---|---|
CONFIGURATION | MISSING_ENV_VAR, INVALID_URL |
NETWORK | NETWORK_TIMEOUT, NETWORK_UNREACHABLE, NETWORK_ABORTED, NETWORK_UNKNOWN |
HTTP | HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_TOO_MANY_REQUESTS, HTTP_SERVER_ERROR, HTTP_UNKNOWN |
GRAPHQL | GRAPHQL_VALIDATION, GRAPHQL_EXECUTION, PERMISSION_DENIED, GRAPHQL_UNKNOWN |
PARSE | RESPONSE_NOT_JSON, EMPTY_RESPONSE, PARSE_FAILED |
VALIDATION | VALIDATION_FAILED |
Next Steps
- @lsp-indexer/react — Client-side hooks built on these fetch functions
- @lsp-indexer/next — Server actions and hooks for Next.js
- Quickstart — End-to-end setup guide
- Domain Playgrounds — Try every hook live