Skip to content

Read on-chain data

Reads are the simplest part of Rolink. Your server asks the API, and Rolink answers from its database (for wallet links) or from Solana (for everything on-chain, with balances and assets going through a short cache). Every method yields, needs no managed wallet, and raises "[Rolink] <code>: <message>" on failure.

All reads use your project’s cluster, devnet or mainnet, which you chose when you created the project. A mint that exists only on mainnet isn’t found from a devnet project, and the other way around.

The examples assume one shared client per server, kept in a ModuleScript that every script requires. The Quickstart covers installing the SDK itself.

ServerScriptService/RolinkClient.luau
local HttpService = game:GetService("HttpService")
local ServerScriptService = game:GetService("ServerScriptService")
local Rolink = require(ServerScriptService.Rolink)
return Rolink.new({
apiKey = HttpService:GetSecret("rolink_api_key"),
})
local address = rolink:GetWallet(player.UserId) -- string, or nil if they haven't linked

Rolink answers from its database, so this never touches Solana. The SDK caches each answer, including nil, for walletCacheSeconds (default 60). Live WalletLinked events overwrite the cache with the new address, WalletUnlinked events clear it when it holds the unlinked address, and an answer that was already in flight when an event landed never overwrites what the event wrote.

local rolink = Rolink.new({
apiKey = HttpService:GetSecret("rolink_api_key"),
walletCacheSeconds = 30, -- shorter if you don't use live events
})
local userIds = {}
for _, player in Players:GetPlayers() do
table.insert(userIds, player.UserId)
end
local wallets = rolink:GetWallets(userIds) -- { [userId]: address }
for userId, address in wallets do
print(userId, address)
end

The result only contains players who have linked a wallet. Players with fresh cache entries are answered from the cache, and the rest are looked up in bulk, 200 user IDs per request. The SDK splits longer lists for you, so 500 uncached players cost 3 requests. If you call the HTTP API directly, POST /v1/players/wallets refuses more than 200 IDs with invalid_request.

local sol = rolink:GetSolBalance(address) -- "1.5"

The balance at confirmed commitment, in SOL, as a decimal string. The HTTP API also returns the raw lamports.

local MY_MINT = "<your token mint address>"
local balance = rolink:GetTokenBalance(address, MY_MINT) -- "12.5", or "0"

The human-readable amount of one mint, summed across all of the wallet’s token accounts for that mint. It works for SPL Token and Token-2022 mints. A wallet that holds none returns "0". An address that isn’t a mint on your project’s cluster raises an error: invalid_mint, or internal when the RPC rejects the lookup itself.

for _, token in rolink:GetTokens(address) do
print(token.mint, token.uiAmount, token.decimals, token.amount)
end

One entry per mint across SPL Token and Token-2022:

FieldExampleMeaning
mint"EPjF…Dt1v"Mint address
amount"12500000"Raw amount in base units
decimals6The mint’s decimals
uiAmount"12.5"amount shifted by decimals, no trailing zeros

The list can include mints with a "0" balance (empty token accounts the wallet never closed), and NFTs held as classic SPL tokens show up as decimals = 0, amount = "1". Filter for what you need.

local COLLECTION = "<collection address>"
if rolink:OwnsCollection(address, COLLECTION) then
-- the wallet holds at least one asset from this collection
end
for _, asset in rolink:GetAssets(address, COLLECTION) do
print(asset.id, asset.name, asset.image, asset.interface)
end

GetAssets(address) lists the wallet’s NFTs, and GetAssets(address, collection) only those in that collection. OwnsCollection is #GetAssets(address, collection) > 0, so it costs one request. Each asset has id, name, collection, image and interface (such as "V1_NFT" or "ProgrammableNFT"). Any of the last four can be nil.

Only a verified collection counts, so a mint can’t pass itself off as part of yours. These reads work on devnet and mainnet, in one of two ways:

  • With a DAS endpoint for your project’s cluster, Rolink reads assets through the DAS (Digital Asset Standard) API. That also sees compressed NFTs and Metaplex Core assets, and gives each asset its image.
  • Without one, Rolink reads classic NFTs straight from the chain: Metaplex Token Metadata NFTs, programmable NFTs and editions. image is then nil.
local status = rolink:GetSignatureStatus(signature)
-- { signature = "5h6x…", status = "finalized", slot = 312345678, error = nil }

