> 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/app-configuration.md).

# App Configuration

Every registered app (tenant) carries one JSON document, `app_config`, alongside its `app_id`, `app_secret`, `app_type`, and `status` in the app registry. For the registry record itself and trust archetypes, see [Roles](/architecture-and-trust/users-and-actors.md).

In order: [what's in the document](#whats-in-app_config), [the rule that enforces it](#declaring-actions-and-resources), [the app's own S2S credential](#app-s2s-authentication-appkey), [how a change reaches the running engine](#the-configuration-lifecycle), and [how a new app gets registered](#declaring-a-new-app).

## What's in `app_config`

Four fields:

```csharp
public class AppConfigDto
{
    public List<ActionContractDto> AllowedActions { get; set; } = [];
    public MechanicsConfigDto? Mechanics { get; set; }
    public PackageRef? InitPackageRef { get; set; }
    public List<DirectiveDto> StartSessionDirectives { get; set; } = [];
}
```

Field names appear in two forms in this page: the JSON wire names an integrator sends (`allowedActions`, `mechanics.resources`, `mechanics.curves`, `init_package_ref`, `start_session_directives`, `output.require_receipt`) and the C# DTO members the typed SDK exposes (`AllowedActions`, `Mechanics.Resources`, `Mechanics.Curves`, `InitPackageRef`, `StartSessionDirectives`). They are the same fields; the JSON form is what crosses the wire, and the DTO form is what the .NET SDK binds to.

`app_config` is the app's action-and-resource contract: it declares which actions exist and how resources behave, and the engine enforces it on every request. The content a studio builds on top of that contract, its quests, reward packages, pool items, shops (as priced trades), and player listings, is defined as separate configuration provisioned alongside `app_config`, not as more fields inside it. Those content models are covered in [Quests and Progression](/core-concepts/quests-and-progression.md), [Rewards and Inventory](/core-concepts/rewards-and-inventory.md), and [Resources and Quotas](/core-concepts/resources-and-quotas.md); today all of it is prepared and provisioned with the team rather than through a self-serve typed editor.

### Which actions your app can send: `allowedActions`

The verb registry. Every action id (verb) an app can send to `/api/events/track` or `/api/events/execute` must appear here as an `ActionContractDto`:

| Field                                      | What it controls                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                       | The action string the client sends.                                                                                                                                                                                                                                                                                                                                                          |
| `type`                                     | `stat` (verb=noun write), `mechanic` (runs a linked package), `none` (heartbeat/technical), `process_pending_packages` (touch reconciler for pending packages), or `process_available_quests` (touch-driven auto-accept sweep over the player's available quest pool items).                                                                                                                 |
| `scope` (required)                         | `target` (acts on a client-named entity), `root` (account-wide, server-resolved), or `none` (technical, no direct write).                                                                                                                                                                                                                                                                    |
| `idempotency`                              | The deduplication window for a repeated client key: `unique` (the key is remembered indefinitely), `daily` (deduplicated per UTC day), or `none` (no deduplication, so a resend re-applies). Choose `none` only where a double-apply is harmless. What this means when a call fails is in [Production Operations](/architecture-and-trust/production-operations.md#idempotency-and-retries). |
| `rules`                                    | Validation rules run against the incoming value before the action executes.                                                                                                                                                                                                                                                                                                                  |
| `config`                                   | Opaque per-type payload (a `mechanic` action's config carries `LinkedPackageRef` and an optional multiplier flag).                                                                                                                                                                                                                                                                           |
| `output.require_receipt` / `output.reveal` | Routes the action to `/execute`, which returns a receipt, or to `/track`, which does not, and sets whether that receipt is packed into a reveal directive. Both lanes return a response the client must inspect. `require_receipt` defaults to `false`, so an action that does not declare it runs on `/track`.                                                                              |

### How your resources behave: `mechanics.resources`

The noun registry for resources (as opposed to `stat`-type actions, which are declared directly in `allowedActions`). Each key is a resource name; each value is a `ResourceConfigDto`:

```csharp
public class ResourceConfigDto
{
    public required string Scope { get; set; }
    public required ResourceCalculationDto Calculation { get; set; }
    public string? Creation { get; set; }
}

public class ResourceCalculationDto
{
    public required string Type { get; set; }
    public ResourceBoundsDto? Bounds { get; set; }
    public JsonElement? Config { get; set; }
}
```

`Scope` decides where the value is written (bubble-up to the entity's account root, or the acting entity itself); `Calculation.Type` decides how it evolves: `none` for a static counter, `linear` for JIT regen/decay against `Bounds`, or a derived type such as `weighted_avg` for a computed-only resource (mood in a pet-care config, fed by sibling vitals, with no stored row). `Bounds` is a `{ cap, floor }` pair, and `Config` holds strategy-private data such as a linear rate. `Creation: "explicit"` guards a resource so only a `force_create` grant can create its row (used for vitals so a stray grant can't activate a pet's needs before the onboarding stage that is meant to).

### Scaling a reward by a resource's value: `mechanics.curves`

Named response curves, a step function from a resource's current value to a multiplier, looked up by `scale_by` directives inside a grant item's config (e.g. tap XP scaled by a `mood_curve`). The engine pre-computes the multiplier before fulfillment; the shared executor only ever performs a plain value × multiplier calculation.

### What a player gets at enrollment: `init_package_ref`

A `PackageRef(packageId, mode, mark?)` granted at enrollment. One example config uses `Manual` mode with `Mark{open}`: the box is granted pending, and the deferred-touch reconciler opens it on the player's next `process_pending_packages` touch. No client-driven open call is needed to create the starting state.

### What runs at the start of every session: `start_session_directives`

Directives returned verbatim by `POST /api/session/start`. A typical config declares one: a `track_actions` directive naming `process_pending_packages`, so the client fires that action right after session start and the reconciler runs deterministically post-enrollment, without a race against the init package grant. This is the config-only baseline for server-driven boot. The list is config-only: directives cannot be queued or added at runtime. Because it is delivered as a `track_actions` directive, `process_pending_packages` runs on the no-receipt `/track` lane, so it emits no `reveal` directive and returns no skip count to the caller. A package opened by this session-start touch is observed through the resulting `effects` and state; what a test integration verifies here is that the action is declared and on the right lane.

## Declaring actions and resources

The engine enforces one hard rule: **a resource or stat must be declared in config before any action can write it.** There is no implicit schema. An undeclared key is a rejection, not a silent create. Two independent gates enforce this, one per fulfillment path:

* The stat fulfillment path looks up the item's key in `AllowedActions` and requires an entry with `Type == "stat"` and a non-empty `Scope`; otherwise it rejects with a business-logic error ("Integrity Violation" / "Strict Mode: stat ... has no declared scope").
* The resource fulfillment path looks up the item's key in `Mechanics.Resources`; a missing entry or missing `Scope` rejects the same way ("Strict Mode: resource ... is not declared with a scope in mechanics.resources").

The action itself is gated earlier: the request is resolved against `AllowedActions` first. An id not present there logs a rejection and the action never reaches a fulfillment strategy at all. An undeclared verb and an undeclared key fail differently. An undeclared *verb* (not in `AllowedActions`) does not raise a `400`: the action is not processed and is counted in the response's `skipped` on an HTTP 200, which [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure) covers as an integration failure. A declared action whose *stat or resource key* is undeclared is instead a business rejection, surfaced as HTTP 400 with a stable `code`, not a 500; see [HTTP API](/evaluate-and-integrate/api-guide.md#errors) for the full error-code reference.

```mermaid
flowchart TD
    A["/api/events/track or /execute"] --> B{"action.id in\nAllowedActions?"}
    B -- no --> R1["not processed:<br/>counted in skipped (HTTP 200)"]
    B -- yes --> C{"action.type"}
    C -- stat --> D{"key in AllowedActions\nas type=stat, scope set?"}
    C -- mechanic --> E["runs linked package<br/>(items may write resources)"]
    D -- no --> R2["BusinessLogicException:<br/>Integrity Violation / no scope"]
    D -- yes --> F["write via declared scope"]
    E --> G{"item key in\nmechanics.resources,\nscope set?"}
    G -- no --> R3["BusinessLogicException:<br/>not declared with a scope"]
    G -- yes --> H["write via declared scope<br/>+ calculation strategy"]
```

## App S2S authentication (AppKey)

App-to-app calls (an app acting on its own behalf, not a player acting through a JWT) authenticate with a distinct credential from player auth: `X-App-Id` + `X-App-Secret` headers, validated server-side against the app registry. On success the archetype (`app_type`) is available to downstream authorization checks. This app credential is a separate perimeter from the player JWT described in [Authentication](/evaluate-and-integrate/authentication.md). For an external game you never handle it directly: the edge gateway holds your app secret and attaches it when it forwards your event calls.

The secret itself never reaches a game client. The gateway in front of an external game holds that one app's secret in its own server-side configuration, separate from the Hub's copy. When a game client calls the event-tracking path through it, the gateway attaches `X-App-Id` + `X-App-Secret` for its configured app to the outbound call to the engine itself. The inbound request from the game carries only its player JWT and `X-App-Id`. Non-event calls (player-scoped GETs/POSTs) instead pass the player's JWT through unchanged, with no AppKey translation. `system`/`friend_of_system` apps on the platform door don't go through this proxy at all. They hold their own app secret in their own service config and present it directly. None of these secret values are reproduced here.

The gateway in front of an external game is provisioned for one app: it holds that app's secret and will not attach a secret for, or forward, any other `X-App-Id`. Player-facing calls receive no AppKey translation, so their tenant is fixed by the single `X-App-Id` the gateway is configured to accept together with the player JWT they carry; there is no other app id a client could name through that gateway. Multi-tenant access from one process is the platform's own internal path, not this external surface; see [Security and Tenant Isolation](/architecture-and-trust/security-and-tenant-isolation.md).

## The configuration lifecycle

A game's `app_config` is one document per app, and the content configured alongside it (quests, packages, pool items, trades, listings) follows the same path from an edit to running behavior: no code change, no client release, no redeploy.

1. **Author.** The configuration is written as a typed `AppConfigDto`, its `AllowedActions`, `Mechanics.Resources`, `Mechanics.Curves`, `InitPackageRef`, and `StartSessionDirectives`, prepared and reviewed with the MetaMarket team during onboarding rather than through a self-serve editor.
2. **Apply.** The reviewed configuration is provisioned by the platform, keyed by app id; re-provisioning the same app updates its configuration in place.
3. **Take effect.** The engine reads configuration per request through a short-lived cache (about ten minutes), so a change reaches the running engine within that window without a redeploy. What the engine runs at request time is data, not compiled logic.

Validation is the same declare-before-write gate described above: the engine rejects an action or resource write whose key is not declared in configuration, at the moment a request tries to use it. A configuration gap surfaces as a clear rejection on the first request that exercises it.

A designer-facing authoring interface for this configuration is planned; see the [Roadmap](/releases-and-reference/roadmap.md).

You prepare a change with the MetaMarket team, we review it, and re-provisioning replaces your app's configuration in place. Once activated, it reaches the running engine within the refresh window above.

Plan around three limits:

* **No staging environment.** You cannot rehearse a change before it goes live.
* **No version history.** Rolling back means re-applying the previous configuration, so keep your own copy of each version.
* **Validation happens on use, not on activation.** A bad key does not fail when you apply it. It fails on the first request that touches it.

## Operating a change

Studios can update supported mechanics without requiring a new game client release. Configuration changes are prepared and reviewed through the current MetaMarket-assisted workflow: the studio defines the mechanic and its rules, the team helps translate them into configuration, the change is reviewed, a supported test scenario is agreed, the studio validates the behaviour against a test player, and the approved configuration is then activated.

Example: a seven-day retention campaign. The game designer and economy designer define the daily milestones and the rewards. The team prepares or updates the quest and reward configuration. A studio developer runs the flow against named test users and confirms each day pays out as intended. The approved configuration is activated, and after launch the live-ops manager reviews how it is performing from the operational records the team pulls (there is no self-serve stream today; see [Operational Data](/use-cases/engagement-data-and-operational-insights.md)). The developer is needed to drive and verify the integration, not to hand-write every rule.

## Declaring a new app

A new app is registered with the team; see [Getting Access](/evaluate-and-integrate/getting-access.md). In practice that means agreeing its `AllowedActions`, `Mechanics.Resources`, optional `Mechanics.Curves`, `InitPackageRef`, and `StartSessionDirectives` as one `AppConfigDto`, which the platform then provisions.

***

Last reviewed 2026-07. Documentation release `docs-release-2026-07`; runtime `net10.0`. The configuration schema (`AppConfigDto`) is unversioned, as are the SDK and HTTP API ([Compatibility](/releases-and-reference/compatibility.md)). Status source of truth: [Product availability](/overview/current-scope.md).
