Frayit.Sdk 1.2.1

dotnet add package Frayit.Sdk --version 1.2.1
                    
NuGet\Install-Package Frayit.Sdk -Version 1.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Frayit.Sdk" Version="1.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Frayit.Sdk" Version="1.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Frayit.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Frayit.Sdk --version 1.2.1
                    
#r "nuget: Frayit.Sdk, 1.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Frayit.Sdk@1.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Frayit.Sdk&version=1.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Frayit.Sdk&version=1.2.1
                    
Install as a Cake Tool

Frayit.Sdk

Official .NET SDK for Frayit chat moderation and voice channel services.

Security note: keep ClientId and ClientSecret on trusted infrastructure.
Recommended pattern: studio backend uses this SDK and returns only livekit_token + livekit_url to game clients.


Install

dotnet add package Frayit.Sdk

Initialize

Your game's chat mode (Managed or Proxied) is configured at game registration in the Frayit dashboard — not in FrayitOptions. The SDK reads it from your credentials automatically.

using Frayit.Sdk;

var options = new FrayitOptions
{
    ClientId     = Environment.GetEnvironmentVariable("FRAYIT_CLIENT_ID")!,
    ClientSecret = Environment.GetEnvironmentVariable("FRAYIT_CLIENT_SECRET")!,
    BaseUrl      = Environment.GetEnvironmentVariable("FRAYIT_BASE_URL")!,
    ChatTimeoutMilliseconds = 100,
    VoiceTimeoutMilliseconds = 1500
};

using var client = new FrayitClient(options);
await client.InitializeAsync();

Call InitializeAsync() once at startup — not on every request.


Integration Modes

Managed Proxied
Who calls SDK Game client directly Studio backend (server-to-server)
Message delivery Frayit publishes to channel subscribers Studio backend delivers via own transport
ConnectChatAsync() WebSocket ✓ Client connects to Frayit — Studio backend handles delivery
Best for No game backend Existing game backend

EvaluateChatAsync()

Pre-checks a message. Does not deliver it.

using Frayit.Sdk.Models;

var result = await client.EvaluateChatAsync(new ChatEvaluationRequest
{
    Message   = "hello world",
    PlayerId  = "player-123",
    SessionId = "session-1",
    ChannelId = "global"
});

if (result.FailOpen)
{
    Console.WriteLine("fail-open: " + result.FailOpenReason);
}

if (!result.Allowed)
{
    Console.WriteLine("blocked: " + string.Join(", ", result.BlockReasonCodes));
}

SendMessageAsync()

Evaluates and delivers a message. Works in both modes — delivery mechanism differs based on game registration config.

var result = await client.SendMessageAsync(new SendMessageRequest
{
    PlayerId  = "player-123",
    SessionId = "session-1",
    ChannelId = "global",
    Message   = "gg wp"
});

if (result.IsFailOpen)
{
    // Frayit unreachable — message allowed through locally
    Console.WriteLine("fail-open: " + result.FailOpenReason);
}

if (!result.Allowed && result.Redacted)
{
    // Soft block — redacted version was delivered
}

if (!result.Allowed && !result.Redacted)
{
    // Hard block — message suppressed entirely
    Console.WriteLine("suppressed: " + string.Join(", ", result.BlockReasonCodes));
}

Managed mode: message is already published after this call. Do not re-render from the result — the sender receives it back via WebSocket like every other player.

Proxied mode: use result.Allowed and result.Redacted to decide what to broadcast via your own transport.

Proxied mode — ASP.NET controller example

[HttpPost("chat/send")]
public async Task<IActionResult> Send([FromBody] SendChatRequest req)
{
    var result = await _frayitClient.SendMessageAsync(new SendMessageRequest
    {
        PlayerId  = req.PlayerId,
        SessionId = req.SessionId,
        ChannelId = req.ChannelId,
        Message   = req.Message
    });

    if (result.Allowed || result.Redacted)
    {
        await _hub.BroadcastToChannelAsync(req.ChannelId, result); // SignalR / your own WS
    }

    return Ok(result);
}

ConnectChatAsync()

Opens a real-time WebSocket to a Frayit channel. Managed mode only.

var channel = await client.ConnectChatAsync(
    playerId:       "player-123",
    channelId:      "global",
    onMessage:      evt => Console.WriteLine($"[{evt.PlayerId}] {evt.Message}"),
    onDelete:       evt => Console.WriteLine("deleted: " + evt.MessageId),
    onJoined:       evt => Console.WriteLine("joined: " + evt.ChannelId),
    onError:        ex  => Console.Error.WriteLine("socket error: " + ex.Message),
    onDisconnected: ()  => ScheduleReconnect()
);

