---
title: "EOS platform services"
description: "Add EOS progression, files, commerce, and player protection features."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Complete [EOS setup](/integrations/eos/setup) and [login](/integrations/eos/identity) first. Configure each service in the target deployment and allow it in your client policy. Use the linked Crossplay guides when the same gameplay must work across stores.

## Progression

Create your stats, achievement definitions, and leaderboards in the Developer Portal. EOS aggregates stat samples using each stat's configured rule; a SUM stat takes the amount earned, not the player's new total.

**Blueprint**

<BlueprintViewer title="Record a match win" src="/blueprints/crossplay-eos-ingest-stat.txt" />
<Accordion>
  <AccordionItem title="Read achievement definitions">
<BlueprintViewer title="Read achievement definitions" src="/blueprints/crossplay-eos-achievement-definitions.txt" />
  </AccordionItem>
  <AccordionItem title="Read achievement progress">
<BlueprintViewer title="Read achievement progress" src="/blueprints/crossplay-eos-achievement-states.txt" />
  </AccordionItem>
  <AccordionItem title="Unlock a first-win achievement">
<BlueprintViewer title="Unlock a first-win achievement" src="/blueprints/crossplay-eos-unlock-achievement.txt" />
  </AccordionItem>
</Accordion>
<BlueprintViewer title="Read global EOS leaderboard records" src="/blueprints/crossplay-eos-leaderboard-records.txt" />

**C++**

Game module dependencies: `EIKCore`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKStats.h"
#include "Interfaces/IEIKAchievements.h"

bool RecordEosMatchWin(UGameInstance &GameInstance, const FEIKAsyncCallbackVoid &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Stats = Services->GetStatsInterface();
    if (!Stats)
        return false;
    FEIKUpdateStatsParams Params;
    Params.Stats.Emplace(TEXT("MatchesWon"), 1);
    Stats->UpdateStats(0, Params, Completion);
    return true;
}

void ReadEosAchievementDefinitions(
    IEIKAchievements &Achievements,
    const FEIKAsyncCallback<FEIKQueryAchievementDefinitionsResult> &Completion)
{
    Achievements.QueryAchievementDefinitions(0, Completion);
}

void ReadEosAchievementProgress(
    IEIKAchievements &Achievements,
    const FEIKAsyncCallback<FEIKQueryAchievementStatesResult> &Completion)
{
    Achievements.QueryAchievementStates(0, Completion);
}

void UnlockEosFirstWin(IEIKAchievements &Achievements, const FEIKAsyncCallbackVoid &Completion)
{
    FEIKUnlockAchievementsParams Params;
    Params.AchievementIds.Add(TEXT("FIRST_WIN"));
    Achievements.UnlockAchievements(0, Params, Completion);
}
```

Bind completion callbacks with `CreateWeakLambda`. A `true` return means dispatched, not saved. Resolve `IEIKAchievements` through `Services->GetAchievementsInterface()` and check it before calling. Successful queries return `Result.Result->Definitions` or `Result.Result->States`. `IEIKLeaderboards` exposes leaderboard reads.

Create `MatchesWon` as a **SUM** stat for this example. Submit once per earned win; repeating an accepted write adds another win.

Match definitions and states by `AchievementId`. `Progress` runs from 0 to 1; use locked text and icons until unlocked, and honor `bIsHidden` in your UI. Refresh states after an unlock.

Replace `FIRST_WIN` with an achievement ID from your deployment. Direct unlocks require the client-policy permission; stat-linked achievements use their configured thresholds instead. Leaderboards are derived from their configured stats.

The global-board example uses **direct EOS SDK nodes**. Replace `MostWins` with your portal leaderboard ID and pass the signed-in player's PUID. Read records only after the query succeeds. These nodes use the default EOS platform and a shared record cache; finish copying before starting another board query. The copy helper already releases native records.

### Read a rank window

Replace `MostWins` with your portal leaderboard ID. Rank is **1-based**: center rank `10` with limit `5` requests ranks `8–12`. The returned window can be shorter near the end of the board. This flow supports OSSv1 and OSSv2.

**Blueprint**

<BlueprintViewer title="EOS leaderboard around rank" src="/blueprints/crossplay-eos-rank-window.txt" />

Game Instance variable: `ReadingRanks` (**Boolean**, false).

**C++**

Game module dependencies: `EIKCore`, `BetideCore`, `Engine`.

```cpp
// EosRankWindowGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "EosRankWindowGameInstance.generated.h"

UCLASS()
class UEosRankWindowGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void ReadRanksNearPosition(int32 CenterRank);
    UPROPERTY(BlueprintReadOnly)
    bool ReadingRanks = false;
};
```

```cpp
// EosRankWindowGameInstance.cpp
#include "EosRankWindowGameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKLeaderboards.h"
#include "Logging/LogMacros.h"

DEFINE_LOG_CATEGORY_STATIC(LogEosRankWindow, Log, All);

