Docs · sdk

Godot SDK

How to integrate Lynx Relay, LynxHandler, and reward offers in a Godot game.

Godot SDK

The Godot integration is built around three autoloads and one scene-level handler pattern:

LynxRelay
LynxRewards
LynxEventPopup
LynxHandler / your own LynxHandler subclass

LynxRelay talks to the backend. LynxRewards turns completed Lynx milestones into local reward offers. LynxEventPopup is the required in-game event notification UI. LynxHandler is the bridge between Lynx and your actual game scene: it listens for Lynx signals, updates the counter UI, spawns or hides the reward object, triggers the required popup feedback, and only grants the reward after Lynx confirms the claim.

Add the plugin

Copy the Lynx addon into your project:

res://addons/lynx/

Enable the plugin in Godot. The plugin registers these autoloads:

LynxRelay       -> res://addons/lynx/lynx_relay_client.gd
LynxRewards     -> res://addons/lynx/lynx_reward_offer_manager.gd
LynxEventPopup  -> res://addons/lynx/lynx_event_popup.tscn

Recommended autoload order:

1. LynxRelay
2. LynxRewards
3. LynxEventPopup

Configure your game

In the Godot project, configure the runtime game credentials and event IDs in lynx_relay_client.gd:

const BASE_URL := "https://api.lynxrelay.space"

const GAME_ID := "your-game-id"
const GAME_KEY := "your-game-key"

const TRACKED_RELAYS := [
    "sunfall"
]

The Game Key is the runtime key used by your exported Godot game for relay and reward requests. It identifies the game client, but it is not an admin credential and cannot join events, edit game metadata, rotate credentials, or delete anything.

Do not put the Game Admin Password in the Godot project, in exported game builds, or in a public repository. The Game Admin Password is only for trusted developer/admin actions, such as connecting a registered game to an event from the developer join page.

Use the exact same event ID everywhere. For example, if the backend event ID is sunfall, then TRACKED_RELAYS, LynxHandler.listened_event, and all increment_relay() calls should also use sunfall.

The game must be linked to the event before relay increment/sync calls are accepted. Developers can do that outside the game client from the developer join page with:

Game ID
Game Admin Password

What the sample 2D project contains

The sample scene is structured like this:

Sample2d
├─ Counter
├─ CounterDescription
├─ CounterDescription2
├─ HUD
│  └─ CoinCount
├─ Player
├─ PlayerDetectorArea
├─ CoinGetArea
└─ LynxHandler
   └─ GetterTimer

The important scripts are:

player_detector_area_2D.gd   -> contributes to the event counter
lynx_handler.gd              -> reusable base handler
lynx_handler_coin2D.gd       -> sample-specific reward handler
coin_get_area2D.gd           -> reward pickup interaction
hud_2D.gd                    -> local coin UI

The sample has two gameplay zones:

Blue zone   -> increments the Lynx event counter
Yellow zone -> appears only when a Lynx reward offer is available

Contributing to an event

The sample increments the event when the player enters the blue trigger area:

class_name PlayerDetectorArea2D extends Area2D

func _on_body_entered(body: Node2D) -> void:
    if body is Player2D:
        LynxRelay.increment_relay("sunfall", 60)

In a real game, call increment_relay() when the player completes the action that should contribute to the shared event.

Examples:

LynxRelay.increment_relay("sunfall", 1)
LynxRelay.increment_relay("sunfall", 10)

The SDK queues increment calls and sends them safely in the background. Do not call increment_relay() every frame from _process(). Trigger it from actual gameplay actions, such as a kill, pickup, room clear, completed objective, or timed achievement.

Runtime contribution requests are rate limited to protect the shared service from accidental request spam. The current increment limit is 30 increment requests per minute per player installation, per game, per event. If the backend returns rate_limited, the SDK waits and retries instead of dropping the contribution.

If your game can generate many contributions quickly, keep each gameplay trigger as its own increment call and let the SDK queue them. For high-frequency mechanics, design the integration around meaningful milestones rather than per-frame updates.

The LynxHandler pattern

LynxHandler is a scene-level adapter. It is not the backend client itself. Its job is to connect your scene to LynxRelay, LynxRewards, and LynxEventPopup.

The base handler exports the event ID it listens to:

class_name LynxHandler extends Node

@export var listened_event: String = "sunfall"

On _ready(), it connects to the Lynx autoload signals:

LynxRelay.sync_completed.connect(_on_sync_completed)
LynxRelay.increment_completed.connect(_on_local_increment_completed)
LynxRewards.offer_available.connect(_on_offer_available)
LynxRewards.offer_updated.connect(_on_offer_available)

The handler also supports polling through a child timer. In the sample scene, GetterTimer is a child of LynxHandler and calls:

func _on_getter_timer_timeout() -> void:
    LynxRelay.sync_relay(listened_event)

This keeps the UI updated even when other players/games contribute to the same event.

The default passive sync interval is 5 seconds. Avoid reducing it below that in your own handlers. Passive sync is for UI freshness; local player contributions should use increment_relay() and let the SDK handle queueing, retry, and backend rate limits.

