Live events
When something happens in your project (a player links a wallet, a transaction settles, a watched address moves), Rolink publishes a small JSON message to your experience through Roblox Open Cloud MessagingService. Every server running the SDK is subscribed to the same topic and turns those messages into signals: WalletLinked, WalletUnlinked, TransactionUpdated and ChainActivity.
Rolink publishes with an Open Cloud key that you create and own, so messages only ever reach your experience. Live events are optional. Without them, everything else works: servers learn about new links when their wallet cache expires, and transaction results come from GetTransaction.
Turn on live events
Section titled “Turn on live events”-
Create an Open Cloud API key. In Creator Dashboard, open Open Cloud > API Keys and create a key. Under access permissions, add Messaging Service, select your experience, and grant the
universe-messaging-service:publishoperation. Nothing else is needed. -
Check its restrictions. If you restrict the key’s accepted IP addresses, the restriction must allow Rolink’s requests. If you set an expiration date, pick one you’ll remember to renew: once the key expires, events stop until you save a new one.
-
Find your universe ID. It’s the ID of the experience as a whole, not the place ID in the experience’s URL. Creator Dashboard shows it for each experience.
-
Save both in the dashboard. Open your project’s Settings and fill in the Live events card:
Field Value Universe ID Digits only. Open Cloud API key The key from step 1. Leave the field empty later to keep the saved key. Topic The MessagingService topic Rolink publishes to. Keep rolinkunless you need another. Letters, digits,-and_, up to 80 characters.Rolink stores the key encrypted with AES-256-GCM and decrypts it only to publish. The dashboard never shows it again. To remove it, tick Remove the saved key and save. Changes apply to new events right away.
-
Send a test event. Under Send a test event, choose Send test event. Rolink publishes a
pingmessage with your saved settings and shows whether Roblox accepted it. If Roblox refuses it, you see its HTTP status and a hint: a401or403usually means the key lacks the publish permission on this experience, and a404usually means the universe ID is wrong.
Once both values are saved, the project card shows events as on, and rolink:GetProject().events returns true in game.
On the game side there’s nothing to configure: Rolink.new subscribes to the topic "rolink" by default. If you change the topic in the dashboard, pass the same value as topic:
local rolink = Rolink.new({ apiKey = HttpService:GetSecret("rolink_api_key"), topic = "rolink-staging", -- must equal the event topic in the dashboard})The Quick start on the project’s Overview includes topic automatically when you’ve changed it.
Event payloads
Section titled “Event payloads”Every message is JSON with "v": 1 and a type. The SDK ignores messages that don’t parse, have another version, or have a type it doesn’t know.
// A player linked a wallet (new link, or swapped to another wallet){ "v": 1, "type": "wallet_linked", "userId": 42, "address": "9xQe…" }
// A player unlinked, or another account took over their wallet{ "v": 1, "type": "wallet_unlinked", "userId": 42, "address": "9xQe…" }
// A transaction from the managed wallet reached a final status{ "v": 1, "type": "tx", "key": "quest:42:intro", "kind": "transfer-token", "status": "confirmed", "signature": "5h6x…", "userId": 42, "error": null }
// A new transaction touched a watched address{ "v": 1, "type": "watch", "name": "treasury", "address": "…", "signature": "…", "slot": 312345678, "failed": false }
// The dashboard's "Send test event"{ "v": 1, "type": "ping", "at": 1767225600000 }When each one is published:
| Type | Published when |
|---|---|
wallet_linked | A player proves ownership of a wallet on your linking page, including when they replace their previous wallet. |
wallet_unlinked | A player unlinks, or another Roblox account proves ownership of their wallet in your project. Swapping your own wallet only sends wallet_linked. |
tx | A transaction becomes confirmed or failed, whether that happened during the request or later. pending and submitted are never published, and neither is a request Rolink marks failed because it was abandoned before anything was sent. |
watch | Rolink finds a new transaction on one of your watched addresses. See Watch addresses. |
ping | You choose Send test event in the dashboard. No signal fires for it. |
For tx, userId is the player the transaction went to (directly, or through an address linked to them), or null. kind is transfer-sol, transfer-token or mint-token.
SDK signals
Section titled “SDK signals”| Signal | Arguments | Notes |
|---|---|---|
WalletLinked | userId: number, address: string, player: Player? | Also updates this server’s wallet cache. |
WalletUnlinked | userId: number, address: string, player: Player? | Clears the cached wallet if it was this address. |
TransactionUpdated | { idempotencyKey, kind, status, signature?, userId?, error? } | status is "confirmed" or "failed". The event’s key is renamed idempotencyKey. |
ChainActivity | { name, address, signature, slot, failed } | name is the watch name from the dashboard. |
player is the Player object when that user is on the server that received the event, and nil everywhere else. Signals support Connect and Once, and both return a connection with Disconnect(). Each handler runs in its own thread, so one that errors or yields doesn’t block the others.
local ServerScriptService = game:GetService("ServerScriptService")local rolink = require(ServerScriptService.RolinkClient)
rolink.WalletLinked:Connect(function(userId, address, player) if not player then return -- that player is on another server end print(`{player.Name} linked {address}`)end)
rolink.WalletUnlinked:Connect(function(userId, address, player) if player then print(`{player.Name} unlinked {address}`) endend)
rolink.TransactionUpdated:Connect(function(update) -- Fires on every server, for every final result. Only act on keys this server cares about. if update.status == "confirmed" then print(`{update.idempotencyKey} confirmed: {update.signature}`) else warn(`{update.idempotencyKey} failed: {update.error}`) endend)
rolink.ChainActivity:Connect(function(activity) print(`new transaction on "{activity.name}": {activity.signature}`)end)Treat events as hints
Section titled “Treat events as hints”MessagingService delivers on a best-effort basis. A message can arrive late or not at all, and a server that starts after an event never sees it. Design for that:
- Read the API when it matters. Call
GetWalletbefore acting on a wallet, andGetTransactionfor the authoritative status of a transaction. The SDK’s wallet cache already does this for you when it expires. - Don’t depend on order. Two events about the same player can arrive in either order. Re-read rather than applying them as a sequence.
- Expect duplicates for watches. An interrupted poll can publish the same
watchevent twice. Usesignatureto deduplicate. - Filter on every server. Each event reaches every subscribed server in the universe, across all of its places. Use
player,userIdoridempotencyKeyto decide whether this server should react.
Publishing never blocks the request that caused it, and a player never waits on Open Cloud to finish linking. Rolink makes up to 4 attempts when Open Cloud answers with a rate limit (429) or a server error, or can’t be reached, backing off 0.5, 1 and 2 seconds between them. Other refusals, such as 401, 403 or 404, are dropped without a retry. If events stop arriving, send a test event from the dashboard: it shows Roblox’s answer.
Message size
Section titled “Message size”MessagingService rejects messages over 1 kB. Rolink’s events are well under that, with one exception it handles itself: if a tx event’s error would push the message over the limit, Rolink cuts that error to its first 300 characters. Call GetTransaction for the full text.
If you publish your own messages to the topic, keep them small too, and prefer a different topic so the SDK doesn’t have to skip them.
Subscription and retries
Section titled “Subscription and retries”Rolink.new subscribes in the background, so it never yields. If SubscribeAsync fails, the SDK warns and tries again, for up to 5 attempts in total, waiting 2, 4, 8 and then 16 seconds between them:
[Rolink] could not subscribe to topic "rolink" (attempt 1/5): <reason>After the fifth failure it stops trying, and that server runs without live events. Reads, linking and transactions still work, and the wallet cache still expires after walletCacheSeconds.
To skip the subscription entirely (for example in a script that only reads), pass subscribe = false:
local rolink = Rolink.new({ apiKey = HttpService:GetSecret("rolink_api_key"), subscribe = false,})Destroy
Section titled “Destroy”rolink:Destroy() disconnects the MessagingService subscription and every handler on the four signals. If the subscription is still being set up when you call it, it’s disconnected as soon as it succeeds. Most games never need it, since the client lives as long as the server. It’s useful in tests, or when you replace a client at runtime:
rolink:Destroy()rolink = Rolink.new(newConfig)Next steps
Section titled “Next steps”- Send transactions: use
TransactionUpdatedto finish rewards that answeredsubmitted. - Watch addresses: turn on
ChainActivity. - Events reference: payloads and signal types in one place.
- Troubleshooting: when nothing fires.