void UEosRankWindowGameInstance::ReadRanksNearPosition(int32 CenterRank)
{
    if (ReadingRanks)
    {
        UE_LOG(LogEosRankWindow, Warning, TEXT("A leaderboard read is already running"));
        return;
    }
    const auto Services = FEIKOnlineServicesFactory::Get(GetWorld());
    const auto Leaderboards = Services ? Services->GetLeaderboardsInterface() : nullptr;
    if (!Leaderboards)
    {
        UE_LOG(LogEosRankWindow, Warning, TEXT("EOS leaderboards are unavailable"));
        return;
    }
    FEIKReadLeaderboardAroundRankParams Params;
    Params.LeaderboardId = TEXT("MostWins");
    Params.Rank = CenterRank;
    Params.Limit = 5;
    ReadingRanks = true;
    Leaderboards->ReadEntriesAroundRank(
        0, Params,
        FEIKAsyncCallback<FEIKLeaderboardResult>::CreateWeakLambda(
            this, [this](const TEIKAsyncResult<FEIKLeaderboardResult> &Result) {
                ReadingRanks = false;
                if (!Result.IsSuccess() || !Result.Result.IsSet())
                {
                    UE_LOG(LogEosRankWindow, Warning, TEXT("%s"), *Result.Error.Message);
                    return;
                }
                const auto &Entries = Result.Result->Entries;
                for (const FEIKLeaderboardEntry &Entry : Entries)
                {
                    UE_LOG(LogEosRankWindow, Log, TEXT("%d. %s: %lld"), Entry.Rank,
                           *Entry.DisplayName, Entry.Score);
                }
                UE_LOG(LogEosRankWindow, Log, TEXT("Loaded %d ranks"), Entries.Num());
            }));
}
```

Shared examples: [achievements and stats](/crossplay/achievements-stats), [leaderboards](/crossplay/leaderboards).


<Accordion>
  <AccordionItem title="C++: direct EOS leaderboard records">

Add `EIKBlueprints` and `EIKSDK` to your module dependencies.

```cpp
#include "Engine/GameInstance.h"
#include "SdkFunctions/LeaderboardsInterface/EIK_Leaderboards_QueryLeaderboardRanks.h"
#include "SdkFunctions/LeaderboardsInterface/EIK_LeaderboardsSubsystem.h"
#include "Logging/LogMacros.h"

DEFINE_LOG_CATEGORY_STATIC(LogEosLeaderboardExample, Log, All);

void QueryGlobalEosBoard(UGameInstance &GameInstance, FEIK_ProductUserId LocalUserId,
                         const FScriptDelegate &OnComplete)
{
    FEIK_Leaderboards_QueryLeaderboardRanksOptions Options;
    Options.LeaderboardId = TEXT("MostWins");
    Options.LocalUserId = LocalUserId;
    auto *Request =
        UEIK_Leaderboards_QueryLeaderboardRanks::EIK_Leaderboards_QueryLeaderboardRanks(Options);
    Request->RegisterWithGameInstance(&GameInstance);
    Request->OnCallback.Add(OnComplete);
    Request->Activate();
}

