braket.gg
EN

Unity SDK reference

The Braket Unity SDK is the Unity counterpart of the Unreal Engine SDK: it discovers the local player's live tournament match, optionally auto-polls for one, and submits verified results on match end with retries and backoff. It speaks the same wire protocol (The game-client result protocol (dual attestation)), braket.result.v3, and its canonical serializer is byte-for-byte compatible with the Unreal SDK and the server, so a Unity client and an Unreal client in the same match produce the identical hash. This chapter is the complete API reference plus a step-by-step wire-up; every field and default is quoted from the package source under sdk/unity/Braket/.

The package is MIT-licensed.

Install

  1. Copy the Braket/ folder into your project (for example Assets/Braket/), or add it as a UPM package.
  2. Add the BraketClient component to a persistent GameObject, one that survives scene loads (your bootstrap or "game instance" object).

Requirements:

Configuration: the BraketClient component

BraketClient is a MonoBehaviour; configure it in the inspector. Every field:

Field Type Default Meaning
Enabled bool true Master switch. When false, FetchLiveMatch and SubmitResult are no-ops and auto-discovery does not start
BaseUrl string "https://yourgame.braket.gg" Your arena origin, no trailing slash (a trailing slash is stripped)
MaxSubmitAttempts int 3 Result-submission retry attempts (clamped 1 to 5), used with exponential backoff
AutoDiscover bool true Poll for a live match automatically; fires OnLiveMatchUpdated; stops once a match is found
AutoDiscoverIntervalSeconds float 60 Seconds between auto-discovery polls (clamped 15 to 600)
MockSteamId string "" Dev only: a simulated SteamID64. When set, the SDK uses MOCK:<id> tickets against a Braket in mock mode. Leave empty in production
EnableReferee bool false Stream public gameplay traces (braket.gameplay.v1) for AI-referee training. Inert unless the arena enabled the referee and both players consented on braket.gg
RefereeTraceSchema string "herd.gameplay.v1" Trace schema id; must match ^[a-z0-9][a-z0-9.-]{2,79}\.v[1-9][0-9]*$
RefereeRuleset string "herd-standard" Ruleset label stamped into the trace envelope

The client: BraketClient

Access the component and drive it from your own code:

var braket = FindObjectOfType<Braket.BraketClient>();

Public methods and events

public void FetchLiveMatch();
public void SubmitResult(BraketMatchResult result);
public BraketLiveMatch LiveMatch { get; }               // last discovered match
public event Action<BraketLiveMatch> OnLiveMatchUpdated; // discovery completed
public event Action<bool, string> OnResultSubmitted;     // (accepted, status)

Auto-discovery

When Enabled && AutoDiscover, the component polls FetchLiveMatch() every AutoDiscoverIntervalSeconds from Update() while no valid match is known, and stops once one is found. Bind OnLiveMatchUpdated and show your banner; no manual polling needed. Set AutoDiscover = false to poll yourself.

BraketLiveMatch

The live match discovered from GET /api/v1/me/live-match. P1/P2 are Braket slot order (the canonical order), not your engine's player indices. Fields:

Field Type Meaning
MatchId int Braket match id
GameSeed long Braket-issued fairness seed, minted server-side when the match went live. Drive your shuffle/RNG from it so a player-host cannot pick a favorable deal. 0 means no seed was issued (casual session): use a host-generated seed
Slot int The local player's slot, 1 or 2 (their side's slot in team mode)
SessionNonce string Per-match nonce; the reserved sessionNonce in your result
P1SteamId / P2SteamId string Slot-1 / slot-2 SteamID64
OpponentSteamId string The other side's SteamID64
TournamentName / TournamentSlug string The tournament this match belongs to
WindowClosesAt string ISO-8601 window close
AllowSpectators bool Tournament allows casting/spectating
CastHandMode string Recording-release policy for hidden information (see below): "hidden", "delayed", or "shown". Empty/absent → treat as "hidden"
CastDelaySeconds int For "delayed": seconds after capture before a full-scope (both-hands) recording or segment is released
RefereeEnabled bool The arena runs the M0 referee
RefereeConsentComplete bool Both players granted training consent on braket.gg
Casters List<BraketCaster> Every active caster on the match (a match can have several)
CasterSteamId / StreamUrl string First caster (convenience mirror of Casters[0])
P1Roster / P2Roster List<string> Registered SteamID64s per side (single element for 1v1)
TeamMinPlayers / TeamMaxPlayers int Players per side (>1 means a team tournament)
ResultProtocol string Always "braket.result.v3"

Helpers: IsValid (MatchId > 0 && SessionNonce non-empty) and IsTeamMatch (TeamMaxPlayers > 1). BraketCaster has SteamId, StreamUrl, and Source (community / invited / arena).