// Disconnect cleanly
await channel.DisposeAsync();

Multiple channels — open one connection per channel:

var globalChannel = await client.ConnectChatAsync("player-123", "global",   onMessage: Render);
var teamChannel   = await client.ConnectChatAsync("player-123", "team-red", onMessage: Render);

Voice API Services

Use these SDK methods to manage voice channels from your backend:

  • JoinVoiceChannelAsyncPOST /v1/voice/join
  • LeaveVoiceChannelAsyncPOST /v1/voice/leave
  • CloseVoiceChannelAsyncPOST /v1/voice/close
  • GetVoiceRoomStateAsyncGET /v1/voice/room
  • SetServerMuteAsyncPOST /v1/voice/mute

Join Voice Channel

using Frayit.Sdk.Models;

var join = await client.JoinVoiceChannelAsync(new VoiceJoinRequest
{
    ChannelId = "squad-red",
    PlayerId = "player-123",
    SessionId = "match-9812",
    MaxParticipants = 8
});

// send these two fields to game client to connect LiveKit
var livekitUrl = join.LivekitUrl;
var livekitToken = join.LivekitToken;

Game client (Unity/Unreal/native) then connects to LiveKit with those values:

// pseudo-code: use your game's LiveKit client SDK
// await livekitRoom.ConnectAsync(livekitUrl, livekitToken);

Moderation + Room State

await client.SetServerMuteAsync(new VoiceMuteRequest
{
    ChannelId = "squad-red",
    PlayerId = "player-123",
    Muted = true
});

var room = await client.GetVoiceRoomStateAsync("squad-red");
Console.WriteLine($"participants={room.ParticipantCount}/{room.MaxParticipants}");

Leave / Close

await client.LeaveVoiceChannelAsync(new VoiceLeaveRequest
{
    ChannelId = "squad-red",
    PlayerId = "player-123"
});

await client.CloseVoiceChannelAsync("squad-red"); // match ended

Client-side Voice Controls (local state helpers)

These methods help studios keep per-client mute/deafen state consistent:

client.SetSelfMuted(true);         // local mic mute intent
client.SetSelfDeafened(false);     // local output mute intent
client.SetPeerMutedLocally("player-77", true);

bool canPlay = client.ShouldPlayPeerAudio("player-77"); // false if self-deafened or peer-muted

Matchmaking

Frayit's behavior-based matchmaking groups players by social compatibility (fray score + shared behavioral history). The SDK wraps the full queue lifecycle.

Configure a game mode (server startup)

using Frayit.Sdk.Models;

await client.UpsertMatchmakingConfigAsync(new MatchmakingConfigRequest
{
    GameMode       = "ranked",   // discriminator — one config per mode
    TeamSize       = 5,          // players per team
    TeamsPerMatch  = 2,          // 2 = 5v5, 25 = 100-player BR solo
    MinPoolSize    = 10,         // formation fires once this many players are waiting
    MaxWaitSec     = 120,        // max wait time before auto-assigning (default: 120)
});

Each GameMode string is fully isolated — "ranked_5v5" and "ranked_3v3" get separate queues and configs.

Join queue

var entry = await client.JoinQueueAsync(new JoinQueueRequest
{
    PlayerId = "player-123",
    GameMode = "ranked",
});

// If the pool hit MinPoolSize on this join, formation fires inline
if (entry.MatchSessionId != null)
{
    var match = await client.GetMatchAsync(entry.MatchSessionId);
}

Poll until matched

var result = await client.PollForMatchAsync(
    playerId: "player-123",
    gameMode: "ranked",
    options: new PollMatchOptions
    {
        Interval         = TimeSpan.FromSeconds(2),
        Timeout          = TimeSpan.FromMinutes(2),
        OnStatusUpdate   = s => Console.WriteLine($"pos={s.Position} status={s.Status}"),
    });

if (result.Matched)
{
    var match = await client.GetMatchAsync(result.MatchSessionId!);

    foreach (var team in match!.Teams)
    {
        Console.WriteLine($"Team {team.TeamIndex} players: {string.Join(", ", team.PlayerIds)}");
        Console.WriteLine($"  harmony={team.HarmonyScore:F2}  avg_score={team.AvgFrayScore:F0}");
    }
}
else if (result.TimedOut)
{
    Console.WriteLine("No match found within timeout.");
}

