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.
local HttpService = game:GetService("HttpService")local ServerScriptService = game:GetService("ServerScriptService")local Rolink = require(ServerScriptService.Rolink)
return Rolink.new({ apiKey = HttpService:GetSecret("rolink_api_key"),})Find a player’s wallet
Section titled “Find a player’s wallet”One player: GetWallet
Section titled “One player: GetWallet”local address = rolink:GetWallet(player.UserId) -- string, or nil if they haven't linkedRolink 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})Many players: GetWallets
Section titled “Many players: GetWallets”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)endThe 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.
Read balances
Section titled “Read balances”SOL: GetSolBalance
Section titled “SOL: GetSolBalance”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.
One token: GetTokenBalance
Section titled “One token: GetTokenBalance”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.
Everything: GetTokens
Section titled “Everything: GetTokens”for _, token in rolink:GetTokens(address) do print(token.mint, token.uiAmount, token.decimals, token.amount)endOne entry per mint across SPL Token and Token-2022:
| Field | Example | Meaning |
|---|---|---|
mint | "EPjF…Dt1v" | Mint address |
amount | "12500000" | Raw amount in base units |
decimals | 6 | The 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.
Check NFTs and collections
Section titled “Check NFTs and collections”GetAssets and OwnsCollection
Section titled “GetAssets and OwnsCollection”local COLLECTION = "<collection address>"
if rolink:OwnsCollection(address, COLLECTION) then -- the wallet holds at least one asset from this collectionend
for _, asset in rolink:GetAssets(address, COLLECTION) do print(asset.id, asset.name, asset.image, asset.interface)endGetAssets(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.
imageis thennil.
Check a transaction: GetSignatureStatus
Section titled “Check a transaction: GetSignatureStatus”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.
Caching
Section titled “Caching”There are two caches, and neither needs setup.
| Cache | Where | Covers | Lifetime |
|---|---|---|---|
| Wallet cache | Each game server (SDK) | GetWallet, GetWallets | walletCacheSeconds, default 60, refreshed by live events |
| Read cache | Rolink | SOL balances, token balances, assets | Up 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.
Amounts are strings
Section titled “Amounts are strings”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 tokensendWhen you need an exact answer, compare the raw base-unit amount from GetTokens as integer strings:
local function trimZeros(digits: string): string local trimmed = string.gsub(digits, "^0+", "") return trimmedend
-- 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 >= bend
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 endendStay within your budgets
Section titled “Stay within your budgets”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,
5xxanswers and short-lived429s 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 asdaily_quota_exceeded. - Read on change, not on a timer. Read balances when a player joins and when
WalletLinkedfires, 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
GetWalletsonce instead ofGetWalletin a loop. - Use one client per server. Put
Rolink.newin a ModuleScript andrequireit everywhere, so all scripts share one wallet cache and one event subscription. - Prefer events to polling. Use
TransactionUpdatedandChainActivityinstead of callingGetSignatureStatusin 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.
Handle errors
Section titled “Handle errors”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 returnend| Code | When |
|---|---|
invalid_user_id | The user ID isn’t a positive integer |
invalid_address | The address (or a mint or collection) isn’t a Solana address |
invalid_mint | The mint doesn’t exist on your project’s cluster, or isn’t a token mint |
invalid_signature | GetSignatureStatus got something that isn’t a transaction signature |
unauthorized | The API key is missing, mistyped or revoked |
rate_limited | More than 600 requests in a minute for this project |
daily_quota_exceeded | The project used its 100,000 requests for the day (UTC) |
internal | Rolink hit an unexpected error, usually the RPC failing. Try again later |
network_error | The 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.
Next steps
Section titled “Next steps”- Live events: keep the wallet cache fresh across servers.
- Send transactions: pay players from your project’s managed wallet.
- Luau SDK reference: every method, type and signal.
- Limits and quotas: the beta quotas and every other limit.
