> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pioneeroptimisation.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent integration guide

> The integration spec, written for a reader that executes it.

<Note>The code on this page is extracted from a sample project that compiles against each SDK release. The guide cannot show code that does not build. Every snippet is regenerated from the release the install step pins, so the code matches the version you install.</Note>

This is the executable path through these docs, written for a coding agent with a human on hand for the few steps only a human can do. Follow the steps in order. Each ends with a check that either passes or names the fix.

Two reading rules. Treat every account id and project id as an opaque string: read them from tool responses or from your human, never construct or guess one. Where a step says hand-off, stop, tell your human exactly what to do and where, and wait for their confirmation.

## Preconditions

* A Unity 2022.3 LTS or newer project that compiles cleanly.
* Write access to `Packages/manifest.json`, `Assets/`, and the startup scene.
* A PIO account, and a human who can reach its dashboard.
* Recommended: the [PIO MCP server](/ai/mcp-server) connected. It provides `check_integration`, the live readout this guide's verification loop uses. Without it, each verification falls back to the dashboard through your human.

## Step 1: install the SDK

The SDK installs from the public PIO package registry. No key, login or `.upmconfig.toml` is needed.

Add the registry, the SDK and its one dependency to `Packages/manifest.json`:

```json Packages/manifest.json theme={null}
{
  "scopedRegistries": [
    {
      "name": "PIO",
      "url": "https://packages.pioneeroptimisation.com",
      "scopes": [
        "com.pioneeroptimisation"
      ]
    }
  ],
  "dependencies": {
    "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#2.5.11",
    "com.pioneeroptimisation.sdk": "0.3.6"
  }
}
```

Merge these entries into the existing `scopedRegistries` and `dependencies` blocks; every manifest edit in this guide is additive, and replacing the file would drop the project's built-in modules. The SDK is pre-1.0, so pin the exact version shown, never a range. [Installation & versioning](/integration/installation-and-versioning) carries the upgrade rules.

Check: the project compiles and `using PIO;` resolves. Without the Editor open, compile in batch mode, with `UNITY_EDITOR` set to the editor binary for the project's Unity version:

```bash theme={null}
"$UNITY_EDITOR" -batchmode -nographics -quit -projectPath . -logFile -
```

Exit code 0 and no `error CS` lines in the log mean the install compiles.

The log never confirms that the packages resolved. Check `Packages/packages-lock.json`: it must lock `com.pioneeroptimisation.sdk` at the pinned version from the PIO registry, and UniTask at the pinned revision. A failed or partial resolve can leave an older cached version compiling, and still exits 0. A registry `401`, `403` or `404` in the log means the registry refused the request. Check the registry URL matches the snippet exactly. If it does, hand off to your human.

On macOS the editor binary lives at `/Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/MacOS/Unity`; export it as `UNITY_EDITOR` once and every later step reuses it.

## Step 2: choose a project and collect credentials

Three values connect the SDK to a project: `ApiKey`, `AccountId`, and `ProjectId`.

With the MCP server:

1. Call `whoami`. It names the account, your scopes, and the account's projects.
2. Use the project your human names, or create one with `create_project`. A project you create starts under a not-child-directed default and accepts events at once. A studio member must confirm the child-directed answer in the project's settings within 14 days (the `confirm_by` date in tool responses), or the key is unpublished.
3. Call `get_api_key` for the project. The response carries the API key and the account id. It needs an admin credential; a `forbidden` error names who can help. While the confirmation is outstanding the response carries `attestation_pending: true` and the `confirm_by` date; the key works throughout the grace.

Without the MCP server, hand-off: ask your human for all three values from the project's settings page in the dashboard.

Keep the API key out of version control. Put it in an ignored file or an environment variable, and leave serialised credential fields empty in committed scenes and assets.

## Step 3: write the integration

Create `Assets/Scripts/PioBootstrap.cs` containing exactly this file. It initialises once at startup, registers the action definitions with fallbacks, surfaces errors while integrating, requests a decision, and tracks events:

```csharp Samples~/BasicIntegration/Assets/Scripts/PioBootstrap.cs theme={null}
using System;
using System.Collections.Generic;
using PIO;
using UnityEngine;

// The PIO class shares its name with the PIO namespace, so a script outside
// the PIO namespace must bring the statics in explicitly.
using static PIO.PIO;

/// <summary>
/// The agent integration guide's bootstrap, verbatim: attach to a GameObject
/// named PIO in the first loaded scene, fill the credentials, press Play.
/// Lives in the global namespace on purpose, so it compiles exactly as
/// pasted into a fresh project.
/// </summary>
public class PioBootstrap : MonoBehaviour
{
    [SerializeField] string apiKey = "";
    [SerializeField] string accountId = "";
    [SerializeField] string projectId = "";

    async void Start()
    {
        // Headless setups export credentials instead of typing them into the
        // Inspector; a filled Inspector field always wins, and nothing writes
        // a key into a committed scene or asset.
        apiKey = string.IsNullOrEmpty(apiKey) ? Environment.GetEnvironmentVariable("PIO_API_KEY") ?? "" : apiKey;
        accountId = string.IsNullOrEmpty(accountId) ? Environment.GetEnvironmentVariable("PIO_ACCOUNT_ID") ?? "" : accountId;
        projectId = string.IsNullOrEmpty(projectId) ? Environment.GetEnvironmentVariable("PIO_PROJECT_ID") ?? "" : projectId;

        // Surface every SDK error as a console line while integrating.
        OnError += e =>
            Debug.LogWarning($"[PIO] {e.Severity} {e.Code}: {e.Message}");

        var config = new PioConfig
        {
            ApiKey = apiKey,
            AccountId = accountId,
            ProjectId = projectId,
            Actions = new Dictionary<string, ActionDef>
            {
                // The fallback serves whenever the backend cannot answer.
                ["Tutorial"] = new ActionDef { FallbackAction = "Short" },
            },
        };

        await Init(config);
        if (this == null) return; // Destroyed during the await: stop.
        if (!IsReady)
        {
            Debug.Log($"PIO init failed: {State}");
            return;
        }

        // Ask at the decision point and apply the returned action. A null
        // Action marks the baseline control group; a null result means
        // tracking is disabled; both show your control experience.
        var decision = await GetDecision("Tutorial");
        if (this == null) return;
        ApplyTutorialLength(decision?.Action ?? "Short");

        // A valued event: the value feeds reward derivation.
        TrackEvent("level_complete", 1.0);

        // A bare occurrence, no value.
        TrackEvent("tutorial_complete");
    }

    void ApplyTutorialLength(string action) =>
        Debug.Log($"Tutorial length: {action}");
}
```

The `using static PIO.PIO;` line matters. The `PIO` class shares its name with the `PIO` namespace, so qualified calls like `PIO.Init` do not resolve from a script outside that namespace.

Each key you register must also exist as an action definition in the project. With the MCP server, call `create_action_definition` with the name, the candidate actions, and the default. The candidate actions are a game design decision, not something the guide or the tools can infer; take them from your task brief, or ask your human. This sample's `Tutorial` definition offers `Short`, `Medium` and `Long`, with `Short` as the default, matching the code's fallback. Register the event types the game will track the same way, with `register_event_type`. Without the MCP server, hand-off: your human creates both in the dashboard.

Wire it into the game: add a GameObject named `PIO` to the first loaded scene and attach `PioBootstrap`, filling the three credential fields in the Inspector.

Headless, or in a fresh project with no scene, create the scene with a throwaway Editor script instead of hand-writing scene YAML. The script only builds the scene; credentials never touch it. `PioBootstrap` itself reads `PIO_API_KEY`, `PIO_ACCOUNT_ID`, and `PIO_PROJECT_ID` from the environment when its Inspector fields are blank, so no key lands in a committed file:

```csharp Assets/Editor/PioSetup.cs theme={null}
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

public static class PioSetup
{
    public static void Run()
    {
        System.IO.Directory.CreateDirectory("Assets/Scenes");
        var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
        var go = new GameObject("PIO");
        go.AddComponent<PioBootstrap>();
        EditorSceneManager.SaveScene(scene, "Assets/Scenes/Main.unity");
        var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes);
        if (!scenes.Exists(sc => sc.path == "Assets/Scenes/Main.unity"))
        {
            scenes.Insert(0, new EditorBuildSettingsScene("Assets/Scenes/Main.unity", true));
        }
        EditorBuildSettings.scenes = scenes.ToArray();
    }
}
```

The scene list is appended, never replaced, so an existing project keeps its build entries. Run the script once, then delete the file:

```bash theme={null}
"$UNITY_EDITOR" -batchmode -nographics -quit -projectPath . -executeMethod PioSetup.Run -logFile -
```

Export the same three variables for every headless run of the game or its tests; the Inspector path needs none of them.

This guide configures the SDK in code. The SDK also offers a `PioSettings` asset (menu `PIO/Setup Wizard`), which the zero-argument `PIO.Init()` loads. The `PIO/Verify Integration` window expects that asset and reports a missing-settings failure even when a code-configured integration works; create the asset, or read past that one check.

The whole change, for a game with one decision point:

| File | Change |
| - | - |
| `Packages/manifest.json` | Two dependency lines |
| `Assets/Scripts/PioBootstrap.cs` | New file, the code above |
| First loaded scene | One GameObject carrying `PioBootstrap`; the scene itself, when none exists |
| `Assets/Editor/PioSetup.cs` | Temporary setup script; deleted after it runs |

## Step 4: run the game

Enter Play mode and pass through the decision point, so the SDK initialises, serves a decision, and sends events.

Headless, drive the same path with a PlayMode test run under `-runTests -testPlatform PlayMode`. Two pieces of setup come first:

1. Add `com.unity.test-framework` (`1.1.33`) to `Packages/manifest.json`; step 1's manifest does not include it.
2. Test assemblies cannot reference the default `Assembly-CSharp`, so give `Assets/Scripts` its own assembly definition. Put the test assembly definition under `Assets/Tests/PlayMode`. Delete the temporary `Assets/Editor/PioSetup.cs` after scene setup.

Use the sample's two assembly definitions, preserving their names and references. The code-block titles identify their source files in the SDK.

Copy this file to `Assets/Scripts/PIO.Samples.asmdef`:

```json Samples~/BasicIntegration/Assets/Scripts/PIO.Samples.asmdef theme={null}
{
  "name": "PIO.Samples",
  "rootNamespace": "PIO.Samples",
  "references": [
    "PIO.Runtime"
  ],
  "includePlatforms": [],
  "excludePlatforms": [],
  "allowUnsafeCode": false,
  "overrideReferences": false,
  "precompiledReferences": [],
  "autoReferenced": true,
  "defineConstraints": [],
  "versionDefines": [],
  "noEngineReferences": false
}
```

Copy this file to `Assets/Tests/PlayMode/PIO.Samples.Tests.PlayMode.asmdef`:

```json Samples~/BasicIntegration/Assets/Tests/PlayMode/PIO.Samples.Tests.PlayMode.asmdef theme={null}
{
  "name": "PIO.Samples.Tests.PlayMode",
  "references": ["PIO.Samples", "PIO.Runtime", "UnityEngine.TestRunner", "UnityEditor.TestRunner"],
  "overrideReferences": true,
  "precompiledReferences": ["nunit.framework.dll"],
  "defineConstraints": ["UNITY_INCLUDE_TESTS"]
}
```

The test, exactly:

```csharp Samples~/BasicIntegration/Assets/Tests/PlayMode/PioIntegrationPlayModeTests.cs theme={null}
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using static PIO.PIO;

/// <summary>
/// Drives the guide's bootstrap end to end: loads the scene the setup script
/// built, lets PioBootstrap run Init, a decision, and both events, then waits
/// one flush interval so the queue ships before the test ends. Export the
/// three PIO_* variables before running it headless. Delete the test assembly
/// after verification if the project does not keep tests.
/// </summary>
public class PioIntegrationPlayModeTests
{
    [UnityTest]
    public IEnumerator BootstrapInitialisesAndShipsEvents()
    {
        yield return SceneManager.LoadSceneAsync("Main", LoadSceneMode.Single);

        var pio = GameObject.Find("PIO");
        Assert.IsNotNull(pio, "PIO GameObject missing from the Main scene");
        Assert.IsNotNull(pio.GetComponent<PioBootstrap>(), "PioBootstrap not attached");

        // Start() is async void, so the scene load returns before init runs.
        // Wait for readiness first, or the flush timer below starts before
        // any event has been tracked.
        var deadline = Time.realtimeSinceStartup + 30f;
        while (!IsReady && Time.realtimeSinceStartup < deadline)
        {
            yield return null;
        }
        Assert.IsTrue(IsReady, $"PIO not ready within 30 seconds (state: {State})");

        // A few seconds for the decision and both events to be tracked, then
        // one flush interval (30 seconds by default) plus a little slack, so
        // the tracked events leave the queue before the test ends.
        yield return new WaitForSeconds(5f);
        yield return new WaitForSeconds(31f);
    }
}
```

It loads the scene from step 3 and lets `PioBootstrap.Start()` drive the whole loop: init, the decision, and both events, with one flush interval waited so the queue ships. Delete the test assembly after verification if the project does not keep tests.

Run it with a results file; for test runs the exit code alone is not a reliable pass signal:

```bash theme={null}
"$UNITY_EDITOR" -batchmode -nographics -projectPath . \
    -runTests -testPlatform PlayMode -testResults "$PWD/playmode-results.xml" -logFile -
```

Read the pass or fail from `playmode-results.xml`.

## Step 5: verify

With the MCP server, call `check_integration` after each run and read three things:

* `status` and `streaming`: `never` means nothing has arrived yet; `integrated` with `streaming: true` means events are flowing now.
* `coverage`: track the event types the project expects, listed in `coverage.expected`. Types under `unregistered` are arriving without being registered; they still flow through, with a console warning. Register them with `register_event_type`; without the MCP server, your human does it in the dashboard.
* `next_step`: the one thing to do now. Do it, run again, and check again.

Two timing facts matter here. A successful flush writes nothing to the console at any verbosity, and `check_integration` can lag minutes behind real delivery. After a clean run, poll it patiently rather than reading a single `never` as failure.

Without the MCP server, hand-off: your human opens [integration health](/dashboard/integration-health) and reads the same facts, or runs `PIO/Verify Integration` in the Editor.

You are done when `check_integration` reports `status: "integrated"` with your event types seen, or your human confirms the dashboard shows events arriving. Integration is one early stage of six; `get_project_status` names what remains after it, none of which needs code.

## When something fails

* Init fails: `PIO.State` names the phase, and the `OnError` line carries the code. Re-read the three credential values before touching code; most failures are a wrong or swapped id.
* Events sent, console clean, `status` still `never`: normal for the first minutes after a fresh integration. Keep polling `check_integration`; client-side silence is what success looks like.
* Attestation overdue: a project unconfirmed past its `confirm_by` date has its key unpublished. Init then fails with a JSON parse error at the auth call, nothing naming attestation. `check_integration` reports `attestation_overdue`; confirm in project settings to restore events.
* Decisions arrive but events never do: decisions and events travel to different services. Leave every `*UrlOverride` field blank; see [Verify your integration](/getting-started/verify-your-integration). When overrides are set deliberately, for a PIO internal environment, check their values instead of blanking them.
* `INGESTION_FAILED_PERMANENTLY`, events dropped after retries: the SDK queues events on disk under Unity's Company Name and Product Name, not under a PIO project. A machine that ran a different project under the same names retries that stale backlog, then drops it. If `check_integration` shows this run's events as seen, the drops were stale and nothing is wrong. To clear the backlog, quit the application, then delete `pio_events.db` under `Application.persistentDataPath`. Leave the Product Name alone; changing it moves the whole persistent-data directory, saves included.
* Any MCP tool error returns `error`, `message`, and `next_step`. Act on `next_step` rather than retrying the same call.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.