> For the complete documentation index, see [llms.txt](https://sdk.metamarket.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sdk.metamarket.tech/evaluate-and-integrate/quickstart.md).

# Integration Walkthrough

Register the SDK, sign a player in, send an action, and read back player state. This is the shortest path to a working integration, in the order you would build it.

The detail behind each step is in [Authentication](/evaluate-and-integrate/authentication.md), [SDK Guide](/evaluate-and-integrate/sdk-guide.md), and [Sending Actions](/evaluate-and-integrate/events-and-directives.md). Follow this page first, then use those as reference.

## Prerequisites

* **.NET 10** if you're using the typed SDK (`Game.Sdk` targets `net10.0`). Any other stack needs an HTTP and JSON client and must implement the device-key authentication the SDK otherwise handles: generate a P-256 keypair, store the private key, and sign the proof-of-possession on sign-in, refresh, and handoff (see [Authentication](/evaluate-and-integrate/authentication.md)). The rest of the flow is the same against the [HTTP API](/evaluate-and-integrate/api-guide.md).
* **A registered app id and a gateway address to point at.** App registration is hand-run today: [get in touch](https://metamarket.tech/#contact) to have your app set up. You'll get back an app id and, for the game door, the gateway URL to use.
* **The SDK itself.** There is no public NuGet package; the .NET SDK is delivered as part of onboarding, the same step covered in [Getting access](/evaluate-and-integrate/getting-access.md).

The examples below use the .NET `Game.Sdk` through the gateway; any other stack follows the same steps against the [HTTP API](/evaluate-and-integrate/api-guide.md).

## 1. Register the SDK

At startup, register `Game.Sdk` with the gateway address and app id you were given:

```csharp
builder.Services.AddGameSdk(opts =>
{
    opts.GatewayUrl = "https://your-gateway.example";
    opts.AppId = "your-app-id";
});
```

This registers Bearer auth, the baked `X-App-Id` header, one typed client per domain, and a single `IAppFacade` that exposes all of them under your app id.

`AddGameSdk` does not register the two host-supplied services the SDK depends on. You provide both, because both are platform decisions the SDK cannot make for you:

```csharp
// Required. Without these the container throws when it first resolves a client.
builder.Services.AddSingleton<IDeviceCryptoProvider, YourDeviceCryptoProvider>();
builder.Services.AddSingleton<ISecretStorage, YourSecretStorage>();
```

`IDeviceCryptoProvider` holds the device's P-256 keypair and signs the proof of possession. `ISecretStorage` decides where the private key and the refresh token are kept. The SDK ships a plaintext `ISecretStorage` suitable for development only; a shipping client stores these in the platform keystore.

From there you get the clients either by injecting `IAppFacade` and reading its members, or by injecting a specific client directly. The rest of this page works inside one class, so every service the later steps use is declared once here:

```csharp
public class GameSession(
    IAppFacade app,
    IPlayerSession session,
    IDirectiveProcessor directives)
{
    private readonly IAuthClient _auth = app.Auth;
    private readonly IEventClient _events = app.Events;
    private readonly IPlayerClient _player = app.Player;
    private readonly IPlayerSession _session = session;
    private readonly IDirectiveProcessor _directives = directives;

    // Identifies this app and player to a directive handler that fires a follow-up action.
    private DirectiveExecutionContext Context() =>
        new("your-app-id", _session.PlayerId ?? Guid.Empty, app.Events);
}
```

`IAppFacade` carries the domain clients. `IPlayerSession` holds the signed-in player's id and tokens, and `IDirectiveProcessor` runs any directives an action returns. All three come from the registration above.

## 2. Sign a player in

A device that the platform has not seen before is bootstrapped as a guest, and the server does not check a proof it has no registered key for. The client sends the same body either way:

```
POST /api/auth/device-login
{ "deviceId": "...", "publicKey": "...", "deviceInfoJson": "...", "proof": "..." }
```

Send all four members. The SDK always signs and sends a proof, and on every later sign-in the server verifies it against the key it registered on first contact. A raw HTTP client that sends only `deviceId` and `publicKey` works once and then has no way to sign in again.

On success you get back `{ accessToken, refreshToken, accessTokenExpiresAt }`.

Two things the SDK does not do for you. It does not generate the keypair: your `IDeviceCryptoProvider` owns the key and signs the proof. It does not persist tokens or mark the session as signed in, so activate the session yourself:

```csharp
var login = await _auth.DeviceLoginAsync();
if (login.IsSuccess && login.Data is { } auth)
{
    _session.Activate(auth.PlayerId, auth.AccessToken, auth.RefreshToken, auth.AccessTokenExpiresAt);
}
```

Until you call `Activate`, the session is not authenticated and the automatic refresh path has nothing to work from. [Authentication](/evaluate-and-integrate/authentication.md) covers the full signing contract and what changes once the device is already known.

## 3. Send an action

An action needs two values you supply yourself. The first is the player id: `IPlayerSession.PlayerId` is a `Guid?`, while `ActionRequestDto` takes a `Guid`, so unwrap it and treat a missing value as "not signed in yet". The second is `clientKey`, the idempotency token for this call. Generate a fresh one for each genuinely new action and reuse the same one only when retrying that action, because it is what lets the platform recognise a repeat.

```csharp
if (_session.PlayerId is not { } playerId) { return; }   // not signed in
var clientKey = Guid.NewGuid().ToString();               // fresh per action, reused on retry

var response = await _events.TrackActionsAsync(
    [new ActionRequestDto(playerId, "tap", 1, clientKey, DateTimeOffset.UtcNow)]);

if (!response.IsSuccess || response.Data is not { } env)
{
    return;   // transport failure: no response reached us, so nothing is known about the action
}

if (env.Data is { Skipped: > 0 })
{
    return;   // the call succeeded but the action was not applied. Do not proceed as if done.
}

await _directives.ExecuteAsync(env.Directives, Context());
```

Those three outcomes need different handling:

| Outcome            | What it means                                             | What to do                                                               |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------ |
| Transport failure  | The request may or may not have reached the engine        | The action's fate is unknown. Recovery depends on its deduplication mode |
| Skipped action     | The engine answered and declined to apply it              | A configuration or wiring problem. A retry will not fix it               |
| Business rejection | A `400` with a stable `code`, on the pool and quest verbs | Correct the input or the state                                           |

[Production Operations](/architecture-and-trust/production-operations.md#idempotency-and-retries) sets out what is safe to resend in each case.

Two client behaviours matter when you build error handling around this. The SDK sets no request timeout, so a call falls back to `HttpClient`'s 100 second default, and it retries nothing automatically. A transport failure arrives as `IsSuccess == false` with `StatusCode` 0, which is how you tell a timeout or a DNS failure from a status the server actually returned.

`tap` has to be an action id your app configuration declares. An action the engine does not apply comes back in the response's `skipped` count with HTTP 200 rather than as an error, so confirm `skipped` is `0` before treating the call as done. Several different conditions produce a skip, including a duplicate client key on an action that deduplicates, so do not assume a skip means the action id is wrong. [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure) lists the causes and how to tell them apart. `response.Data` is the `ClientResponse` envelope, and its `.Data` is `ActionBatchResult { Processed, Skipped }`. On the wire, the raw envelope looks like:

```json
{ "data": { "processed": 1, "skipped": 0 }, "effects": [], "mutations": [], "directives": [] }
```

If `skipped` is not `0`, find the cause before resending: a blind retry re-applies whatever did succeed. [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure) covers the failure rule and how to choose between `TrackActionsAsync` and `ExecuteActionsAsync`, and [App Configuration](/evaluate-and-integrate/app-configuration.md) covers how actions get declared.

## 4. Read player state

```csharp
var state = await _player.GetStateAsync();
```

This returns composite player state directly, with no envelope to unwrap. If the action in step 3 touched any resources or derived values, the action response already echoed the change through its `effects` channel, so you do not need this call just to see what the last action did. [SDK Guide](/evaluate-and-integrate/sdk-guide.md) lists every domain client and which ones return an envelope versus plain data.

## Confirming the integration worked

At this point you should be able to check off:

* Sign-in returned a token pair (`accessToken`, `refreshToken`, `accessTokenExpiresAt`) without an error.
* The `tap` action returned `skipped: 0`.
* `GetStateAsync()` returns your player's state, including whatever the `tap` action touched.

If a call fails or behaves unexpectedly, [Troubleshooting](/evaluate-and-integrate/troubleshooting.md) lists the common symptoms and their causes. If `skipped` is not `0`, start with the two most common causes: the action is not declared in your configuration, or it was sent on the lane its configuration does not select. [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure) lists the rest, including a duplicate client key, which means the work already happened rather than that anything is misconfigured. If neither applies, confirm that your `AppId` and `GatewayUrl` match what you were given at registration.

## After this

One player signed in through a device keypair, one action recorded against your configuration, and their state readable back. Everything past this point is the same loop: declare a new action or resource in configuration, send it, read the result. [What You Can Build](/overview/capabilities.md) covers the rest of the engine (quests, packages, bundles, the cross-game currency bridge) on top of these same steps.