Hidden-information policy (CastHandMode). A player's hand is hidden information, so exposing it is a stream-sniping vector. Watching a Braket match means replaying its recording — there is no live in-game spectator feed — and the arena sets, per tournament, when the full-scope (both-hands) recording may be released: "hidden" (default) never releases hands, so only public-scope recordings exist; "delayed" releases full-scope recordings and near-live segments CastDelaySeconds after capture; "shown" releases full scope immediately (only sound for trusted, neutral-hosted broadcasts). Your game uses this policy to decide which scope to record and upload; the release gate itself is enforced server-side on fetch. Players themselves are unaffected; each still receives only their own hand during play.

BraketMatchResult

What your game reports at match end. Your game owns the result shape: name the winner and provide your own fields as JSON.

Field Type Meaning
WinnerSteamId string SteamID64 of a player on the winning side (the reserved winner key). Empty submits nothing (a draw)
ResultJson string Your game-defined result as a JSON object string, e.g. {"score":[2,1],"turns":17,"seed":"0451d2be"}. Stored verbatim; both clients must produce the same fields. Reserved keys you include are ignored (the SDK sets them). Empty attests only the reserved keys
IsHost bool True if this client is the listen-server host (metadata only; becomes clientRole)
OpponentSteamId string Anti-mixup guard (1v1): the SteamID64 you actually played. If set and it doesn't match the tournament opponent, the SDK refuses to submit

Building ResultJson deterministically

The single rule: both clients must produce the same fields. The SDK canonicalizes key order for you (BraketJson.CanonicalizeResult sorts keys recursively), so field insertion order doesn't matter — but the values must be equal on both clients and come from replicated, host-authoritative state. Only include facts both clients observe identically (winner, scores, turn count, the replicated seed); do not put wall-clock timestamps in ResultJson (the SDK sends endedAt as unhashed metadata). Team matches: set WinnerSteamId to anyone on the winning side and Braket maps it via the rosters.

How the SDK builds a submission

After a fresh discovery, SubmitResult resolves the local SteamID64 and confirms it is a player of the match (in team mode, on a roster), applies the 1v1 anti-mixup guard, confirms WinnerSteamId is a player of one side, then calls BraketJson.CanonicalizeResult(MatchId, SessionNonce, WinnerSteamId, ResultJson) and hashes the bytes with BraketJson.Sha256Hex. It POSTs { "protocol": "braket.result.v3", "result": <canonical string>, "resultHash": <hash>, "clientRole": "host"|"guest", "endedAt": <UTC now> }. result is sent as a JSON string value; the server re-hashes those exact bytes, compares to resultHash, then reads the reserved keys.

Retries follow MaxSubmitAttempts: only HTTP 0 (network failure) or >= 500 retry, with 2^(MaxSubmitAttempts - attemptsLeft + 1)-second backoff; 4xx is definitive (the manual fallback exists). When attempts are exhausted the match id is released for a later manual retry.

Verifying your bytes

BraketJson.CanonicalizeResult and BraketJson.Sha256Hex are public static, so you can unit-test the exact bytes and hash your game will send. For match 842, nonce c3f1a4e89b02d715, winner 76561197977425772, and ResultJson = {"score":[2,1],"turns":17,"seed":"0451d2be"}, CanonicalizeResult returns (keys sorted, compact):

{"matchId":842,"score":[2,1],"seed":"0451d2be","sessionNonce":"c3f1a4e89b02d715","turns":17,"winner":"76561197977425772"}

and Sha256Hex of that string is:

5ab1be222c607be9c897be0d5cf400be99e7a7d29845aa78c4a0ab59ee39e2f8

That is the same value the Unreal SDK and the server produce, which is why a Unity client and an Unreal client can play the same match: they hash identically. If your two clients disagree on this hash, they are not producing the same ResultJson fields; fix the divergence until both print identical bytes.

Step-by-step wire-up

using Braket;

public class TournamentHook : MonoBehaviour
{
    BraketClient braket;

    void Start()
    {
        braket = FindObjectOfType<BraketClient>();
        braket.OnLiveMatchUpdated += m => { if (m.IsValid) ShowBanner(m.TournamentName, m.OpponentSteamId); };
        braket.OnResultSubmitted += (ok, status) => Debug.Log($"Braket: {status}");
        braket.FetchLiveMatch(); // or rely on Auto Discover
    }

    // Call on BOTH clients from a match-end signal (a networked RPC / RepNotify equivalent),
    // gated to ranked/tournament sessions.
    public void OnMatchOver(string winnerSteamId64, int myScore, int oppScore, int turns, uint seed, bool isHost)
    {
        var r = new BraketMatchResult {
            WinnerSteamId = winnerSteamId64,
            ResultJson    = $"{{\"score\":[{myScore},{oppScore}],\"turns\":{turns},\"seed\":\"{seed:x8}\"}}",
            IsHost        = isHost,
        };
        braket.SubmitResult(r);
    }
}