Cancel queue

var result = await client.LeaveQueueAsync(new LeaveQueueRequest
{
    PlayerId = "player-123",
    GameMode = "ranked",
});

if (result.AlreadyMatched)
{
    // Formation fired before cancel arrived — match is valid, do NOT abort it.
    Console.WriteLine("Too late to cancel — match is ready for this player.");
}

LeaveQueueAsync never throws for the "already matched" case. Check AlreadyMatched instead.

Get match directly

try
{
    var match = await client.GetMatchAsync(matchSessionId);

    if (match == null)
    {
        // 404 — session ID not found
    }
    else
    {
        // Use match.Teams, match.FormedAt, match.ExpiresAt
    }
}
catch (InvalidOperationException ex) when (ex.Message == "match_session_expired")
{
    // 410 — session expired (5-min TTL). Affected players were automatically re-queued.
}

Match team structure

// MatchSessionResponse
// ├── MatchId, GameMode, Status, FormedAt, ExpiresAt
// └── Teams: List<MatchTeam>
//     ├── TeamIndex, HarmonyScore, AvgFrayScore
//     ├── PlayerIds: List<string>
//     └── MatchMethods: Dictionary<string, string>  // playerId → placement method

Logging Hooks

using Frayit.Sdk.Logging;

public sealed class GameLogger : IFrayitLogger
{
    public void OnTokenRefreshStarted() =>
        Console.WriteLine("[token] refresh started");

    public void OnTokenRefreshSucceeded(DateTimeOffset expiresAtUtc) =>
        Console.WriteLine("[token] expires " + expiresAtUtc);

    public void OnTokenRefreshFailed(Exception ex) =>
        Console.WriteLine("[token] failed: " + ex.Message);

    public void OnSdkError(string operation, Exception ex) =>
        Console.WriteLine("[sdk] " + operation + ": " + ex.Message);

    public void OnCircuitBreakerStateChanged(string prev, string next, string reason) =>
        Console.WriteLine($"[cb] {prev} -> {next} ({reason})");
}

using var client = new FrayitClient(options, logger: new GameLogger());

Config Options

Option Default Description
ChatTimeoutMilliseconds 100 Timeout for EvaluateChatAsync and SendMessageAsync.
VoiceTimeoutMilliseconds 1500 Timeout for voice backend endpoints (join/leave/close/room/mute).
MatchmakingTimeoutMilliseconds 2000 Timeout for matchmaking endpoints (JoinQueue, LeaveQueue, GetQueueStatus, GetMatch, config).
TokenPrefetchWindow 30min Refresh token before expiry window.
IdleRefreshCheckInterval 30s Periodic background refresh check interval.
TokenFailureBackoff 20s Backoff after failed refresh when token still valid.
MaxRetries 2 Retry count for transient failures (5xx, 408).
RetryBaseDelay 200ms Exponential backoff base delay.
CircuitBreakerFailureThreshold 5 Consecutive failures before circuit opens.
CircuitBreakerOpenDuration 20s Open-state duration before half-open probe.

Fail-Open Reasons

When Frayit is unreachable, both EvaluateChatAsync() and SendMessageAsync() return a fail-open result. Gameplay is never blocked.

Reason Cause
circuit_breaker_open Circuit open after repeated failures — request skipped.
token_unavailable Token not yet acquired or refresh in progress.
invalid_response_body Frayit returned 200 but response could not be parsed.
timeout Request exceeded ChatTimeoutMilliseconds.
network_error TCP-level failure (DNS, refused, unreachable).
http_<status> Non-retryable HTTP error, e.g. http_429, http_403.
unknown_error Unexpected exception — check OnSdkError logs.
retry_exhausted All retries consumed on transient failures.

Dispose

FrayitClient implements IDisposable. Use using or call Dispose() on shutdown.

// Preferred
using var client = new FrayitClient(options);

// Or explicit
client.Dispose();

ChatChannel instances must be disposed separately before disposing FrayitClient.


Documentation

Full integration guide, architecture, and production best practices at docs.frayit.com.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.1 0 6/9/2026
1.2.0 94 6/3/2026
1.1.1 117 3/6/2026
1.1.0 105 3/6/2026
1.0.2 103 2/23/2026
1.0.1 112 2/21/2026
1.0.0 111 2/20/2026