void PrintQueriedEosBoard(UGameInstance &GameInstance, EEIK_Result QueryResult)
{
    if (QueryResult != EEIK_Result::EOS_Success)
    {
        UE_LOG(LogEosLeaderboardExample, Warning, TEXT("Leaderboard query failed: %d"),
               static_cast<int32>(QueryResult));
        return;
    }
    auto *Boards = GameInstance.GetSubsystem<UEIK_LeaderboardsSubsystem>();
    if (!Boards)
        return;
    const int32 Count = Boards->EIK_Leaderboards_GetLeaderboardRecordCount();
    for (int32 Index = 0; Index < Count; ++Index)
    {
        FEIK_Leaderboards_LeaderboardRecord Record;
        const auto Result = Boards->EIK_Leaderboards_CopyLeaderboardRecordByIndex(Index, Record);
        if (Result != EEIK_Result::EOS_Success)
        {
            UE_LOG(LogEosLeaderboardExample, Warning, TEXT("Leaderboard record %d failed: %d"),
                   Index, static_cast<int32>(Result.GetValue()));
            continue;
        }
        UE_LOG(LogEosLeaderboardExample, Log, TEXT("Rank %d: %s - %d"), Record.Rank,
               *Record.UserDisplayName, Record.Score);
    }
}
```

Bind `OnComplete` to a `UFUNCTION` taking `(const TEnumAsByte<EEIK_Result>& Result, const FString& LeaderboardId)`, then call `PrintQueriedEosBoard` with that result. The query returning does not mean records are ready.

  </AccordionItem>
</Accordion>

## Player files and title files

**Player Data Storage** holds per-player saves. **Title Storage** holds files published for your game, such as shared configuration. Generate your project's encryption key before using either.

**Blueprint**

<Accordion>
  <AccordionItem title="Upload player settings">
<BlueprintViewer title="Upload player settings" src="/blueprints/crossplay-eos-write-user-file.txt" />
  </AccordionItem>
  <AccordionItem title="Download player settings">
<BlueprintViewer title="Download player settings" src="/blueprints/crossplay-eos-read-user-file.txt" />
  </AccordionItem>
  <AccordionItem title="List player files">
<BlueprintViewer title="List player files" src="/blueprints/crossplay-eos-list-user-files.txt" />
  </AccordionItem>
  <AccordionItem title="List title files">
<BlueprintViewer title="List title files" src="/blueprints/crossplay-eos-list-title-files.txt" />
  </AccordionItem>
  <AccordionItem title="Download game rules">
<BlueprintViewer title="Download game rules" src="/blueprints/crossplay-eos-read-title-file.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKUserFile.h"
#include "Interfaces/IEIKTitleFile.h"

bool UploadEosSettings(UGameInstance &GameInstance, const FString &SettingsJson,
                       const FEIKAsyncCallbackVoid &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Files = Services->GetUserFileInterface();
    if (!Files)
        return false;
    FEIKUserFileContents Contents;
    Contents.SetDataFromString(SettingsJson);
    Files->WriteFile(0, FEIKWriteUserFileParams(TEXT("settings.json"), Contents.Data), Completion);
    return true;
}

void DownloadEosSettings(IEIKUserFile &Files,
                         const FEIKAsyncCallback<FEIKReadUserFileResult> &Completion)
{
    Files.ReadFile(0, FEIKReadUserFileParams(TEXT("settings.json")), Completion);
}

void ListEosPlayerFiles(IEIKUserFile &Files,
                        const FEIKAsyncCallback<FEIKEnumerateUserFilesResult> &Completion)
{
    Files.EnumerateFiles(0, {}, Completion);
}

void DownloadEosRules(IEIKTitleFile &Files,
                      const FEIKAsyncCallback<FEIKReadTitleFileResult> &Completion)
{
    Files.ReadFile(0, FEIKReadTitleFileParams(TEXT("rules.json")), Completion);
}

void ListEosTitleFiles(IEIKTitleFile &Files,
                       const FEIKAsyncCallback<FEIKEnumerateTitleFilesResult> &Completion)
{
    Files.EnumerateFiles(0, {}, Completion);
}
```

Resolve the interfaces from the current world using `GetUserFileInterface()` or `GetTitleFileInterface()`. Bind completions with `CreateWeakLambda` and check the result. Successful reads return `Result.Result->FileContents`; use `GetDataAsString()` for these UTF-8 files. Enumeration returns `Result.Result->Files`, which can be empty.

Pass valid JSON such as `{"version":1,"musicVolume":0.8}` to the upload example; string conversion does not validate JSON. Upload replaces `settings.json`. Use EIK **Save Game to Bytes** / **Bytes to Save Game** for Unreal SaveGame objects instead.

Use the **Async** user-file nodes for remote transfers. Cache helpers read or change local cached bytes; they are not proof of a completed cloud transfer. Version your save payload and decide how to handle simultaneous writes from different devices.

Upload `rules.json` in the Developer Portal before reading it. **OSSv1 requires Title Storage Tags in EOS settings** and ignores the node’s Tag input; this wrapper uses the first configured tag page. OSSv2 uses the Tag input, with an empty value requesting all files. Shared examples: [cloud saves](/crossplay/cloud-saves), [title files](/crossplay/title-files).

## Epic friends and presence

These features require Epic-account authentication and the matching application permissions. Connect-only login supplies a PUID but no Epic social account.

**Blueprint**

<Accordion>
  <AccordionItem title="Refresh Epic friends">
<BlueprintViewer title="Refresh Epic friends" src="/blueprints/crossplay-eos-query-friends.txt" />
  </AccordionItem>
  <AccordionItem title="Publish lobby presence">
<BlueprintViewer title="Publish lobby presence" src="/blueprints/crossplay-eos-rich-presence.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKFriends.h"
#include "Interfaces/IEIKPresence.h"

bool QueryEpicFriends(UGameInstance &GameInstance,
                      const FEIKAsyncCallback<FEIKQueryFriendsResult> &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Friends = Services->GetFriendsInterface();
    if (!Friends)
        return false;
    Friends->QueryFriends(0, Completion);
    return true;
}

