Skip to main content

Indexer & GraphQL API

Overview

A custom indexer is available for integration, providing GraphQL access to indexed on-chain data from Solana RWA programs. The indexer continuously monitors and indexes transactions from all Solana RWA programs, making historical and real-time data easily accessible through a unified GraphQL API.

Key Features

  • Multi-Program Indexing: Indexes all Solana RWA programs
  • Token-2022 Activity: Indexes Token-2022 mint and burn instructions for registered tokens
  • GraphQL API: Unified GraphQL endpoint with interactive GraphiQL IDE
  • Chronological Ordering: All queries return data in chronological order optimized for cap table building

Authentication

The GraphQL endpoint uses API key authentication via the X-API-Key header. The key is available upon request.

Using the API Key

Include the API key in all GraphQL requests using the X-API-Key header:

curl -H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ __typename }"}' \
https://solana-rwa-indexer.upside.gg/graphql

Endpoints

Production

  • GraphQL Endpoint: https://solana-rwa-indexer.upside.gg/graphql
  • GraphiQL IDE: https://solana-rwa-indexer.upside.gg/graphiql
  • Health Check: https://solana-rwa-indexer.upside.gg/health (no authentication required)

Staging

  • GraphQL Endpoint: https://solana-rwa-indexer-staging.upside.gg/graphql
  • GraphiQL IDE: https://solana-rwa-indexer-staging.upside.gg/graphiql
  • Health Check: https://solana-rwa-indexer-staging.upside.gg/health (no authentication required)

Example Request

# Production
curl -H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ __typename }"}' \
https://solana-rwa-indexer.upside.gg/graphql

# Staging
curl -H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ __typename }"}' \
https://solana-rwa-indexer-staging.upside.gg/graphql

GraphQL Schema for Cap Table Activity

The indexer provides a unified tokenActivity query that combines all token movements (mints, burns, force transfers, and transfers) in a single sorted list, designed for cap table building.

TokenActivity Query

query TokenActivity($mint: String!, $limit: Int!, $offset: Int!, $order: SortOrder) {
tokenActivity(mint: $mint, limit: $limit, offset: $offset, order: $order) {
activityType # "mint", "burn", "force_transfer", or "transfer"
signature
instructionIndex
innerIndex # Only for transfers (null for other types)
slot
slotTimestamp
stackHeight
amount
mint
createdAt
decimals # Token decimals from mint metadata (null if unknown)

# Authority fields (check activityType to know which are populated)
destinationAuthority # For mints, force_transfers, and transfers
targetAuthority # For burns
sourceAuthority # For force_transfers and transfers

# Enriched metadata (null when no metadata applies)
meta {
eventType
admin
fromAuthority
toAuthority
amount
scheduleId
commencementTimestamp
cancelableBy
scheduleReleaseCount
scheduleDelayUntilFirstReleaseInSeconds
scheduleInitialReleasePortionInBips
schedulePeriodBetweenReleasesInSeconds
timelockId
canceledBy
canceledAmount
paidAmount
target
reclaimer
}
}
}

Query Parameters

ParameterTypeRequiredDescription
mintString!YesToken mint address to query
limitIntNoMaximum number of results (default: 100)
offsetIntNoNumber of results to skip (default: 0)
orderSortOrderNoSort order: ASC (oldest first, default) or DESC (newest first)

TokenActivity Response Fields

FieldTypeDescription
activityTypeStringType of activity: "mint", "burn", "force_transfer", or "transfer"
signatureStringTransaction signature (part of unique event ID)
instructionIndexIntIndex of instruction within transaction (part of unique event ID)
innerIndexInt?Ordinal within the same outer instruction; only populated for transfer activity type (null for others). Part of the unique event ID for transfers.
slotStringSolana slot number
slotTimestampString?Unix timestamp of the slot
stackHeightInt?Call stack height (1 = outer, >=2 = inner/CPI)
amountStringToken amount (as decimal string)
mintStringToken mint address
createdAtString?Timestamp when record was indexed
decimalsInt?Token decimals from mint metadata. Null if the mint was not initialized through AccessControl.
destinationAuthorityString?Destination authority (populated for mints, force_transfers, and transfers)
targetAuthorityString?Target authority (populated for burns)
sourceAuthorityString?Source authority (populated for force_transfers and transfers)
metaTokenActivityMeta?Enriched metadata for specific event types (see below)
note

The unique identifier for each event is a composite key of signature + instructionIndex + innerIndex. For non-transfer activity types, the GraphQL API returns innerIndex as null; when constructing composite keys, clients should treat null as 0 to maintain a consistent identifier format.

TokenActivityMeta Fields

The meta object is populated when enrichment data exists for a given activity. It is null when no metadata applies (e.g., standard burns).

