Frayit.Sdk
1.2.1
dotnet add package Frayit.Sdk --version 1.2.1
NuGet\Install-Package Frayit.Sdk -Version 1.2.1
<PackageReference Include="Frayit.Sdk" Version="1.2.1" />
<PackageVersion Include="Frayit.Sdk" Version="1.2.1" />
<PackageReference Include="Frayit.Sdk" />
paket add Frayit.Sdk --version 1.2.1
#r "nuget: Frayit.Sdk, 1.2.1"
#:package Frayit.Sdk@1.2.1
#addin nuget:?package=Frayit.Sdk&version=1.2.1
#tool nuget:?package=Frayit.Sdk&version=1.2.1
Frayit.Sdk
Official .NET SDK for Frayit chat moderation and voice channel services.
Security note: keep
ClientIdandClientSecreton trusted infrastructure.
Recommended pattern: studio backend uses this SDK and returns onlylivekit_token+livekit_urlto 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.Allowedandresult.Redactedto 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:
JoinVoiceChannelAsync→POST /v1/voice/joinLeaveVoiceChannelAsync→POST /v1/voice/leaveCloseVoiceChannelAsync→POST /v1/voice/closeGetVoiceRoomStateAsync→GET /v1/voice/roomSetServerMuteAsync→POST /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 | Versions 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. |
-
.NETStandard 2.1
- System.Text.Json (>= 10.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.