bool PublishEpicLobbyPresence(UGameInstance &GameInstance, const FEIKAsyncCallbackVoid &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Presence = Services->GetPresenceInterface();
    if (!Presence)
        return false;
    Presence->SetRichPresenceText(0, TEXT("Waiting in the lobby"), Completion);
    return true;
}
```

Bind completions with `CreateWeakLambda` and handle failures there. A `true` return only means dispatched. Successful friend queries return `Result.Result->Friends`.

The query returns `Friends`; an empty list can be a successful result. Refresh after relationship changes. **Set EIK Rich Presence Text** also sets your status to **Online** in both backends. Update the text when leaving the lobby; it does not make a game joinable. Use [Crossplay friends and presence](/crossplay/friends-presence) to combine providers.

## Offers and ownership

Sign in with an **Epic account** and configure offers for your application and sandbox. Connect-only login is not enough. Use the returned `FormattedPrice` for display and the selected `OfferId` for checkout. Entitlements contain a separate catalog `ProductId` and a unique `EntitlementId`.

**Blueprint**

<Accordion>
  <AccordionItem title="List store offers">
<BlueprintViewer title="List store offers" src="/blueprints/crossplay-eos-query-offers.txt" />
  </AccordionItem>
  <AccordionItem title="Buy an offer and refresh ownership">
<BlueprintViewer title="Buy an offer and refresh ownership" src="/blueprints/crossplay-eos-checkout.txt" />
  </AccordionItem>
  <AccordionItem title="List owned content">
<BlueprintViewer title="List owned content" src="/blueprints/crossplay-eos-query-entitlements.txt" />
  </AccordionItem>
  <AccordionItem title="Redeem fulfilled content (OSSv2)">
<BlueprintViewer title="Redeem fulfilled content (OSSv2)" src="/blueprints/crossplay-eos-redeem-entitlement.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKCommerce.h"

bool QueryEosOwnership(UGameInstance &GameInstance,
                       const FEIKAsyncCallback<FEIKQueryEntitlementsResult> &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Commerce = Services->GetCommerceInterface();
    if (!Commerce)
        return false;
    Commerce->QueryEntitlements(0, false, Completion);
    return true;
}

void QueryEosOffers(IEIKCommerce &Commerce,
                    const FEIKAsyncCallback<FEIKQueryOffersResult> &Completion)
{
    Commerce.QueryOffers(0, Completion);
}

bool BuyEosOffer(UGameInstance &GameInstance, const FString &OfferId,
                 const FEIKAsyncCallback<FEIKQueryEntitlementsResult> &Ownership)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    const auto Commerce = Services ? Services->GetCommerceInterface() : nullptr;
    if (!Commerce || OfferId.IsEmpty())
        return false;
    FEIKPurchaseOffer Offer;
    Offer.OfferId = OfferId;
    Offer.Quantity = 1;
    Commerce->Checkout(
        0, {Offer},
        FEIKAsyncCallback<FEIKCheckoutResult>::CreateWeakLambda(
            &GameInstance,
            [Commerce, Ownership](const TEIKAsyncResult<FEIKCheckoutResult> &Result) {
                if (!Result.bWasSuccessful)
                {
                    Ownership.ExecuteIfBound(
                        TEIKAsyncResult<FEIKQueryEntitlementsResult>::Failure(Result.Error));
                    return;
                }
                Commerce->QueryEntitlements(0, false, Ownership);
            }));
    return true;
}

bool RedeemFulfilledEosEntitlementV2(
    UGameInstance &GameInstance, const FString &EntitlementId,
    const FEIKAsyncCallback<FEIKQueryEntitlementsResult> &Ownership)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    const auto Commerce = Services ? Services->GetCommerceInterface() : nullptr;
    if (!Commerce || EntitlementId.IsEmpty())
        return false;
    Commerce->RedeemEntitlement(
        0, EntitlementId,
        FEIKAsyncCallbackVoid::CreateWeakLambda(
            &GameInstance, [Commerce, Ownership](const TEIKAsyncResult<void> &Result) {
                if (!Result.bWasSuccessful)
                {
                    Ownership.ExecuteIfBound(
                        TEIKAsyncResult<FEIKQueryEntitlementsResult>::Failure(Result.Error));
                    return;
                }
                Commerce->QueryEntitlements(0, false, Ownership);
            }));
    return true;
}
```

Resolve `IEIKCommerce` through `GetCommerceInterface()`. Bind callbacks with `CreateWeakLambda`; check `bWasSuccessful` and `Result.IsSet()` before reading `Offers` or `Entitlements`. The `bool` returns mean dispatched; checkout and redemption finish through the ownership callback after refreshing entitlements.

Pass an offer returned by the query to **Buy Selected Offer**. A failed or canceled checkout grants nothing; if a purchase is still processing, refresh ownership later instead of starting another purchase. The refreshed list contains all unredeemed entitlements, not just this purchase. Use server-verified ownership for backend-controlled rewards.

**Redemption example: OSSv2 only.** Redeem a consumable's entitlement after your fulfillment flow records its grant, then refresh ownership. The current OSSv1 wrapper calls an unsupported finalization operation and reports success without redeeming. OSSv1 also ignores **Include Redeemed** when querying entitlements. Do not use these results to confirm consumption or retrieve redeemed history on OSSv1.

<Accordion>
  <AccordionItem title="Reports and sanctions">

A report submits a complaint; it does not ban the target. Query sanctions after login and apply the returned `Action` in your game's access checks. Keep privileged sanction-management credentials on your backend.

Use the signed-in reporter's **Product User ID** and the selected player's PUID. Submit only after the player confirms the report. Messages may contain up to **512 UTF-8 bytes**. The node uses the configured EOS platform with either OSSv1 or OSSv2.

**Blueprint**

<BlueprintViewer title="EOS player reports" src="/blueprints/crossplay-eos-player-report.txt" />