FieldTypeDescription
eventTypeStringMeta event type (see table below)
adminString?Admin authority that performed the operation
fromAuthorityString?Source wallet authority
toAuthorityString?Destination wallet authority
amountString?Amount involved
scheduleIdString?Release schedule ID
commencementTimestampString?Schedule commencement timestamp
cancelableByString?Authority that can cancel the schedule
scheduleReleaseCountString?Number of releases in the schedule
scheduleDelayUntilFirstReleaseInSecondsString?Delay before first release (seconds)
scheduleInitialReleasePortionInBipsString?Initial release portion in basis points
schedulePeriodBetweenReleasesInSecondsString?Period between releases (seconds)
timelockIdString?Timelock ID
canceledByString?Authority that canceled the timelock
canceledAmountString?Amount returned from cancellation
paidAmountString?Amount already paid out before cancellation
targetString?Target wallet of the timelock
reclaimerString?Wallet that reclaimed canceled tokens

Meta event types by activity type:

Activity TypeMeta Event TypeDescription
mintschedule_fundedMint was part of a MintReleaseSchedule, populates schedule fields
transfervesting_transferTransfer from a vesting schedule release
transfervesting_transfer_timelockVesting transfer with associated timelock
transfertimelock_canceledTransfer resulting from a timelock cancellation, populates cancellation fields
force_transferforce_transferEmbedded directly from the ForceTransferBetween instruction accounts
burn(none expected)Burns do not currently have associated metadata

Activity Type Details

The tokenActivity query sources data from different on-chain instructions depending on the activity type:

Activity TypeSourceNotes
mintAccessControl::MintSecuritiesDirect Token-2022 MintTo is impossible because the mint authority is the AccessControl PDA
burnAccessControl::BurnSecurities + Token-2022 BurnToken holders can burn directly via Token-2022 without going through AccessControl. Deduplicated to avoid double-counting.
force_transferAccessControl::ForceTransferBetweenAdmin-initiated forced transfer. First-class type because Token-2022 may skip the transfer hook for self-transfers (source == dest), making these invisible in regular transfer data.
transferTransferRestrictions::ExecuteTransaction (transfer hook)Regular transfers. Excludes transactions that are also force transfers.

SortOrder Enum

enum SortOrder {
ASC # Ascending order (oldest first) - default
DESC # Descending order (newest first)
}

GraphQL Schema for Admin Activity

The indexer provides a unified tokenAdminActivity query that combines all administrative operations for a token (role management, group configuration, transfer rules, address permissions, security accounts, pause state, and release schedules) in a single sorted list.

TokenAdminActivity Query

query TokenAdminActivity($mint: String!, $limit: Int, $offset: Int, $order: SortOrder) {
tokenAdminActivity(mint: $mint, limit: $limit, offset: $offset, order: $order) {
activityType
signature
instructionIndex
slot
slotTimestamp
stackHeight
securityMint
createdAt

# Role fields (grant_role, revoke_role)
role
userWallet

# Group fields
groupId
groupIdFrom
groupIdTo

# Transfer rule fields
lockUntil
lockedUntil

# Group max holders
holderGroupMax

# Address permission fields
frozen

# Security associated account fields
holderId

# Pause field
paused

# Release schedule fields
uuid
releaseCount
delayUntilFirstReleaseInSeconds
initialReleasePortionInBips
periodBetweenReleasesInSeconds
}
}

Query Parameters

ParameterTypeRequiredDescription
mintString!YesToken mint address (security mint) to query
limitIntNoMaximum number of results (default: 100)
offsetIntNoNumber of results to skip (default: 0)
orderSortOrderNoSort order: ASC (oldest first, default) or DESC (newest first)

TokenAdminActivity Response Fields

Common fields (present on every row):

FieldTypeDescription
activityTypeStringType of admin operation (see Activity Types table below)
signatureStringTransaction signature
instructionIndexIntIndex of instruction within the transaction
slotString?Solana slot number
slotTimestampString?Unix timestamp of the slot
stackHeightInt?Call stack height (1 = outer, >=2 = inner/CPI)
securityMintStringToken mint address
createdAtString?Timestamp when record was indexed

Activity-specific fields (populated depending on activityType, null otherwise):

FieldTypeDescription
roleInt?Role identifier (1 = Contract Admin, 2 = Reserve Admin, etc.)
userWalletString?Wallet address of the affected user
groupIdString?Transfer restriction group ID
groupIdFromString?Source group ID (for transfer rules)
groupIdToString?Destination group ID (for transfer rules)
lockUntilString?Lock-until timestamp for transfer rules
lockedUntilString?Locked-until timestamp for allow transfer rules
holderGroupMaxString?Maximum number of holders in a group
frozenBoolean?Whether the address is frozen
holderIdString?Holder ID for security associated accounts
pausedBoolean?Whether the token is paused
uuidString?Release schedule UUID
releaseCountInt?Number of releases in the schedule
delayUntilFirstReleaseInSecondsString?Delay before the first release (seconds)
initialReleasePortionInBipsInt?Initial release portion in basis points
periodBetweenReleasesInSecondsString?Period between releases (seconds)

Activity Types

