Luau SDK reference
The SDK is one ModuleScript named Rolink with two children, Http and Signal. It calls the Rolink API over HttpService with your project’s API key, and receives live events over MessagingService. This page covers everything the module exports. To install it, download Rolink.rbxmx and add it as shown in the Quickstart.
local HttpService = game:GetService("HttpService")local Rolink = require(game:GetService("ServerScriptService").Rolink)
local rolink = Rolink.new({ apiKey = HttpService:GetSecret("rolink_api_key"),})At a glance
Section titled “At a glance”| Member | Kind | Yields | Returns |
|---|---|---|---|
Rolink.new(config) | constructor | no | a client |
Rolink.DEFAULT_API_URL | constant | string | |
Rolink.errorCode(err) | function | no | string? |
Rolink.newIdempotencyKey() | function | no | string |
:GetProject() | method | yes | Project |
:GetWallet(userId) | method | on a cache miss | string? |
:GetWallets(userIds) | method | on a cache miss | { [number]: string } |
:GetSolBalance(address) | method | yes | string |
:GetTokenBalance(address, mint) | method | yes | string |
:GetTokens(address) | method | yes | { TokenBalance } |
:GetAssets(address, collection?) | method | yes | { Asset } |
:OwnsCollection(address, collection) | method | yes | boolean |
:GetSignatureStatus(signature) | method | yes | SignatureStatus |
:TransferSol(params) | method | yes | TxResult |
:TransferToken(params) | method | yes | TxResult |
:MintToken(params) | method | yes | TxResult |
:GetTransaction(idempotencyKey) | method | yes | TxResult |
:Destroy() | method | no | nothing |
.WalletLinked | signal | (userId, address, player?) | |
.WalletUnlinked | signal | (userId, address, player?) | |
.TransactionUpdated | signal | (TxUpdate) | |
.ChainActivity | signal | (ChainActivity) |
Conventions
Section titled “Conventions”- Server only.
Rolink.newassertsRunService:IsServer(). Your project API key must never reach a client. - One project per client. Everything a client reads or sends belongs to the project its API key was created in.
- Methods yield. Every method that talks to Rolink yields until the request finishes. Call them from a script or a
task.spawnthread, never from a context that cannot yield. - Errors are strings. A failed call raises a string such as
"[Rolink] <code>: <message> (HTTP <status>)". Wrap calls inpcalland branch onRolink.errorCode(err). The codes are listed in Error codes. - Amounts are decimal strings. Balances come back as strings such as
"12.5", and transaction amounts are sent as strings, so large token amounts stay exact. Numbers are accepted on input and converted. - Recipients. Transaction methods take
toas a player’sUserId(a number) or a Solana address (a string). - Every request counts. Each call that reaches the API counts toward the project’s request quota. Answers served from the SDK’s wallet cache don’t.
local ok, result = pcall(rolink.GetWallet, rolink, player.UserId)if not ok then warn(result) -- "[Rolink] network_error: ..." returnendConstructor
Section titled “Constructor”Rolink.new
Section titled “Rolink.new”Rolink.new(config: Config) -> RolinkCreates a client. Unless subscribe is false, it also starts a background thread that subscribes to the events topic. The constructor itself never yields.
| Field | Type | Default | Description |
|---|---|---|---|
apiKey | Secret or string | required | Your project API key (rlk_live_...) from the dashboard. Use HttpService:GetSecret("rolink_api_key"). A plain string also works in tests; never commit a key to a script. It is sent in the x-api-key header. |
apiUrl | string? | Rolink.DEFAULT_API_URL | Base URL of the Rolink API. Trailing slashes are removed. Leave it unset in your game; set it only to point the SDK at a test server. |
topic | string? | "rolink" | MessagingService topic to subscribe to. Must match the project’s event topic in the dashboard. |
walletCacheSeconds | number? | 60 | How long GetWallet and GetWallets answers are cached, including “no wallet” answers. Live link and unlink events update the cache anyway. |
subscribe | boolean? | true | Set to false to skip the MessagingService subscription. Signals then never fire. |
Rolink.new raises a plain assertion error (with no code) when:
- it runs on a client:
[Rolink] Rolink runs on the server only; never expose your project API key to clients configis not a table:[Rolink] Rolink.new expects a config tableconfig.apiKeyisnil:[Rolink] config.apiKey is required (use HttpService:GetSecret)config.apiUrlis set to something other than a string:[Rolink] config.apiUrl must be a string
The API key isn’t checked until the first request. Call GetProject at startup if you want to fail early.
Static members
Section titled “Static members”Rolink.DEFAULT_API_URL
Section titled “Rolink.DEFAULT_API_URL”Rolink.DEFAULT_API_URL: stringThe base URL used when config.apiUrl is not set: https://api.rolink.tech, the hosted Rolink API.
Rolink.errorCode
Section titled “Rolink.errorCode”Rolink.errorCode(err: any) -> string?Extracts the code from an error raised by a Rolink method, such as "wallet_not_linked" from "[Rolink] wallet_not_linked: Player 42 has not linked a wallet (HTTP 404)".
It returns nil when err is not a string or does not start with [Rolink] <code>:. Assertion errors raised before a request is sent (a missing config field, a bad to or amount) have no code, so they also return nil.
local ok, err = pcall(rolink.TransferToken, rolink, params)if not ok then local code = Rolink.errorCode(err) if code == "wallet_not_linked" then -- ask the player to link a wallet elseif code == "player_rate_limited" then -- try again later else warn(err) endendRolink.newIdempotencyKey
Section titled “Rolink.newIdempotencyKey”Rolink.newIdempotencyKey() -> stringReturns a random GUID from HttpService:GenerateGUID(false), without braces.
Project
Section titled “Project”GetProject
Section titled “GetProject”rolink:GetProject() -> ProjectReturns the project the API key belongs to, as a Project. Useful at startup to check that the key works, to find the managed wallet’s address, and to get the linking page URL to show players.
- Calls
GET /v1/project. Not cached: each call is a request. - Errors. The common errors.
local project = rolink:GetProject()print(project.name, project.cluster) -- "My Game" "devnet"print(project.linkUrl) -- the hosted linking page for this gameif not project.events then warn("Live events are off: add a universe ID and Open Cloud key in the dashboard")endWallet methods
Section titled “Wallet methods”GetWallet
Section titled “GetWallet”rolink:GetWallet(userId: number) -> string?Returns the Solana address the player linked to this project, or nil if they have not linked one.
- Calls
GET /v1/players/:userId/wallet. - Caching. Answers, including
nil, are cached forwalletCacheSeconds. A cached answer returns without yielding.WalletLinkedandWalletUnlinkedevents update the cache as they arrive. See Wallet cache. - Errors.
invalid_user_idwhenuserIdis not a positive integer, plus the common errors.
GetWallets
Section titled “GetWallets”rolink:GetWallets(userIds: { number }) -> { [number]: string }Looks up many players at once. The result maps each UserId to its address and only contains players who have linked a wallet. Players without a wallet are absent from the table.
- Calls
POST /v1/players/walletsonce per 200 uncached players. Cached players are answered from the cache, and an empty or fully cached list sends no request. - Caching. Same cache as
GetWallet. Every looked-up player is cached, with or without a wallet. - Errors.
invalid_requestwhen an id is not a positive integer, plus the common errors.
local Players = game:GetService("Players")
local ids = {}for _, player in Players:GetPlayers() do table.insert(ids, player.UserId)endlocal wallets = rolink:GetWallets(ids)print(wallets[ids[1]]) -- address, or nilRead methods
Section titled “Read methods”Reads use the project’s cluster (devnet or mainnet). Balance, token and asset reads are cached by Rolink for about 10 seconds, so repeated calls within that window return the same value. Signature lookups are not cached.
GetSolBalance
Section titled “GetSolBalance”rolink:GetSolBalance(address: string) -> stringReturns the SOL balance as a decimal string without trailing zeros, such as "1.5" or "0". Read at confirmed commitment.
- Calls
GET /v1/wallets/:address/balance. - Errors.
invalid_address, plus the common errors.
GetTokenBalance
Section titled “GetTokenBalance”rolink:GetTokenBalance(address: string, mint: string) -> stringReturns the wallet’s balance of one SPL or Token-2022 mint in token units, such as "12.5". Returns "0" when the wallet holds none. Balances across several token accounts of the same mint are added together.
- Calls
GET /v1/wallets/:address/tokens?mint=. - Errors.
invalid_addresswhenaddressormintis not an address.invalid_mintwhenmintdoes not exist on the project’s cluster or is not a token mint. Plus the common errors.
GetTokens
Section titled “GetTokens”rolink:GetTokens(address: string) -> { TokenBalance }Returns one TokenBalance per mint the wallet has a token account for, across the Token and Token-2022 programs. Empty token accounts are included with an amount of "0".
- Calls
GET /v1/wallets/:address/tokens. - Errors.
invalid_address, plus the common errors.
GetAssets
Section titled “GetAssets”rolink:GetAssets(address: string, collection: string?) -> { Asset }Returns the NFTs and other digital assets the wallet owns, as Asset tables. With collection, only assets grouped under that collection address are returned.
- Calls
GET /v1/wallets/:address/assets[?collection=]. - Limit. Rolink reads the first page of up to 1,000 assets.
- Collections. Only a verified collection counts.
- Errors.
invalid_addresswhenaddressorcollectionis not an address. Plus the common errors.
OwnsCollection
Section titled “OwnsCollection”rolink:OwnsCollection(address: string, collection: string) -> booleanReturns true when GetAssets(address, collection) returns at least one asset. Same request, cache and errors as GetAssets.
if rolink:OwnsCollection(address, FOUNDERS_COLLECTION) then grantFounderBadge(player)endGetSignatureStatus
Section titled “GetSignatureStatus”rolink:GetSignatureStatus(signature: string) -> SignatureStatusReturns the status of any transaction signature as a SignatureStatus. Rolink searches the RPC node’s transaction history and does not cache this read.
- Calls
GET /v1/signatures/:signature. - Errors.
invalid_signaturewhensignatureis not a base58 transaction signature, plus the common errors.
Transaction methods
Section titled “Transaction methods”These methods ask Rolink to sign and send a transaction with the project’s managed wallet. The wallet must exist, and the transaction must fit the policy set in the dashboard. The guide is Send transactions.
Each call waits up to 15 seconds for confirmation and returns a TxResult:
status | Meaning |
|---|---|
confirmed | The transaction landed. |
failed | Nothing reached the recipient. error says why. Calling again with the same key and the same parameters retries it. |
submitted | Sent but not yet confirmed. The final result arrives through TransactionUpdated and GetTransaction. |
pending | Another request with this key is being prepared right now. Rare. Check again with GetTransaction. |
Idempotency. Rolink remembers every idempotencyKey a project uses, permanently:
- the same key and the same parameters after a
pending,submittedorconfirmedattempt returns that attempt and sends nothing new - the same key and the same parameters after a
failedattempt sends again - the same key with different parameters raises
idempotency_conflict.to = 42andto = "<the same wallet's address>"count as different parameters, and so do"1"and"1.0".
A request refused by validation, the policy or a quota never reserves its key, so you can fix it and reuse the key. The exceptions are rpc_error and wallet_unavailable: Rolink couldn’t build or sign the transaction, so it records the attempt as failed. Send the same parameters again with the same key to retry it.
Errors common to the three methods, in the order Rolink checks them: invalid_request (and, for the token methods, invalid_address for mint), wallet_not_configured, idempotency_conflict, wallet_not_linked or invalid_address for to, then the asset and amount checks listed under each method, then daily_tx_quota_exceeded, recipient_rate_limited, player_rate_limited, the hourly budget (invalid_limit_config if the stored budget doesn’t fit the asset’s decimals, otherwise hourly_budget_exceeded), and finally wallet_unavailable and rpc_error. Plus the common errors. The full sequence is in the HTTP API’s order of checks.
TransferSol
Section titled “TransferSol”rolink:TransferSol(params: TransferSolParams) -> TxResultSends SOL from the managed wallet.
| Field | Type | Description |
|---|---|---|
to | Recipient | A UserId with a linked wallet, or a Solana address. |
amount | Amount | SOL to send, such as "0.01". At most 9 decimals, greater than zero, and no more than the policy’s Max SOL per transfer. |
idempotencyKey | string | 1 to 128 characters, unique per intended payment. |
- Calls
POST /v1/tx/transfer-sol. - Asset and amount errors.
sol_transfers_disabled,invalid_amount,amount_over_limit,invalid_limit_config.
local ok, result = pcall(rolink.TransferSol, rolink, { to = player.UserId, amount = "0.01", idempotencyKey = `tournament:{tournamentId}:{player.UserId}`,})if ok and result.status == "confirmed" then print("paid", result.signature)endTransferToken
Section titled “TransferToken”rolink:TransferToken(params: TokenParams) -> TxResultSends an SPL or Token-2022 token from the managed wallet’s associated token account. The recipient’s associated token account is created if it does not exist, paid for by the managed wallet.
| Field | Type | Description |
|---|---|---|
to | Recipient | A UserId with a linked wallet, or a Solana address. |
mint | string | The token mint. It must be under Allowed mints in the policy. |
amount | Amount | Token units, such as "25". At most the mint’s decimals, greater than zero, and no more than the mint’s limit. |
idempotencyKey | string | 1 to 128 characters. |
- Calls
POST /v1/tx/transfer-token. - Asset and amount errors.
invalid_address(formint, checked first),mint_not_allowed,invalid_mint,invalid_amount,amount_over_limit,invalid_limit_config.
MintToken
Section titled “MintToken”rolink:MintToken(params: TokenParams) -> TxResultMints new tokens to the recipient. The managed wallet must be the mint authority of mint, or the result is failed. Same fields, policy and errors as TransferToken.
- Calls
POST /v1/tx/mint-token.
GetTransaction
Section titled “GetTransaction”rolink:GetTransaction(idempotencyKey: string) -> TxResultReturns the current state of a transaction request. It answers immediately. If the request is still submitted, Rolink resumes checking its confirmation in the background. It works whether or not the managed wallet exists.
- Calls
GET /v1/tx/:idempotencyKey. The key is URL-encoded, so keys with:or/are safe. - Errors.
not_foundwhen the project has no request with this key, plus the common errors.
Lifecycle
Section titled “Lifecycle”Destroy
Section titled “Destroy”rolink:Destroy() -> ()Disconnects the MessagingService subscription and every handler connected to the four signals. If the subscription is still being set up, it is disconnected as soon as it succeeds. The wallet cache is left as is.
Signals
Section titled “Signals”Signals fire on every server that has a client, and only when the project has live events configured (a universe ID and an Open Cloud API key in the dashboard). Delivery is best effort, so treat events as hints and call Rolink when you need the authoritative state. Payloads are described in Events.
WalletLinked
Section titled “WalletLinked”rolink.WalletLinked: Signal<number, string, Player?>-- handler(userId: number, address: string, player: Player?)Fires when a player links a wallet to this project, including when they replace their wallet or link the same one again. player is set when that player is on this server. Before the signal fires, the wallet cache is updated with the new address.
WalletUnlinked
Section titled “WalletUnlinked”rolink.WalletUnlinked: Signal<number, string, Player?>-- handler(userId: number, address: string, player: Player?)Fires when a player unlinks their wallet, or when another Roblox account proves ownership of it in this project and takes it over. In the second case userId is the account that lost the wallet. A player who swaps to a new wallet only fires WalletLinked. If the cached address for userId matches address, it is dropped from the cache.
TransactionUpdated
Section titled “TransactionUpdated”rolink.TransactionUpdated: Signal<TxUpdate>-- handler(update: TxUpdate)Fires when a transaction sent by the managed wallet reaches confirmed or failed, on every server. It also fires for transactions whose call already returned that status. See TxUpdate.
rolink.TransactionUpdated:Connect(function(update) if update.status == "confirmed" then markPaid(update.idempotencyKey) endend)ChainActivity
Section titled “ChainActivity”rolink.ChainActivity: Signal<ChainActivity>-- handler(activity: ChainActivity)Fires once for each new transaction that touches one of the project’s watched addresses, oldest first. See ChainActivity and Watch addresses.
Signal and Connection
Section titled “Signal and Connection”Each signal is a small signal object from Signal.luau. Every handler runs in its own thread through task.spawn, so a handler that errors or yields never blocks the others.
| Member | Description |
|---|---|
signal:Connect(handler) -> Connection | Calls handler every time the signal fires. |
signal:Once(handler) -> Connection | Calls handler the next time only, then disconnects. |
signal:DisconnectAll() | Disconnects every handler. |
connection.Connected | true until the connection is disconnected. |
connection:Disconnect() | Stops this handler. |
signal:Fire(...) also exists. The SDK uses it internally, and calling it yourself fires your own handlers on this server only.
All types are exported from the module, so you can write Rolink.TxResult in type annotations once you require it.
Config
Section titled “Config”export type Config = { apiKey: any, -- Secret or string apiUrl: string?, -- default Rolink.DEFAULT_API_URL topic: string?, -- default "rolink" walletCacheSeconds: number?, -- default 60 subscribe: boolean?, -- default true}Project
Section titled “Project”export type Project = { id: string, name: string, slug: string, -- used in the linking page URL cluster: "devnet" | "mainnet", walletAddress: string?, -- the managed wallet, nil until created in the dashboard linkUrl: string, -- the hosted linking page for this project events: boolean, -- true when a universe ID and an Open Cloud key are set}events says that both settings are present, not that Roblox accepts them. Use the test event in the dashboard’s Settings to check that.
TokenBalance
Section titled “TokenBalance”export type TokenBalance = { mint: string, amount: string, -- raw base units, e.g. "2500000" decimals: number, uiAmount: string, -- token units, e.g. "2.5"}export type Asset = { id: string, -- asset address name: string?, collection: string?, -- collection address, if grouped image: string?, -- image URL from the asset metadata interface: string?, -- DAS interface, e.g. "V1_NFT"}SignatureStatus
Section titled “SignatureStatus”export type SignatureStatus = { signature: string, status: "processed" | "confirmed" | "finalized" | "failed" | "unknown", slot: number?, -- nil when unknown error: string?, -- the on-chain error as JSON, when failed}unknown means the RPC node has no record of the signature.
TxStatus
Section titled “TxStatus”export type TxStatus = "pending" | "submitted" | "confirmed" | "failed"TxResult
Section titled “TxResult”export type TxResult = { idempotencyKey: string, kind: string, -- "transfer-sol" | "transfer-token" | "mint-token" status: TxStatus, signature: string?, -- set once the transaction is signed recipient: string, -- the resolved wallet address error: string?, -- why it failed}TxUpdate
Section titled “TxUpdate”export type TxUpdate = { idempotencyKey: string, kind: string, status: "confirmed" | "failed", signature: string?, userId: number?, -- the recipient player, if known error: string?,}userId is set when the request used to = userId, or when the recipient address was linked to a player in this project at the time of the request.
ChainActivity
Section titled “ChainActivity”export type ChainActivity = { name: string, -- the watch name from the dashboard address: string, signature: string, slot: number, failed: boolean, -- true when the transaction failed on-chain}Recipient
Section titled “Recipient”export type Recipient = number | stringA number is a UserId and is sent as { "userId": n }. The player must have linked a wallet to this project, or Rolink raises wallet_not_linked. A string is a Solana address and is sent as { "address": s }. A numeric string such as "42" is treated as an address, so pass player.UserId itself. Any other type raises an assertion error before the request is sent.
Amount
Section titled “Amount”export type Amount = string | numberA decimal string such as "1.5", or a number. See Amount conversion.
TransferSolParams
Section titled “TransferSolParams”export type TransferSolParams = { to: Recipient, amount: Amount, idempotencyKey: string,}TokenParams
Section titled “TokenParams”export type TokenParams = { to: Recipient, mint: string, amount: Amount, idempotencyKey: string,}Behaviour details
Section titled “Behaviour details”HTTP requests and retries
Section titled “HTTP requests and retries”Every request goes through HttpService:RequestAsync to apiUrl with the headers x-api-key and Content-Type: application/json. A request is tried up to 3 times, waiting 0.5 seconds before the second attempt and 1.5 seconds before the third. It is retried when:
RequestAsyncitself fails (HTTP requests disabled, DNS or connection failure, timeout)- Rolink answers with a 5xx status
- Rolink answers 429, except for
recipient_rate_limited,player_rate_limited,hourly_budget_exceeded,daily_quota_exceededanddaily_tx_quota_exceeded, because waiting a second does not lift an hourly or daily cap. The per-minuterate_limitedis retried.
Other 4xx answers are raised at once. Retrying a transaction is safe because it always carries its idempotency key. A retried request that already went through returns the original result.
After the last attempt, the SDK raises one of these:
| Error string | When |
|---|---|
[Rolink] <code>: <message> (HTTP <status>) | Rolink answered with a JSON error body. |
[Rolink] http_<status>: <status message> (HTTP <status>) | The answer had no Rolink error body, for example from a proxy. |
[Rolink] network_error: <HttpService error> | No HTTP answer at all. |
Common errors
Section titled “Common errors”Any method that sends a request can raise:
unauthorizedwhen the API key is missing, revoked, or belongs to a deleted projectrate_limitedanddaily_quota_exceededwhen the project is over its request quotainternalwhen Rolink hit an unexpected error, such as an RPC failure during a readnetwork_errorandhttp_<status>from the SDK itself
URL encoding
Section titled “URL encoding”Every value placed in a URL path or query string goes through HttpService:UrlEncode(tostring(value)): user ids, addresses, mints, collections, signatures and idempotency keys. Values that come from players cannot change the route.
Wallet cache
Section titled “Wallet cache”GetWallet and GetWallets share one cache, keyed by UserId. Each entry, whether it holds an address or “no wallet”, expires after walletCacheSeconds.
Live events keep it fresh:
wallet_linkedstores the new address, then firesWalletLinked.wallet_unlinkeddrops the entry if it still holds that address, then firesWalletUnlinked.
Each event also bumps a per-player version. A lookup that was already in flight when an event arrived does not overwrite the newer value with its older answer.
Without live events, a player who links while a “no wallet” answer is cached is seen as unlinked until the entry expires. Lower walletCacheSeconds if your project runs without Open Cloud.
Live event subscription
Section titled “Live event subscription”Rolink.new subscribes to topic with MessagingService:SubscribeAsync in a background thread. If that fails, it warns [Rolink] could not subscribe to topic "<topic>" (attempt n/5): <error> and tries again after 2, 4, 8 and 16 seconds, for 5 attempts in all. After the fifth failure it gives up, and the signals stay silent until the server restarts.
Incoming messages that are not JSON, that have no "v": 1, or that have a type the SDK doesn’t handle (such as the dashboard’s ping test message) are ignored.
Amount conversion
Section titled “Amount conversion”A string amount is sent unchanged, and Rolink checks it against ^\d+(\.\d+)?$. So "1.5" is valid, while "1,5", " 1.5", ".5" and "-1" are refused with invalid_request.
A number is converted with string.format("%.9f", amount), then trailing zeros and a trailing dot are removed:
| Number | Sent as |
|---|---|
1.5 | "1.5" |
10 | "10" |
0.1 | "0.1" |
0.0000000001 | "0", which Rolink refuses with invalid_amount |
Negative numbers and NaN raise an assertion error before the request is sent. Numbers above 2^53 cannot be represented exactly in Luau, so pass large amounts as strings.