Game Instance variable: `ReportInProgress` (**Boolean**, false).

**C++**

Game module dependencies: `EIKBlueprints`, `EIKCore`, `BetideCore`, `Engine`.

```cpp
// EosReportsGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "AsyncFunctions/Reports/EIK_SendReport_AsyncFunction.h"
#include "EosReportsGameInstance.generated.h"

UCLASS()
class UEosReportsGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void ReportSelectedPlayer(const FString &TargetPuid, EEIKPlayerReportCategory Category,
                              const FString &Message);
    UPROPERTY(BlueprintReadOnly)
    bool ReportInProgress = false;

  private:
    UFUNCTION()
    void ReportSucceeded(const FBetideError &Error);
    UFUNCTION()
    void ReportFailed(const FBetideError &Error);
};
```

```cpp
// EosReportsGameInstance.cpp
#include "EosReportsGameInstance.h"
#include "Logging/LogMacros.h"

DEFINE_LOG_CATEGORY_STATIC(LogEosReportExample, Log, All);

void UEosReportsGameInstance::ReportSelectedPlayer(const FString &TargetPuid,
                                                   EEIKPlayerReportCategory Category,
                                                   const FString &Message)
{
    if (ReportInProgress)
    {
        UE_LOG(LogEosReportExample, Warning, TEXT("Wait for the current report to finish"));
        return;
    }
    auto *Request = UEIK_SendReport_AsyncFunction::SendEIKPlayerReportAsyncFunction(
        this, 0, TargetPuid, Category, Message, FString());
    Request->Success.AddDynamic(this, &ThisClass::ReportSucceeded);
    Request->Failure.AddDynamic(this, &ThisClass::ReportFailed);
    ReportInProgress = true;
    Request->Activate();
}

void UEosReportsGameInstance::ReportSucceeded(const FBetideError &Error)
{
    ReportInProgress = false;
    UE_LOG(LogEosReportExample, Log, TEXT("Player report submitted"));
}

void UEosReportsGameInstance::ReportFailed(const FBetideError &Error)
{
    ReportInProgress = false;
    UE_LOG(LogEosReportExample, Warning, TEXT("%s"), *Error.Message);
}
```

**Blueprint**

<Accordion>
  <AccordionItem title="Read player sanctions">
<BlueprintViewer title="Read player sanctions" src="/blueprints/crossplay-eos-query-sanctions.txt" />
  </AccordionItem>
  <AccordionItem title="Submit a sanction appeal">
<BlueprintViewer title="Submit a sanction appeal" src="/blueprints/crossplay-eos-appeal-sanction.txt" />
  </AccordionItem>
</Accordion>

**C++**

Add `EIKBlueprints` and `EIKSDK` for these SDK-facing headers.

```cpp
#include "Engine/GameInstance.h"
#include "Logging/LogMacros.h"
#include "SdkFunctions/SanctionsInterface/EIK_Sanctions_QueryActivePlayerSanctions.h"
#include "SdkFunctions/SanctionsInterface/EIK_Sanctions_CreatePlayerSanctionAppeal.h"
#include "SdkFunctions/SanctionsInterface/EIK_SanctionsSubsystem.h"

DEFINE_LOG_CATEGORY_STATIC(LogEosSanctionExample, Log, All);

void QueryEosSanctions(UGameInstance &GameInstance, FEIK_ProductUserId LocalUserId,
                       FEIK_ProductUserId TargetUserId, const FScriptDelegate &OnComplete)
{
    auto *Request =
        UEIK_Sanctions_QueryActivePlayerSanctions::EIK_Sanctions_QueryActivePlayerSanctions(
            LocalUserId, TargetUserId);
    if (!Request)
        return;
    Request->RegisterWithGameInstance(&GameInstance);
    Request->OnCallback.Add(OnComplete);
    Request->Activate();
}

void ReadQueriedEosSanctions(FEIK_ProductUserId TargetUserId, TEnumAsByte<EEIK_Result> QueryResult)
{
    if (QueryResult != EEIK_Result::EOS_Success)
    {
        UE_LOG(LogEosSanctionExample, Warning, TEXT("Sanctions query failed: %d"),
               int32(QueryResult));
        return;
    }
    const int32 Count = UEIK_SanctionsSubsystem::EIK_Sanctions_GetPlayerSanctionCount(TargetUserId);
    if (Count < 0)
    {
        UE_LOG(LogEosSanctionExample, Warning, TEXT("Unable to read cached sanctions"));
        return;
    }
    for (int32 Index = 0; Index < Count; ++Index)
    {
        FEIK_Sanctions_PlayerSanction Sanction;
        const auto Result = UEIK_SanctionsSubsystem::EIK_Sanctions_CopyPlayerSanctionByIndex(
            TargetUserId, Index, Sanction);
        if (Result != EEIK_Result::EOS_Success)
        {
            UE_LOG(LogEosSanctionExample, Warning, TEXT("Sanction copy failed: %d"), int32(Result));
            continue;
        }
        UE_LOG(LogEosSanctionExample, Log, TEXT("%s | Ref %s | Expires %lld"), *Sanction.Action,
               *Sanction.ReferenceId, Sanction.TimeExpires);
    }
    UE_LOG(LogEosSanctionExample, Log, TEXT("Finished reading %d cached sanctions"), Count);
}

void SubmitEosSanctionAppeal(UGameInstance &GameInstance, FEIK_ProductUserId LocalUserId,
                             const FString &ReferenceId,
                             TEnumAsByte<EEIK_ESanctionAppealReason> Reason,
                             const FScriptDelegate &OnComplete)
{
    auto *Request =
        UEIK_Sanctions_CreatePlayerSanctionAppeal::EIK_Sanctions_CreatePlayerSanctionAppeal(
            LocalUserId, Reason, ReferenceId);
    if (!Request)
        return;
    Request->RegisterWithGameInstance(&GameInstance);
    Request->OnCallback.Add(OnComplete);
    Request->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` before dispatch. The query handler takes `const FEIK_ProductUserId& LocalUserId`, `const FEIK_ProductUserId& TargetUserId`, and `const TEnumAsByte<EEIK_Result>& Result`; call `ReadQueriedEosSanctions(TargetUserId, Result)` there. The appeal handler takes only `LocalUserId` and `Result`; check for `EOS_Success`.

