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
| Parameter | Type | Required | Description |
|---|---|---|---|
mint | String! | Yes | Token mint address to query |
limit | Int | No | Maximum number of results (default: 100) |
offset | Int | No | Number of results to skip (default: 0) |
order | SortOrder | No | Sort order: ASC (oldest first, default) or DESC (newest first) |
TokenActivity Response Fields
| Field | Type | Description |
|---|---|---|
activityType | String | Type of activity: "mint", "burn", "force_transfer", or "transfer" |
signature | String | Transaction signature (part of unique event ID) |
instructionIndex | Int | Index of instruction within transaction (part of unique event ID) |
innerIndex | Int? | Ordinal within the same outer instruction; only populated for transfer activity type (null for others). Part of the unique event ID for transfers. |
slot | String | Solana slot number |
slotTimestamp | String? | Unix timestamp of the slot |
stackHeight | Int? | Call stack height (1 = outer, >=2 = inner/CPI) |
amount | String | Token amount (as decimal string) |
mint | String | Token mint address |
createdAt | String? | Timestamp when record was indexed |
decimals | Int? | Token decimals from mint metadata. Null if the mint was not initialized through AccessControl. |
destinationAuthority | String? | Destination authority (populated for mints, force_transfers, and transfers) |
targetAuthority | String? | Target authority (populated for burns) |
sourceAuthority | String? | Source authority (populated for force_transfers and transfers) |
meta | TokenActivityMeta? | Enriched metadata for specific event types (see below) |
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).
| Field | Type | Description |
|---|---|---|
eventType | String | Meta event type (see table below) |
admin | String? | Admin authority that performed the operation |
fromAuthority | String? | Source wallet authority |
toAuthority | String? | Destination wallet authority |
amount | String? | Amount involved |
scheduleId | String? | Release schedule ID |
commencementTimestamp | String? | Schedule commencement timestamp |
cancelableBy | String? | Authority that can cancel the schedule |
scheduleReleaseCount | String? | Number of releases in the schedule |
scheduleDelayUntilFirstReleaseInSeconds | String? | Delay before first release (seconds) |
scheduleInitialReleasePortionInBips | String? | Initial release portion in basis points |
schedulePeriodBetweenReleasesInSeconds | String? | Period between releases (seconds) |
timelockId | String? | Timelock ID |
canceledBy | String? | Authority that canceled the timelock |
canceledAmount | String? | Amount returned from cancellation |
paidAmount | String? | Amount already paid out before cancellation |
target | String? | Target wallet of the timelock |
reclaimer | String? | Wallet that reclaimed canceled tokens |
Meta event types by activity type:
| Activity Type | Meta Event Type | Description |
|---|---|---|
mint | schedule_funded | Mint was part of a MintReleaseSchedule, populates schedule fields |
transfer | vesting_transfer | Transfer from a vesting schedule release |
transfer | vesting_transfer_timelock | Vesting transfer with associated timelock |
transfer | timelock_canceled | Transfer resulting from a timelock cancellation, populates cancellation fields |
force_transfer | force_transfer | Embedded 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 Type | Source | Notes |
|---|---|---|
mint | AccessControl::MintSecurities | Direct Token-2022 MintTo is impossible because the mint authority is the AccessControl PDA |
burn | AccessControl::BurnSecurities + Token-2022 Burn | Token holders can burn directly via Token-2022 without going through AccessControl. Deduplicated to avoid double-counting. |
force_transfer | AccessControl::ForceTransferBetween | Admin-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. |
transfer | TransferRestrictions::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
| Parameter | Type | Required | Description |
|---|---|---|---|
mint | String! | Yes | Token mint address (security mint) to query |
limit | Int | No | Maximum number of results (default: 100) |
offset | Int | No | Number of results to skip (default: 0) |
order | SortOrder | No | Sort order: ASC (oldest first, default) or DESC (newest first) |
TokenAdminActivity Response Fields
Common fields (present on every row):
| Field | Type | Description |
|---|---|---|
activityType | String | Type of admin operation (see Activity Types table below) |
signature | String | Transaction signature |
instructionIndex | Int | Index of instruction within the transaction |
slot | String? | Solana slot number |
slotTimestamp | String? | Unix timestamp of the slot |
stackHeight | Int? | Call stack height (1 = outer, >=2 = inner/CPI) |
securityMint | String | Token mint address |
createdAt | String? | Timestamp when record was indexed |
Activity-specific fields (populated depending on activityType, null otherwise):
| Field | Type | Description |
|---|---|---|
role | Int? | Role identifier (1 = Contract Admin, 2 = Reserve Admin, etc.) |
userWallet | String? | Wallet address of the affected user |
groupId | String? | Transfer restriction group ID |
groupIdFrom | String? | Source group ID (for transfer rules) |
groupIdTo | String? | Destination group ID (for transfer rules) |
lockUntil | String? | Lock-until timestamp for transfer rules |
lockedUntil | String? | Locked-until timestamp for allow transfer rules |
holderGroupMax | String? | Maximum number of holders in a group |
frozen | Boolean? | Whether the address is frozen |
holderId | String? | Holder ID for security associated accounts |
paused | Boolean? | Whether the token is paused |
uuid | String? | Release schedule UUID |
releaseCount | Int? | Number of releases in the schedule |
delayUntilFirstReleaseInSeconds | String? | Delay before the first release (seconds) |
initialReleasePortionInBips | Int? | Initial release portion in basis points |
periodBetweenReleasesInSeconds | String? | Period between releases (seconds) |
Activity Types
| Activity Type | Populated Fields | Notes |
|---|---|---|
grant_role | role, userWallet | Also generated from InitializeAccessControl (auto-grants Contract Admin to deployer) |
revoke_role | role, userWallet | |
freeze_wallet | userWallet | userWallet is the frozen wallet |
thaw_wallet | userWallet | userWallet is the thawed wallet |
initialize_transfer_restriction_group | groupId | Also generated from InitializeTransferRestrictionsData (auto-creates group 0) |
set_holder_group_max | groupId, holderGroupMax | |
initialize_transfer_rule | groupIdFrom, groupIdTo, lockUntil | |
set_allow_transfer_rule | groupIdFrom, groupIdTo, lockedUntil | |
set_address_permission | userWallet, groupId, frozen | |
initialize_security_associated_account | userWallet, groupId, holderId | |
revoke_security_associated_account | userWallet | |
pause | paused | |
create_release_schedule | uuid, releaseCount, delayUntilFirstReleaseInSeconds, initialReleasePortionInBips, periodBetweenReleasesInSeconds |
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
- Query returns all activities in chronological order (oldest first by default, use
order: DESCfor newest first) - Unique Event Identification: Each event should be uniquely identified using the composite key
signature+instructionIndex+innerIndex. For non-transfer types,innerIndexis always 0. This ensures proper deduplication when processing events across multiple queries or pagination. - Check
activityTypeto determine how to process each event:"mint": AddamounttodestinationAuthoritybalance"burn": SubtractamountfromtargetAuthoritybalance"force_transfer": MoveamountfromsourceAuthoritytodestinationAuthority(admin-initiated)"transfer": MoveamountfromsourceAuthoritytodestinationAuthority
- Use
decimalsto convert the rawamountstring to a human-readable value (e.g.,amount / 10^decimals) - Use
metafor enriched context: vesting schedule details on mints, timelock cancellation info on transfers, etc. - 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.