Make a game-specific handler

Do not put your whole reward logic into LynxRelay. Instead, extend LynxHandler and override the game-facing functions.

The sample uses LynxHandlerCoin2D:

class_name LynxHandlerCoin2D extends LynxHandler

@export var counter_label: Label
@export var coin_get_area: CoinGetArea2D

func _ready() -> void:
    super()
    coin_get_area.visible = false
    coin_get_area.coin_claim_ready.connect(_on_offer_clear_interaction)

The scene assigns:

counter_label -> ../Counter
coin_get_area -> ../CoinGetArea

Use the same pattern for your own game: export references to the UI label, chest, NPC, reward panel, or interaction object that should react to Lynx rewards.

Update the counter UI

LynxRelay.sync_completed passes the latest relay state into the handler. The sample updates the counter label like this:

func update_goal_state_visuals(relay_state: Dictionary) -> void:
    var value := int(relay_state.get("value", 0))
    var target := LynxRelay.get_goal_target_from_state(relay_state)
    counter_label.text = str(value) + "/" + str(target)

Useful relay state fields:

value              current total contribution amount
target             current cumulative goal threshold
previous_target    previous completed threshold
next_required      amount required for the current stage
stage_progress     progress inside the current stage, 0.0 to 1.0
completion_version number of completed reward milestones

For most UI, start with value and target.

Show a reward object

LynxRewards creates a local reward offer when the server says the player has a claimable milestone. The handler receives it and calls display_reward_item().

The 2D sample uses a yellow pickup area:

func display_reward_item(offer: Dictionary) -> void:
    coin_get_area.visible = true

In a real game, this could spawn a chest, enable an NPC prompt, show a reward menu button, or update an existing reward object to a higher version.

If a higher reward version becomes available while an older offer is still unclaimed, the handler can receive an updated offer. Design your reward UI so one higher-tier offer can include or replace the lower-tier offer.

Claim rewards safely

The reward pickup object should not grant the reward directly. In the sample, the yellow area only emits a signal:

class_name CoinGetArea2D extends Area2D

signal coin_claim_ready

func _on_body_entered(body: Node2D) -> void:
    if body is Player2D:
        coin_claim_ready.emit()
        # Do not grant the reward here.

LynxHandlerCoin2D connects that signal to _on_offer_clear_interaction(), which is implemented in the base LynxHandler.

The claim flow is:

1. Player interacts with the reward object.
2. LynxHandler calls LynxRewards.prepare_offer_claim(listened_event).
3. Lynx settles/acknowledges the pending server claim safely.
4. Your handler grants the local reward on success.
5. LynxRewards.complete_offer(listened_event) clears the local offer.
6. LynxRelay.sync_relay(listened_event, true) refreshes the state.

The sample success callback grants one local coin:

func on_player_reward_claim_success() -> void:
    coin_get_area.visible = false
    Player2D.coin_count += 1

The failure callback keeps the reward object available:

func on_player_reward_claim_fail() -> void:
    coin_get_area.visible = true

In production, the safe order should be:

func on_player_reward_claim_success() -> void:
    give_reward_to_player()
    save_game()
    # The base handler clears the Lynx offer after prepare/settlement succeeds.

If you write your own claim flow without LynxHandler, keep this order:

var result := await LynxRewards.prepare_offer_claim("sunfall")

if not result.get("ok", false):
    return

var offer: Dictionary = result["offer"]

give_reward(offer)
save_game()
LynxRewards.complete_offer("sunfall")
await LynxRelay.sync_relay("sunfall", true)

LynxEventPopup is a required part of the player-facing integration. Participating games must clearly show Lynx event feedback when the player contributes to an event and when a Lynx reward becomes available. The sample handler uses the popup like this:

popup.show_counter_contribution(listened_event, amount, relay_state)
popup.show_reward_available(listened_event, offer)
popup.show_reward_claimed(listened_event, offer)

The popup should show:

local contribution feedback, for example +60 and current progress
reward available messages
reward claimed messages

You may restyle the popup or replace the default scene with your own implementation, but the same player-facing functionality must remain present in the game.

The popup is registered as an autoload named LynxEventPopup, so the sample handler can retrieve it from the root:

get_tree().root.get_node_or_null("LynxEventPopup")

Minimal integration checklist

1. Copy res://addons/lynx/ into the project.
2. Enable the Lynx plugin.
3. Confirm the autoloads exist: LynxRelay, LynxRewards, LynxEventPopup.
4. Set BASE_URL, GAME_ID, GAME_KEY, and TRACKED_RELAYS in lynx_relay_client.gd. Do not place the Game Admin Password in the Godot project.
5. Set TRACKED_RELAYS to your event IDs, for example ["sunfall"].
6. Link the game to the public event from the developer join page.
7. Call LynxRelay.increment_relay("sunfall", amount) when the player contributes.
8. Keep LynxEventPopup enabled, or provide an equivalent required popup implementation.
9. Add a LynxHandler node to the gameplay scene.
10. Extend LynxHandler for your own reward object/UI.
11. Grant and save rewards only after Lynx confirms the claim.