Activity TypePopulated FieldsNotes
grant_rolerole, userWalletAlso generated from InitializeAccessControl (auto-grants Contract Admin to deployer)
revoke_rolerole, userWallet
freeze_walletuserWalletuserWallet is the frozen wallet
thaw_walletuserWalletuserWallet is the thawed wallet
initialize_transfer_restriction_groupgroupIdAlso generated from InitializeTransferRestrictionsData (auto-creates group 0)
set_holder_group_maxgroupId, holderGroupMax
initialize_transfer_rulegroupIdFrom, groupIdTo, lockUntil
set_allow_transfer_rulegroupIdFrom, groupIdTo, lockedUntil
set_address_permissionuserWallet, groupId, frozen
initialize_security_associated_accountuserWallet, groupId, holderId
revoke_security_associated_accountuserWallet
pausepaused
create_release_scheduleuuid, releaseCount, delayUntilFirstReleaseInSeconds, initialReleasePortionInBips, periodBetweenReleasesInSeconds
note

The unique identifier for each event is a composite key constructed from signature, stackHeight, and instructionIndex. Use this combination to uniquely identify and deduplicate events.

Example Queries

Cap Table Activity Query

Get all token movements for a specific mint in chronological order:

query CapTableActivity {
tokenActivity(
mint: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
limit: 1000
offset: 0
order: ASC # Optional: ASC (default) or DESC
) {
activityType
signature
instructionIndex
innerIndex
slot
slotTimestamp
stackHeight
amount
mint
createdAt
decimals

destinationAuthority
targetAuthority
sourceAuthority

meta {
eventType
admin
fromAuthority
toAuthority
amount
scheduleId
commencementTimestamp
cancelableBy
scheduleReleaseCount
scheduleDelayUntilFirstReleaseInSeconds
scheduleInitialReleasePortionInBips
schedulePeriodBetweenReleasesInSeconds
timelockId
canceledBy
canceledAmount
paidAmount
target
reclaimer
}
}
}

Using for Cap Table Building

  1. Query returns all activities in chronological order (oldest first by default, use order: DESC for newest first)
  2. Unique Event Identification: Each event should be uniquely identified using the composite key signature + instructionIndex + innerIndex. For non-transfer types, innerIndex is always 0. This ensures proper deduplication when processing events across multiple queries or pagination.
  3. Check activityType to determine how to process each event:
    • "mint": Add amount to destinationAuthority balance
    • "burn": Subtract amount from targetAuthority balance
    • "force_transfer": Move amount from sourceAuthority to destinationAuthority (admin-initiated)
    • "transfer": Move amount from sourceAuthority to destinationAuthority
  4. Use decimals to convert the raw amount string to a human-readable value (e.g., amount / 10^decimals)
  5. Use meta for enriched context: vesting schedule details on mints, timelock cancellation info on transfers, etc.
  6. Process sequentially to build current state

Pagination Example

For large datasets, use pagination:

query CapTableActivityPaginated {
tokenActivity(
mint: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
limit: 100
offset: 0
order: ASC
) {
activityType
signature
instructionIndex
innerIndex
slot
amount
decimals
destinationAuthority
targetAuthority
sourceAuthority
}
}

Fetch subsequent pages by incrementing offset:

  • Page 1: offset: 0, limit: 100
  • Page 2: offset: 100, limit: 100
  • Page 3: offset: 200, limit: 100

Admin Activity Query

Get all admin operations for a specific token in chronological order:

query AdminActivity {
tokenAdminActivity(
mint: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
limit: 1000
offset: 0
order: ASC
) {
activityType
signature
instructionIndex
slot
slotTimestamp
stackHeight
securityMint
createdAt

# Role fields
role
userWallet

# Group fields
groupId
groupIdFrom
groupIdTo

# Transfer rule fields
lockUntil
lockedUntil

# Other fields
holderGroupMax
frozen
holderId
paused
uuid
releaseCount
delayUntilFirstReleaseInSeconds
initialReleasePortionInBips
periodBetweenReleasesInSeconds
}
}

Admin Activity Pagination

query AdminActivityPaginated {
tokenAdminActivity(
mint: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
limit: 100
offset: 0
order: ASC
) {
activityType
signature
slot
securityMint
role
userWallet
groupId
frozen
paused
}
}

Fetch subsequent pages by incrementing offset:

  • Page 1: offset: 0, limit: 100
  • Page 2: offset: 100, limit: 100
  • Page 3: offset: 200, limit: 100

Data Ordering

The tokenActivity query orders results by slot, stack_height, instruction_index, inner_index, signature. Individual parsed instruction queries use slot ASC, stack_height ASC, instruction_index ASC.

  • Chronological order from oldest to newest (ascending slot)
  • Outer instructions before inner ones (ascending stack_height)
  • Sequential order within a transaction (ascending instruction_index)
  • Sub-ordering for multiple hooks within the same instruction (ascending inner_index)
  • Deterministic tie-breaking by signature

This ordering is optimized for cap table building and state reconstruction, where you need to process events in the exact order they occurred on-chain.