status is one of processed, confirmed, finalized, failed or unknown. Rolink searches the node’s transaction history, so older signatures resolve too. unknown means the node has no record of it, which isn’t proof it never landed. error is the on-chain error as a JSON string when status is failed. A malformed signature raises invalid_signature.

Use it to look up a signature from a ChainActivity event or one you stored. For transactions your managed wallet sent, GetTransaction is the better source.

There are two caches, and neither needs setup.

CacheWhereCoversLifetime
Wallet cacheEach game server (SDK)GetWallet, GetWalletswalletCacheSeconds, default 60, refreshed by live events
Read cacheRolinkSOL balances, token balances, assetsUp to 10 seconds

Rolink’s read cache is keyed by cluster, address, and mint or collection, and identical lookups that arrive at the same time share one RPC call. A popular wallet, such as one shown on a lobby leaderboard, costs Rolink one RPC lookup every few seconds no matter how many of your servers ask. Each call still counts as one request toward your project’s quota. Signature statuses and wallet lookups aren’t cached by Rolink.

Every amount is a decimal string: "1.5", "0", "184467440737.09551615". Luau numbers are doubles, which can’t represent large token amounts exactly, so Rolink keeps amounts as text end to end.

For display, use the string as is. For an approximate comparison, tonumber is fine:

if (tonumber(rolink:GetTokenBalance(address, MY_MINT)) or 0) >= 100 then
-- holds at least 100 tokens
end

When you need an exact answer, compare the raw base-unit amount from GetTokens as integer strings:

Exact comparison
local function trimZeros(digits: string): string
local trimmed = string.gsub(digits, "^0+", "")
return trimmed
end
-- True if the integer string `a` is greater than or equal to `b`.
local function atLeast(a: string, b: string): boolean
a, b = trimZeros(a), trimZeros(b)
if #a ~= #b then
return #a > #b
end
return a >= b
end
for _, token in rolink:GetTokens(address) do
if token.mint == MY_MINT and atLeast(token.amount, "100000000") then
-- at least 100 tokens of a 6-decimal mint
end
end

Two budgets apply to reads. Roblox allows about 500 HttpService requests per minute per game server, shared by everything in that server. Rolink counts every API call against your project’s beta quota: 100,000 requests per day (UTC) and 600 per minute, across all your servers. A few habits keep you well under both:

  • Every SDK method is one request, except GetWallets (one per 200 uncached players) and wallet lookups served from the cache (none).
  • Failures can cost more. The SDK retries network errors, 5xx answers and short-lived 429s up to 3 attempts in total, waiting 0.5 s and then 1.5 s. It doesn’t retry limits that a short wait can’t lift, such as daily_quota_exceeded.
  • Read on change, not on a timer. Read balances when a player joins and when WalletLinked fires, then refresh only when something happens that could change them. Avoid polling every few seconds per player.
  • Batch wallet lookups. For leaderboards or a full server, call GetWallets once instead of GetWallet in a loop.
  • Use one client per server. Put Rolink.new in a ModuleScript and require it everywhere, so all scripts share one wallet cache and one event subscription.
  • Prefer events to polling. Use TransactionUpdated and ChainActivity instead of calling GetSignatureStatus in a loop.

The dashboard’s Overview shows today’s requests against the daily quota, and the last 14 days. Limits and quotas lists every limit.

Wrap reads in pcall and branch on Rolink.errorCode:

local ok, result = pcall(rolink.GetAssets, rolink, address, COLLECTION)
if not ok then
local code = Rolink.errorCode(result)
if code == "invalid_address" then
warn("Not a Solana address:", address)
else
warn(result) -- network_error, rate_limited, internal, unauthorized...
end
return
end
CodeWhen
invalid_user_idThe user ID isn’t a positive integer
invalid_addressThe address (or a mint or collection) isn’t a Solana address
invalid_mintThe mint doesn’t exist on your project’s cluster, or isn’t a token mint
invalid_signatureGetSignatureStatus got something that isn’t a transaction signature
unauthorizedThe API key is missing, mistyped or revoked
rate_limitedMore than 600 requests in a minute for this project
daily_quota_exceededThe project used its 100,000 requests for the day (UTC)
internalRolink hit an unexpected error, usually the RPC failing. Try again later
network_errorThe request never got an answer: HTTP requests disabled, or a Secret whose domain doesn’t match the API host

The Error codes reference lists every code.