Events reference
Rolink pushes events to every live server of your experience through Roblox Open Cloud MessagingService, using your project’s own settings. The SDK subscribes to the same topic and turns each event into a signal. For patterns and examples, see the Live events guide.
type | SDK signal | Sent when |
|---|---|---|
wallet_linked | WalletLinked | A player links a wallet on the project’s linking page. |
wallet_unlinked | WalletUnlinked | A player unlinks, or loses their wallet to another account. |
tx | TransactionUpdated | A transaction sent by the managed wallet reaches confirmed or failed. |
watch | ChainActivity | A new transaction touches one of the project’s watched addresses. |
ping | none | You send a test event from the dashboard. |
Requirements
Section titled “Requirements”Events are published per project, only when the project has both settings, under Settings in the dashboard:
- Universe ID. The experience’s universe ID, digits only. Not a place ID.
- Open Cloud API key. A key from Creator Hub with
messaging-service→universe-messaging-service:publishon that experience. Rolink stores it encrypted (AES-256-GCM) and decrypts it only to publish.
Without them, nothing is published and GetProject().events is false. Everything else keeps working. The setup is described in Set up Roblox.
The event topic is set per project in the same place: rolink by default, 1 to 80 letters, digits, - and _. The SDK’s topic option must match it.
Message format
Section titled “Message format”Each event is one MessagingService message whose data is a JSON string. Every message has:
| Field | Value |
|---|---|
v | 1, the payload version. Messages with another version are ignored by the SDK. |
type | wallet_linked, wallet_unlinked, tx, watch or ping |
The other fields depend on the type. null fields arrive as nil after JSONDecode. Ignore types and fields you do not know, so a newer payload never breaks an older game.
wallet_linked
Section titled “wallet_linked”{ "v": 1, "type": "wallet_linked", "userId": 42, "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" }| Field | Type | Meaning |
|---|---|---|
userId | number | The player’s Roblox UserId. |
address | string | The wallet they linked. |
Sent after every successful signature check on the project’s linking page: a first link, a swap to a new wallet, or the same wallet linked again.
SDK. The wallet cache for userId is set to address, then WalletLinked fires with (userId, address, player), where player is the Player if they are on this server.
wallet_unlinked
Section titled “wallet_unlinked”{ "v": 1, "type": "wallet_unlinked", "userId": 42, "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" }| Field | Type | Meaning |
|---|---|---|
userId | number | The player who no longer has the wallet. |
address | string | The wallet they lost. |
Sent in two cases:
- the player unlinks on the linking page while a wallet is linked
- another Roblox account proves ownership of the same wallet in this project. The wallet moves to the new account, and Rolink publishes
wallet_unlinkedfor the previous owner andwallet_linkedfor the new owner. The two can arrive in either order.
A player who swaps their own wallet for a new one only triggers wallet_linked, with no wallet_unlinked for the old address.
SDK. If the cached wallet for userId is address, the cache entry is dropped. Then WalletUnlinked fires with (userId, address, player).
{ "v": 1, "type": "tx", "key": "quest:42:intro", "kind": "transfer-token", "status": "confirmed", "signature": "5z99PpyTfnCYHDAbyVoMFuHD3gWQFis2izi9NSJKEbijXWUQQnuWxcnAKUFxAhSJcJMExR9MfN69R1PhsvGiQYeu", "userId": 42, "error": null}{ "v": 1, "type": "tx", "key": "daily:42:2026-10-08", "kind": "transfer-sol", "status": "failed", "signature": "4huotLhmwu1MzxAgG1z8fhctptGdAsG3teAo1QPcxQkddj3jiGmxuK4WQydFK9j8gYUCDWNaSkMQePLdAGBbpEcf", "userId": 42, "error": "Transaction expired before it was confirmed"}| Field | Type | Meaning |
|---|---|---|
key | string | The request’s idempotencyKey. |
kind | string | transfer-sol, transfer-token or mint-token. |
status | string | confirmed or failed. Intermediate statuses are never sent. |
signature | string or null | The transaction signature. null when the transaction could not be built or signed. |
userId | number or null | The recipient player: the userId the request was sent to, or the player whose linked wallet was the recipient address at request time. Otherwise null. |
error | string or null | Why it failed. null when confirmed. |
Sent once per attempt, when it reaches a final status, on every server. That includes attempts whose HTTP answer already carried the final status. Delivery is still best effort, so call GetTransaction when an event does not arrive.
| Outcome | status | error |
|---|---|---|
The RPC reports the signature confirmed or finalized | confirmed | null |
| The transaction landed but failed on-chain | failed | The on-chain error as JSON, such as {"InstructionError":[1,{"Custom":1}]} |
| The network rejected it in simulation | failed | The simulation error, in the coded form Solana error #-32002; Decode this error by running ... |
| It provably expired without landing | failed | Transaction expired before it was confirmed |
It could not be built (the request answered 502 rpc_error) | failed | Could not build the transaction: <reason>, with signature set to null |
The managed wallet could not sign it (the request answered 502 wallet_unavailable) | failed | The wallet provider’s message, with signature set to null |
Not sent for requests refused by validation, the policy or a quota (nothing was stored), for intermediate pending or submitted states, or for requests marked failed with Abandoned before sending. A failed key that you retry starts a new attempt, which sends its own event.
SDK. TransactionUpdated fires with a TxUpdate, where key is renamed idempotencyKey:
{ idempotencyKey = "quest:42:intro", kind = "transfer-token", status = "confirmed", signature = "5z99PpyT...", userId = 42, error = nil,}{ "v": 1, "type": "watch", "name": "treasury", "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "signature": "3s8s4H43urMRvWTRaKLYepZ1BGbKMeMJ2R5xRTZMLgzAiNM3XqTqowHWPWaaeFon1dAesTQKvk25Xtr8JK9991rS", "slot": 312345678, "failed": false}| Field | Type | Meaning |
|---|---|---|
name | string | The watch name set in the dashboard. |
address | string | The watched address. |
signature | string | The new transaction’s signature. |
slot | number | The slot it landed in. |
failed | boolean | true when the transaction failed on-chain. |
Sent once for each new transaction that involves a watched address, at confirmed commitment, failed ones included:
- Rolink polls each watched address every few seconds.
- The first poll of a watch name only records where the address’s history ends, so past transactions are never replayed. An address with no history yet still gets its very first transaction delivered.
- Each poll sends up to 1,000 new transactions per watch, oldest first. If more arrived since the last poll, the oldest ones beyond that are skipped.
- If a poll fails, for example because the RPC is briefly unavailable, the next poll starts again from the same position.
The event names the transaction but does not describe it. Call GetSignatureStatus or your own indexer for details. See Watch addresses.
SDK. ChainActivity fires with { name, address, signature, slot, failed }.
{ "v": 1, "type": "ping", "at": 1791429403583 }| Field | Type | Meaning |
|---|---|---|
at | number | When the test was sent, in Unix milliseconds. |
Sent when you send a test event from Settings in the dashboard. It goes through Open Cloud with the project’s universe ID, key and topic, exactly like a real event, and the dashboard shows Roblox’s answer: accepted, or the HTTP status that explains what to fix. See Troubleshooting.
SDK. Ignored: no signal fires. To see it arrive in a live server, subscribe to the topic yourself, as shown below.
Delivery
Section titled “Delivery”Rolink publishes with POST https://apis.roblox.com/cloud/v2/universes/<universe ID>:publishMessage, using the project’s Open Cloud key:
- Each attempt times out after 5 seconds.
- Network errors,
429and5xxanswers are retried, up to 4 attempts, waiting 0.5, 1 and 2 seconds in between. After the last attempt the event is dropped. - Any other answer, such as
401or403for a key without the right permission, is not retried, and the event is dropped. - Link and
txevents are published in the background. A player linking a wallet, or a transaction request, never waits on Open Cloud. - Order is not guaranteed between events. Within one poll,
watchevents are sent oldest first. - With events turned off, the watcher still polls and records its position, so turning events on later does not replay what it already saw.
- Changes to the universe ID, key or topic take effect within about 30 seconds.
Size limit
Section titled “Size limit”MessagingService refuses messages over 1 kB, and Rolink measures the encoded JSON against 1,000 bytes. When a tx event with an error is over that size, its error is cut to its first 300 characters before sending. No other field is shortened.
The other events stay well under the limit: watch names are at most 32 characters, and addresses, signatures and numbers have bounded sizes.
Subscribing without the SDK
Section titled “Subscribing without the SDK”If you do not use the SDK, subscribe to the topic yourself. Do not do both in one server, or each event is handled twice.
local HttpService = game:GetService("HttpService")local MessagingService = game:GetService("MessagingService")
MessagingService:SubscribeAsync("rolink", function(message) local ok, event = pcall(HttpService.JSONDecode, HttpService, message.Data) if not ok or type(event) ~= "table" or event.v ~= 1 then return end
if event.type == "wallet_linked" then print("linked", event.userId, event.address) elseif event.type == "tx" then print("tx", event.key, event.status, event.signature or event.error) elseif event.type == "ping" then print("test event from the dashboard", event.at) endend)SubscribeAsync can fail, so production code should wrap it in pcall and retry, as the SDK does.