Optional: M0 AI-referee gameplay traces

Independently of the result, the SDK can stream a public gameplay trace (protocol braket.gameplay.v1) — a per-event log of public match state (turns, played cards, visible score) used to train Braket's AI referee. A trace never affects the outcome: a failed or disagreeing trace only excludes that match from training data.

It is inert unless all three hold: EnableReferee is on (on the BraketClient component), the arena runs the referee (BraketLiveMatch.RefereeEnabled), and both players granted training consent on braket.gg (RefereeConsentComplete). Consent is handled on the website — the game never prompts. IsRefereeTraceActive folds all three together; when it is false, every call below is a cheap no-op, so you can wire them unconditionally.

Label actors by Braket slot (1 = p1, 2 = p2), never "me/opponent" — each client submits its own trace and the server compares them:

public void BeginGameplayTrace();                                    // once at match start → match_started (t=0)
public void TraceRoundStarted(int round);
public void TraceRoundEnded(int round, int winnerSlot, int p1Score, int p2Score); // winnerSlot 0=none
public void TraceTurnStarted(int actorSlot, int turn);               // actorSlot 1|2
public void TracePublicAction(int actorSlot, string actionId, string target); // target "p1"|"p2"|"board"|""
public void TracePublicScoreChanged(int actorSlot, int p1, int p2); // actorSlot 0=none
public void EndGameplayTraceAndSubmit(int winnerSlot, string reason); // → match_ended + submit

reason is "normal", "forfeit", "disconnect", or "timeout". The recorder stamps seq/tMs, builds the braket.gameplay.v1 envelope, serializes the trace canonically and SHA-256-hashes it with the same BraketJson serializer as results (so a Unity client and an Unreal client produce identical bytes), and POSTs it to /api/v1/matches/{matchId}/gameplay-trace. Drive the calls from replicated / authoritative signals so both clients record the same public events.

Convergence note: when the arena runs the referee and both players consented, finalizing a match recording (below) derives the braket.gameplay.v1 trace from it server-side. If your game uploads match recordings, a separate gameplay-trace submission is optional — the trace API remains available for games that record no replays.

Match recordings and replays

⚠️ Draft, pending engine verification. The recording and segment methods below are implemented in the SDK source but were written blind against the server API and still need an in-engine verification pass. The server endpoints they call are shipping (see the protocol chapter).

The SDK uploads herd.replay.v1 recordings produced by your replay recorder: init → PUT gzip to the presigned target → finalize. Scope, game build, and duration are read from the file's JSON header; the bytes never transit Braket's app server.

public void SubmitRecording(string replayJson, bool casual);        // → OnRecordingSubmitted
public void FetchMyRecordings();                                    // → OnRecordingsListed
public void FetchRecording(int recordingId, string linkToken);      // → OnRecordingFetched
public void ResubmitPendingRecordings();                            // retry the local spool

public event Action<bool, int, string> OnRecordingSubmitted;        // (ok, recordingId, status)
public event Action<List<BraketRecordingInfo>> OnRecordingsListed;
public event Action<int, string> OnRecordingFetched;                // (recordingId, downloadUrl)

BraketRecordingInfo carries Id, MatchId (0 = casual), Scope ("public"/"full"), GameBuild, DurationMs, Visibility ("private"/"arena"/"link"), and CreatedAt.

Near-live segments

A live recording can additionally be streamed as numbered herd.replay.v1 fragments (header carries segmentSeq and baseTMs), so watchers can replay the match while it is still running:

public void SubmitRecordingSegment(int recordingId, int seq, string fragmentJson); // → OnSegmentSubmitted
public void FetchRecordingSegments(int recordingId, int afterSeq, string linkToken); // → OnSegmentsListed

public event Action<bool, int, string> OnSegmentSubmitted;          // (ok, seq, status)
public event Action<bool, List<BraketSegmentInfo>> OnSegmentsListed; // (live, segments)

SubmitRecordingSegment runs the per-segment init → PUT → finalize round-trip; the SDK only transports segments — your game times the flushes. FetchRecordingSegments lists ready segments with seq > afterSeq; OnSegmentsListed reports live = true while the recording is still being appended (keep polling) and false once it ended. BraketSegmentInfo is { Seq, Url }.

Other engines

The plugin/SDK are reference implementations, but the protocol is plain HTTPS + JSON + SHA-256 + a Steam Web API ticket, so it is implementable in any engine. The wire contract is The game-client result protocol (dual attestation).