Use the logged-in player's **PUID** for `LocalUserId`. On the count/copy nodes, the pin named **Local User Id actually selects the target whose sanctions were queried**. These SDK nodes use the default EOS platform. The copy wrapper releases native memory itself. `TimeExpires = 0` means permanent; other values are Unix timestamps.

An appeal uses a `ReferenceId` returned by the query for the local player. Pass the reason the player selected, such as **Incorrect Sanction**. Submission does not remove the sanction.

For unified projects, see [Crossplay moderation](/crossplay/moderation).

  </AccordionItem>
  <AccordionItem title="Anti-cheat lifecycle">

This example protects a **dedicated-server match**. Enable the EOS anti-cheat service and launch packaged clients through the protected bootstrapper. See [Epic's service and packaging guidance](https://github.com/EpicGames/EOS-Getting-Started/blob/main/OnlineSubsystemEOS/Plugins/EOSAntiCheat/README.md). Editor compilation does not test protection.

1. Use `BP_Docs_EosAntiCheatServer` as the Game Mode and `BP_Docs_EosAntiCheatRelay` as its **Player Controller Class**. Keep one local player per client process.
2. Call **Start Protection** once after EOS initializes, before admitting players.
3. After your server authenticates a connected player, call **Admit Authenticated Player** with that controller, the verified **PUID**, and platform. Never accept a client-supplied PUID as proof of identity.

**Blueprint**

<Accordion>
  <AccordionItem title="EOS anti-cheat player relay">
<BlueprintViewer title="EOS anti-cheat player relay" src="/blueprints/crossplay-eos-anticheat-relay.txt" />
  </AccordionItem>
  <AccordionItem title="EOS anti-cheat dedicated server">
<BlueprintViewer title="EOS anti-cheat dedicated server" src="/blueprints/crossplay-eos-anticheat-server.txt" />
  </AccordionItem>
</Accordion>

Controller variables: `ClientProtectionStarted`, `ServerPlayerRegistered` (**Boolean**). Game Mode variables: `SessionBound`, `SessionRunning` (**Boolean**). All default to false. Keep both graphs together: the Game Mode calls events on the controller class.

**C++**

`EosProtection.h`

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/GameModeBase.h"
#include "AntiCheat/AntiCheatClient.h"
#include "AntiCheat/AntiCheatServer.h"
#include "EosProtection.generated.h"

UCLASS()
class AEosProtectionController : public APlayerController
{
    GENERATED_BODY()
  public:
    void RegisterAuthenticatedPlayer(const FString &VerifiedPuid,
                                     TEnumAsByte<EUserPlatform> Platform);
    void UnregisterProtectedPlayer();
    UFUNCTION(Client, Reliable) void ClientBeginProtection(const FString &VerifiedPuid);
    UFUNCTION(Client, Reliable) void ClientAntiCheatMessage(const TArray<uint8> &Message);
    UFUNCTION(Server, Reliable) void ServerAntiCheatMessage(const TArray<uint8> &Message);
    UFUNCTION(Server, Reliable) void ServerProtectionFailed();

  protected:
    void EndPlay(const EEndPlayReason::Type Reason) override;

  private:
    UPROPERTY() TObjectPtr<UAntiCheatClient> Client;
    UPROPERTY() TObjectPtr<UAntiCheatServer> Server;
    bool bClientProtectionStarted = false;
    bool bServerPlayerRegistered = false;
    void StopClientProtection();
    UFUNCTION() void SendToServer(const TArray<uint8> &Message);
    UFUNCTION()
    void IntegrityViolated(const FEIK_AntiCheatClientIntegrityViolatedCallbackInfo &Data);
};

UCLASS()
class AEosProtectionGameMode : public AGameModeBase
{
    GENERATED_BODY()
  public:
    AEosProtectionGameMode();
    UFUNCTION(BlueprintCallable) void StartProtection(const FString &ServerName);
    UFUNCTION(BlueprintCallable) void StopProtection();
    UFUNCTION(BlueprintCallable)
    void AdmitAuthenticatedPlayer(APlayerController *Player, const FString &VerifiedPuid,
                                  TEnumAsByte<EUserPlatform> Platform);

  protected:
    void EndPlay(const EEndPlayReason::Type Reason) override;

  private:
    UPROPERTY() TObjectPtr<UAntiCheatServer> Server;
    bool bSessionBound = false;
    bool bSessionRunning = false;
    UFUNCTION()
    void SendToClient(APlayerController *ControllerRef, const TArray<uint8> &ClientData);
    UFUNCTION() void ActionRequired(APlayerController *ControllerRef, bool bRemoveFromSession);
};
```

`EosProtection.cpp`

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
#include "EosProtection.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"

DEFINE_LOG_CATEGORY_STATIC(LogEosProtection, Log, All);

void AEosProtectionController::RegisterAuthenticatedPlayer(const FString &VerifiedPuid,
                                                           TEnumAsByte<EUserPlatform> Platform)
{
    if (!HasAuthority() || bServerPlayerRegistered)
        return;
    Server = GetGameInstance()->GetSubsystem<UAntiCheatServer>();
    if (!Server)
    {
        ServerProtectionFailed();
        return;
    }
    // Keep startup ahead of protection packets on this controller's reliable channel.
    ClientBeginProtection(VerifiedPuid);
    bServerPlayerRegistered =
        Server->RegisterClientForAntiCheat(VerifiedPuid, this, Platform, EOS_ProtectedClient);
    if (!bServerPlayerRegistered)
        ServerProtectionFailed();
}

void AEosProtectionController::ClientBeginProtection_Implementation(const FString &VerifiedPuid)
{
    if (bClientProtectionStarted)
        return;
    Client = GetGameInstance()->GetSubsystem<UAntiCheatClient>();
    if (!Client)
    {
        ServerProtectionFailed();
        return;
    }
    Client->OnSendMessageToServer.AddUniqueDynamic(this, &ThisClass::SendToServer);
    Client->OnClientIntegrityViolated.AddUniqueDynamic(this, &ThisClass::IntegrityViolated);
    // The wrapper installs notifications before BeginSession, even when BeginSession fails.
    bClientProtectionStarted = true;
    if (!Client->RegisterAntiCheatClient(VerifiedPuid))
    {
        StopClientProtection();
        ServerProtectionFailed();
        return;
    }
    UE_LOG(LogEosProtection, Log, TEXT("Client session started; authentication is still pending"));
}

void AEosProtectionController::SendToServer(const TArray<uint8> &Message)
{
    ServerAntiCheatMessage(Message);
}

void AEosProtectionController::ClientAntiCheatMessage_Implementation(const TArray<uint8> &Message)
{
    if (!bClientProtectionStarted)
        return;
    if (!Client || !Client->RecievedMessageFromServer(Message))
    {
        StopClientProtection();
        ServerProtectionFailed();
    }
}

void AEosProtectionController::ServerAntiCheatMessage_Implementation(const TArray<uint8> &Message)
{
    if (!bServerPlayerRegistered || Message.IsEmpty() || Message.Num() > 512 || !Server ||
        !Server->RecievedMessageFromClient(this, Message))
    {
        ServerProtectionFailed();
    }
}

void AEosProtectionController::IntegrityViolated(
    const FEIK_AntiCheatClientIntegrityViolatedCallbackInfo &Data)
{
    StopClientProtection();
    ServerProtectionFailed();
}

void AEosProtectionController::StopClientProtection()
{
    if (!bClientProtectionStarted)
        return;
    const bool bEnded = Client && Client->UnregisterAntiCheatClient();
    bClientProtectionStarted = false;
    if (Client)
    {
        Client->OnSendMessageToServer.RemoveDynamic(this, &ThisClass::SendToServer);
        Client->OnClientIntegrityViolated.RemoveDynamic(this, &ThisClass::IntegrityViolated);
    }
    if (!bEnded)
        UE_LOG(LogEosProtection, Error, TEXT("Client protection cleanup failed"));
}

void AEosProtectionController::UnregisterProtectedPlayer()
{
    if (!HasAuthority() || !bServerPlayerRegistered)
        return;
    const bool bRemoved = Server && Server->UnregisterClientFromAntiCheat(this);
    bServerPlayerRegistered = false;
    if (!bRemoved)
        UE_LOG(LogEosProtection, Error, TEXT("Player protection cleanup failed"));
}

void AEosProtectionController::ServerProtectionFailed_Implementation()
{
    if (!HasAuthority())
        return;
    UnregisterProtectedPlayer();
    Destroy();
}

void AEosProtectionController::EndPlay(const EEndPlayReason::Type Reason)
{
    if (HasAuthority())
        UnregisterProtectedPlayer();
    else
        StopClientProtection();
    Super::EndPlay(Reason);
}

AEosProtectionGameMode::AEosProtectionGameMode()
{
    PlayerControllerClass = AEosProtectionController::StaticClass();
}

void AEosProtectionGameMode::StartProtection(const FString &ServerName)
{
    if (GetNetMode() != NM_DedicatedServer || bSessionBound)
        return;
    Server = GetGameInstance()->GetSubsystem<UAntiCheatServer>();
    if (!Server)
    {
        UE_LOG(LogEosProtection, Error, TEXT("Anti-cheat server subsystem is unavailable"));
        return;
    }
    Server->OnAntiCheatRegisterClient.AddUniqueDynamic(this, &ThisClass::SendToClient);
    Server->OnAntiCheatActionRequired.AddUniqueDynamic(this, &ThisClass::ActionRequired);
    bSessionBound = true;
    bSessionRunning = Server->RegisterAntiCheatServer(ServerName, FString());
    if (!bSessionRunning)
    {
        StopProtection();
        UE_LOG(LogEosProtection, Error, TEXT("Server protection failed; do not admit players"));
    }
}

void AEosProtectionGameMode::AdmitAuthenticatedPlayer(APlayerController *Player,
                                                      const FString &VerifiedPuid,
                                                      TEnumAsByte<EUserPlatform> Platform)
{
    if (!IsValid(Player))
        return;
    auto *ProtectedPlayer = Cast<AEosProtectionController>(Player);
    if (!ProtectedPlayer)
    {
        Player->Destroy();
        return;
    }
    if (!bSessionRunning)
    {
        ProtectedPlayer->ServerProtectionFailed();
        return;
    }
    ProtectedPlayer->RegisterAuthenticatedPlayer(VerifiedPuid, Platform);
}

void AEosProtectionGameMode::SendToClient(APlayerController *ControllerRef,
                                          const TArray<uint8> &ClientData)
{
    if (auto *Player = Cast<AEosProtectionController>(ControllerRef); IsValid(Player))
        Player->ClientAntiCheatMessage(ClientData);
}

void AEosProtectionGameMode::ActionRequired(APlayerController *ControllerRef,
                                            bool bRemoveFromSession)
{
    if (!bRemoveFromSession)
        return;
    if (auto *Player = Cast<AEosProtectionController>(ControllerRef); IsValid(Player))
        Player->ServerProtectionFailed();
}

void AEosProtectionGameMode::StopProtection()
{
    if (!bSessionBound)
        return;
    bSessionRunning = false;
    TArray<AActor *> Players;
    UGameplayStatics::GetAllActorsOfClass(this, AEosProtectionController::StaticClass(), Players);
    for (AActor *Actor : Players)
        if (auto *Player = Cast<AEosProtectionController>(Actor); IsValid(Player))
            Player->ServerProtectionFailed();
    const bool bEnded = Server && Server->UnregisterAntiCheatServer();
    bSessionBound = false;
    if (Server)
    {
        Server->OnAntiCheatRegisterClient.RemoveDynamic(this, &ThisClass::SendToClient);
        Server->OnAntiCheatActionRequired.RemoveDynamic(this, &ThisClass::ActionRequired);
    }
    if (!bEnded)
        UE_LOG(LogEosProtection, Error, TEXT("Server protection cleanup failed"));
}

void AEosProtectionGameMode::EndPlay(const EEndPlayReason::Type Reason)
{
    StopProtection();
    Super::EndPlay(Reason);
}
```

Add `EIKBlueprints`, `EIKSDK`, `EOSIntegrationKit`, `OnlineSubsystem`, and `Engine` to your module dependencies.

**ClientProductID means the player's PUID**, not the Product ID from Project Settings. Leave it empty when starting the dedicated server session. A successful registration starts the exchange; it does not mean client authentication has finished.

Keep the relay RPCs **Reliable** on the same Player Controller. The start RPC runs before that player's first server packet, using [Unreal's reliable RPC ordering](https://dev.epicgames.com/documentation/unreal-engine/remote-procedure-calls-in-unreal-engine). **On Anti Cheat Register Client** carries outgoing bytes, despite its name.

Removal requests disconnect the addressed controller. **Stop Protection** disconnects the match's protected players, removes notifications, and ends the server session; End Play also calls it. End this match before changing worlds. Peer-to-peer and seamless-travel protection need a different session owner and lifecycle.

  </AccordionItem>
  <AccordionItem title="SDK nodes, Web APIs, and uploads">

SDK nodes expose EOS operations directly and may require an explicit query before a copy/get call. Check their result code and release SDK handles where the API requires it. Web API credentials for administrative operations belong on a trusted backend.

Use **Betide → Tools** for build upload settings. BuildPatchTool credentials are separate from runtime SDK credentials. Uploading a game build does not publish title files, define achievements, or enable service permissions.

  </AccordionItem>
</Accordion>
