@lsp-indexer/react playground

@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/node

Environment 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();
FunctionEnv VarFallback
getClientUrl()NEXT_PUBLIC_INDEXER_URLthrows
getServerUrl()INDEXER_URLNEXT_PUBLIC_INDEXER_URL
getClientWsUrl()NEXT_PUBLIC_INDEXER_WS_URLderived from getClientUrl()
getServerWsUrl()INDEXER_WS_URLderived 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

DomainSingleList
ProfilesfetchProfilefetchProfiles
Digital AssetsfetchDigitalAssetfetchDigitalAssets
NFTsfetchNftfetchNfts
Owned AssetsfetchOwnedAssetfetchOwnedAssets
Owned TokensfetchOwnedTokenfetchOwnedTokens
CreatorsfetchCreators
Issued AssetsfetchIssuedAssets
FollowsfetchFollows, fetchMutualFollows, fetchMutualFollowers, fetchFollowedByMyFollows
Encrypted AssetsfetchEncryptedAssets, fetchEncryptedAssetsBatch
Data ChangedfetchLatestDataChangedEventfetchDataChangedEvents
Token ID Data ChangedfetchLatestTokenIdDataChangedEventfetchTokenIdDataChangedEvents
Universal ReceiverfetchUniversalReceiverEvents
Collection AttributesfetchCollectionAttributes

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 both addressA and addressB follow
  • fetchMutualFollowers(url, { addressA, addressB, sort?, limit?, offset?, include? }) — profiles that follow both addressA and addressB
  • fetchFollowedByMyFollows(url, { myAddress, targetAddress, sort?, limit?, offset?, include? }) — profiles that myAddress follows and that also follow targetAddress

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

ParameterTypeRequiredDescription
tuplesEncryptedAssetBatchTuple[]YesArray of { address: string, contentId: string, revision: number } to fetch
includeEncryptedAssetIncludeNoNarrow 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

ParameterTypeRequiredDescription
collectionAddressstringYesContract 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 collection

Each 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:

FieldTypeDescription
scorenumber | nullChillwhales score
ranknumber | nullRank within the collection by score
chillClaimedboolean | nullWhether the CHILL token has been claimed
orbsClaimedboolean | nullWhether the ORBS token has been claimed
levelnumber | nullCurrent level (chillwhales game mechanic)
cooldownExpirynumber | nullCooldown expiry as unix epoch
factionstring | nullFaction 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 | null

NFT Filter Fields

NFTs support additional filter fields for chillwhales game state:

FieldTypeDescription
chillClaimedbooleanFilter by CHILL claimed status
orbsClaimedbooleanFilter by ORBS claimed status
maxLevelnumberFilter NFTs at or below this level
cooldownExpiryBeforenumberFilter 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...'
  }
}
CategoryCodes
CONFIGURATIONMISSING_ENV_VAR, INVALID_URL
NETWORKNETWORK_TIMEOUT, NETWORK_UNREACHABLE, NETWORK_ABORTED, NETWORK_UNKNOWN
HTTPHTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_TOO_MANY_REQUESTS, HTTP_SERVER_ERROR, HTTP_UNKNOWN
GRAPHQLGRAPHQL_VALIDATION, GRAPHQL_EXECUTION, PERMISSION_DENIED, GRAPHQL_UNKNOWN
PARSERESPONSE_NOT_JSON, EMPTY_RESPONSE, PARSE_FAILED
VALIDATIONVALIDATION_FAILED

Next Steps