> 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/sdk-guide.md).

# SDK Guide

For an external game, the SDK is `Game.Sdk`: it talks to the edge gateway and is fixed to your one app id. This page is about that SDK, how to wire it into a host application and use it. Any stack without .NET uses the [HTTP API](/evaluate-and-integrate/api-guide.md) directly and gets the same behaviour.

Below: [wiring the SDK up](#registering-the-sdk), [the app facade](#the-app-facade), [the eight domain clients you get](#domain-clients), [token refresh](#auto-refresh), and [the directive-processing pattern](#handling-directives). For the wire-level envelope shape, see [Sending Actions](/evaluate-and-integrate/events-and-directives.md); for the auth flow, see [Authentication](/evaluate-and-integrate/authentication.md).

## Registering the SDK

`AddGameSdk` is an `IServiceCollection` extension method, called once at host startup and bound from config:

```csharp
builder.Services.AddGameSdk(opts =>
    builder.Configuration.GetSection("GameSdk").Bind(opts));
```

with a config section of `GatewayUrl` (the edge gateway base address) and `AppId` (the single tenant this process represents, e.g. `"your-app-id"`).

It registers a player session, a JWT auth handler, an auth client, the full set of domain clients, and a directive processor with its handler registry. Every built-in directive handler registers as a singleton by convention. A host can override a handler (for example, swap the default reveal presenter for a custom one) by registering after the `AddGameSdk` call, since the last registration wins.

## The app facade

`Game.Sdk` exposes one `IAppFacade`, injected wherever a component needs it. Its `AppId` is fixed from `GameSdkOptions`, and every one of its clients has `X-App-Id` stamped by a delegating handler on the same pipeline as the auth handler. View-model code takes an `IAppFacade` constructor argument, so it never has to manage app context: there is only ever the one game.

## Domain clients

`IAppFacade` bundles eight domain clients:

| Client             | Key methods                                                                        | Purpose                                                                                                                    |
| ------------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `IPlayerClient`    | `GetStateAsync`, `GetLocalStatsAsync`                                              | Composite player state across entities                                                                                     |
| `IPoolItemClient`  | `GetItemsAsync` (all / slot-scoped), `GetStateAsync(slotId, itemSlug)`, `UseAsync` | Delivery-layer discovery + the universal quota-gated `use` verb (quest→accept, package→grant, trade→purchase, listing→buy) |
| `IQuestClient`     | `EvaluateAsync`, `ClaimTaskAsync`                                                  | Quest-only lifecycle verbs, addressed by `(slotId, itemSlug)`                                                              |
| `IEventClient`     | `TrackActionsAsync`, `ExecuteActionsAsync`                                         | The action funnel: the no-receipt lane and the synchronous-receipt lane                                                    |
| `IInventoryClient` | `GetInventoryAsync`, `OpenPackageAsync`                                            | Box inventory + manual open                                                                                                |
| `IBundleClient`    | `GetAllAsync`, `UseAsync`                                                          | Regenerating-charge bundles                                                                                                |
| `IAppClient`       | `GetConfigAsync`                                                                   | This app's config contract                                                                                                 |
| `ISessionClient`   | `GetStartDirectivesAsync`                                                          | Server-driven boot directives (e.g. `process_pending_packages` opening the init box)                                       |

`IAuthClient` (device login, refresh, logout, handoff issue/exchange, unbind) is not part of the facade; it is app-agnostic and injected directly. See [Authentication](/evaluate-and-integrate/authentication.md) for the wire flow.

Every command-shaped method (`UseAsync`, `EvaluateAsync`, `ClaimTaskAsync`, `TrackActionsAsync`, `OpenPackageAsync`, session start) returns `ApiResponse<ClientResponse<T>>`: the envelope wraps the domain result inside `Data` alongside `Directives`/`Effects`/`Mutations`. Query-shaped methods (`GetStateAsync`, `GetItemsAsync`, `GetInventoryAsync`, `GetAllAsync`, `GetConfigAsync`) return `ApiResponse<T>` directly with no envelope, since there is nothing for the client to act on beyond the data itself.

## Auto-refresh

A `DelegatingHandler` attached to every domain-client `HttpClient` handles token refresh transparently:

1. If the session is not authenticated and a refresh token is present, it attempts a refresh first, guarded by a lock so concurrent calls don't race multiple refreshes. A double-check after the lock skips a redundant refresh if another request already completed one. This covers a session with no access token at all, not only an expired one. If the refresh fails or the lock times out, the request is still sent without an `Authorization` header, so the resulting `401` is reported as a plain status rather than as an auth failure.
2. If authenticated, it sets the `Authorization: Bearer` header and, if the session carries a non-zero time offset (Time Warp dev tooling), adds the corresponding header.
3. After the inner call, a `401` response with an `Authorization` header already present is treated as an invalid session and notifies the session of the auth failure. It does not retry the request itself.

The SDK sets no `HttpClient` timeout and installs no retry policy, so a call falls back to the 100 second default and nothing is retried automatically. Build your own retry layer on the contract in [Production Operations](/architecture-and-trust/production-operations.md#idempotency-and-retries). Every call returns an `ApiResponse<T>` rather than throwing: a transport failure comes back with `IsSuccess` false and `StatusCode` 0, which is how you distinguish a timeout or a DNS failure from a status the server actually returned.

This handler deliberately does not add `X-App-Id`; `Game.Sdk` adds that via a separate baked handler.

## Handling directives

Directive execution is never automatic inside a domain client. `ProcessDirectivesAsync<T>` is an extension on `Task<ApiResponse<ClientResponse<T>>>` that the caller chains explicitly:

```mermaid
sequenceDiagram
    participant VM as ViewModel
    participant Client as Domain client
    participant Ext as ProcessDirectivesAsync
    participant Proc as IDirectiveProcessor

    VM->>Client: UseAsync(slotId, itemSlug)
    Client-->>Ext: Task of ApiResponse ClientResponse
    Ext->>Ext: await responseTask
    alt success
        Ext->>Proc: ExecuteAsync(response.Data.Directives, context)
        Ext-->>VM: ApiResponse of T (Data, Effects, Mutations)
    else failure
        Ext-->>VM: ApiResponse of T Failure(...)
    end
```

On success it runs `response.Data.Directives` through the injected `IDirectiveProcessor`, then unwraps to a plain `ApiResponse<T>` carrying `Data` plus the `Effects` and `Mutations` lifted out of the envelope, so a caller that doesn't need the directives still gets the scale/entity echo without threading extra parameters through every client method. A failure short-circuits to `ApiResponse<T>.Failure` without touching the processor.

A typical call site builds an execution context once per call from the session and app id, then chains it directly off the client:

```csharp
private async Task SessionStartAsync()
    => await _sessionClient.GetStartDirectivesAsync().ProcessDirectivesAsync(_directives, Context());

private DirectiveExecutionContext Context()
    => new(_appId, _session.PlayerId ?? Guid.Empty, _events);
```

`DirectiveExecutionContext` (`AppId`, `PlayerId`, `IEventClient`) is a `sealed record`. A directive handler that needs to fire a follow-up action (for example `track_actions`) uses the `IEventClient` bound to the same app the original call was made against. The `.ProcessDirectivesAsync(processor, ctx)` chain is used the same way on quest, bundle, and inventory calls. Event calls need the extra step below, because an action can skip and this helper runs directives before you can inspect `Skipped`.

One caution for directive-fired follow-ups. `ProcessDirectivesAsync` runs the returned directives but does **not** surface a non-zero `Skipped` from an action a directive fires, such as the `process_pending_packages` that a `track_actions` start directive triggers.

That skip can happen for either documented reason, an undeclared action id or wrong-lane routing. So the session-start one-liner is **not** a validated success path for a business-critical follow-up action, and you should not treat it as one.

Two ways to handle it:

* **For a directive-fired action whose outcome matters**, confirm it from the resulting state. For the starter box below, the granted contents arrive in `effects` and in state. Settle the action's declaration and lane during integration testing.
* **For an action you call directly**, use the validation-first pattern: call the raw `IEventClient` method on its own lane, check `Skipped == 0`, then process directives and fold the state channels. See [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure).

Whether directive handlers will propagate a nested action's `Skipped` in future is an engine decision, not something this helper does today.

## First-party modules

MetaMarket's own first-party modules use a separate SDK, `Platform.Sdk`, which addresses several tenants from one process. That is an internal-only path, operated by MetaMarket (see [Product availability](/overview/current-scope.md)); it is not part of an external game integration and is out of scope here.
