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

# Android SDK

> Install the Alakazam Portal Kotlin library and generate programmable worlds and drop-in AI characters from Kotlin.

The **Alakazam Portal** Kotlin library (`gg.alakazam.portal`) gives you the whole `/v1` surface from Kotlin — generate and manage worlds, create talk-to characters, mint runtime sessions — plus a drop-in **AI Character** component. Requires **`minSdk 24`** (`compileSdk 34`).

## Install

Add the dependency in your module's `build.gradle.kts`:

<CodeGroup>
  ```kotlin build.gradle.kts (Maven Central) theme={null}
  dependencies {
      implementation("gg.alakazam:portal:1.0.0")
  }
  ```

  ```kotlin build.gradle.kts (JitPack) theme={null}
  // settings.gradle.kts
  dependencyResolutionManagement {
      repositories { maven("https://jitpack.io") }
  }

  dependencies {
      implementation("com.github.Alakazam-studios:alakazam-android:1.0.0")
  }
  ```
</CodeGroup>

Source: `https://github.com/Alakazam-studios/alakazam-android`. Minimal deps — `kotlinx-coroutines-android` only; networking, JSON, audio, and key storage all come from the platform.

## Setup

<Steps>
  <Step title="Get a key">
    Start with an `sk_test_…` secret key ([get one here](https://play.alakazam.gg/?view=developer)) — it runs a free, mocked [sandbox](/testing). Swap to `sk_live_…` when you ship.
  </Step>

  <Step title="Store it, or pass it explicitly">
    Unlike the other ports, `AlakazamClient` never reads a key store implicitly — that would need a `Context`. Stash the key with `AlakazamAuth.setApiKey(context, key)` (encrypted at rest via the Android Keystore, AES-256/GCM) and fetch it yourself with `AlakazamAuth.getApiKey(context)`, or just pass the key straight through to the constructor.
  </Step>
</Steps>

<Warning>
  **Secret keys are prototype/server-only.** Your `sk_` key can create worlds and mint sessions, so never ship it inside your app. At runtime, a player talks to a character through a short-lived **session token** minted by your backend — the [two-token rule](/embedding).
</Warning>

## Programmable worlds & AI characters (`/v1`)

`AlakazamClient` is a thin coroutine wrapper over the [`/v1` API](/api-reference) — every method is a `suspend fun`, called from a `CoroutineScope` (e.g. `lifecycleScope.launch { … }`).

```kotlin theme={null}
import gg.alakazam.portal.AlakazamClient

val client = AlakazamClient(secretKey = "sk_test_…")

lifecycleScope.launch {
    // Generate a world from a premise
    val world = client.createWorld(premise = "a neon rooftop at night")
    Log.d("Alakazam", "${world.worldId} · ${world.slug} · mock=${world.mock}")
}
```

List, fork, edit, and import compiled worlds:

```kotlin theme={null}
val mine   = client.listWorlds(limit = 20)                                          // JSONValue tree
val forked = client.forkWorld(world.worldId ?: "")
val edited = client.editWorld(world.worldId ?: "", instruction = "add rain and a flickering sign")

// Import a pre-compiled SMWorld graph verbatim (no recompile)
val imported = client.importWorld(worldJson = compiledWorldJson, name = "My Level")
```

<Note>
  A `sk_test_…` key returns **deterministic mocks** — every response carries `mock == true`, and nothing spends GPU or quota. Build your whole integration for free, then swap to `sk_live_…`. See [Testing](/testing).
</Note>

### Drop-in AI Character

`AlakazamCharacter` wraps the session → say → speak loop as a lifecycle-aware NPC you can drop into any Activity or Fragment — callbacks and audio playback are delivered on the main looper.

<Steps>
  <Step title="Create it">
    Construct with the `characterId` from `client.createCharacter(...)` (below) and, in a shipped app, a `sessionToken` minted by your backend:

    ```kotlin theme={null}
    val npc = AlakazamCharacter(
        characterId = character.id ?: "",
        sessionToken = backendMintedSessionToken,   // never a secret key in a shipped app
    )
    ```
  </Step>

  <Step title="Wire it up">
    Set `onReply` / `onError`, then drive it with the `suspend` methods from a coroutine, or the `*Async` fire-and-forget helpers (`sayAsync`, `speakAsync`, `showImageAsync`).
  </Step>

  <Step title="Prototype without a backend">
    Leave `sessionToken` `null` and pass a `secretKey` instead — the component mints one itself for prototyping. **In a shipped build, never do this** — assign a backend-minted token.
  </Step>
</Steps>

With `autoSpeak` on (default) it plays the reply through `MediaPlayer` automatically. Call `release()` when you're done (e.g. `onDestroy`):

```kotlin theme={null}
val npc = AlakazamCharacter(characterId = character.id ?: "", sessionToken = backendMintedSessionToken)
npc.onReply = { reply -> Log.d("Alakazam", "${reply.stance}: ${reply.reply}") }
npc.onError = { error -> Log.e("Alakazam", error.message, error) }

npc.sayAsync("hello")   // fires onReply, then auto-speaks via MediaPlayer

override fun onDestroy() {
    npc.release()
    super.onDestroy()
}
```

To create the character the component talks to (an editor/server step — needs the secret key), and to mint the session token your backend would hand the client:

```kotlin theme={null}
val character = client.createCharacter(name = "Iris", premise = "a wry rooftop hacker")
val session   = client.mintSession(worldId = character.id ?: "")   // do this on YOUR backend in production
val turn      = client.characterSay(character.id ?: "", session.token ?: "", "who are you?")
Log.d("Alakazam", "${turn.stance}: ${turn.reply}")
```

The component also exposes `speak(line)` / `speakAsync(line)` (synthesize + play a line) and `showImage(prompt)` / `showImageAsync(prompt)` (returns a `show_image` src). For a public, token-free share link to a *public* world, use `AlakazamClient.publicPlayUrl(slug)`.

## `AlakazamClient` reference

Every method maps 1:1 to a `/v1` endpoint. Typed methods decode into a data class; the raw endpoints return a `JSONValue` tree.

| Method                                               | Auth           | Returns                                            |
| ---------------------------------------------------- | -------------- | -------------------------------------------------- |
| `createWorld(premise, name?, frameUrl?, pov?)`       | secret         | `WorldResult` — `{ worldId, slug, cover, mock }`   |
| `importWorld(worldJson, name?, description?)`        | secret         | `WorldResult` — `{ worldId, slug, imported }`      |
| `getWorld(id)`                                       | secret         | `JSONValue` (graph nested under `world`)           |
| `listWorlds(limit, cursor?)`                         | secret         | `JSONValue` — `{ worlds, nextCursor }`             |
| `forkWorld(id)`                                      | secret         | `ForkResult` — `{ worldId }`                       |
| `editWorld(id, instruction)`                         | secret         | `JSONValue` — `{ world, diagnostics, reply }`      |
| `likeWorld(id)`                                      | publishable ok | —                                                  |
| `createCharacter(name, premise, authored)`           | secret         | `CharacterResult` — `{ id, slug, kind, mock }`     |
| `listCharacters(limit, cursor?)`                     | secret         | `JSONValue` — `{ characters, nextCursor }`         |
| `mintSession(worldId, playerIdentity?, origin?)`     | secret         | `SessionToken` — `{ token, expiresIn, slug }`      |
| `connectSession(sessionToken)`                       | session        | `ConnectResult` — `{ worldId, reactorJwt, world }` |
| `refreshSession(sessionToken)`                       | session        | `RefreshResult` — `{ reactorJwt, expiresIn }`      |
| `trackSession(sessionToken, reactorSessionId)`       | session        | —                                                  |
| `endSession(sessionToken)`                           | session        | `EndResult` — `{ ok, seconds }`                    |
| `characterSay(characterId, sessionToken, text)`      | session        | `SayResult` — `{ reply, stance, emotion, action }` |
| `characterTts(characterId, sessionToken, text)`      | session        | `ByteArray` — raw `audio/mpeg` (MP3)               |
| `characterImage(characterId, sessionToken, prompt?)` | session        | `ImageResult` — `{ src, generated }`               |
| `getUsage()`                                         | secret         | `UsageSnapshot` — `{ day, usage, caps, plan }`     |
| `publicPlayUrl(slug)`                                | —              | `String` (static helper)                           |

<Note>
  Optional params: `frameB64` on **createWorld** / **createCharacter** (seed creation from a base64
  image), `cursor` on **listWorlds** / **listCharacters** (pagination — pass the prior `nextCursor`),
  and `ttlSeconds` on **mintSession** (60–3600, omitted when 0). Empty optionals are omitted from the
  request body (never sent as `""`), so payloads match the Unity/Unreal/iOS SDKs.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="iOS SDK" icon="apple" href="/ios">
    The same worlds + AI characters in Swift.
  </Card>

  <Card title="Characters" icon="user-round" href="/characters">
    Stances, memory, voice, and images in depth.
  </Card>

  <Card title="The two-token rule" icon="key-round" href="/embedding">
    Mint session tokens server-side; never ship a secret key.
  </Card>

  <Card title="Testing sandbox" icon="flask" href="/testing">
    Build the whole integration free with a test key.
  </Card>
</CardGroup>
