---
title: Choose your path
description: Voice apps run on any Bluetooth smart glasses with no vendor setup. Camera and display need vendor credentials. Pick the smaller path first — you can add the other later without rewriting your app.
type: explanation
platform: all
related:
  - /docs/getting-started/with-agent
  - /docs/concepts/capabilities
  - /docs/vendors/meta
---

Extentos has two integration paths, and most apps need only the first. The difference is not how much of Extentos you get — the voice runtime is identical on both. The difference is whether you need vendor credentials at all.

## Which path are you on?

| You are building | Path | Vendor setup |
|---|---|---|
| A voice assistant, dictation, translation, coaching, notes — anything the user talks to and hears back | **Voice** | None |
| Anything that takes a photo, reads a video frame, or draws on the glasses display | **Device** | Required |

If you can't tell yet, start on the voice path. Adding camera later is additive — your handlers, tools and conversation logic don't change.

## The voice path

Smart glasses with a microphone and speaker present themselves to the phone as a standard Bluetooth hands-free audio device. Extentos reaches them through the operating system's own audio routing, so there is no vendor SDK in the picture.

What you need:

- The Extentos SDK (`com.extentos:glasses`). `com.extentos:glasses-ui` is **optional** on this path — it only provides the packaged connection page, which a voice app doesn't render.
- Microphone permission (`RECORD_AUDIO` / `NSMicrophoneUsageDescription`), requested at runtime before you start the assistant
- `INTERNET`, unless you run entirely on [local models](/docs/concepts/local-models) — the managed gateway is a network call
- A foreground service if the assistant must keep listening while your app is backgrounded: `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_MICROPHONE` on Android, `UIBackgroundModes: [audio]` on iOS
- On iOS only, `usedCapabilities: [.microphone, .speaker]` in your config — it picks the transport there, and leaving it empty resolves to real glasses
- **Your project key**, if you use the assistant: `buildConfigField("String", "EXTENTOS_PROJECT_KEY", …)` on Android, `EXTENTOSProjectKey` in the Info.plist on iOS. Neither causes a build error when missing — the assistant just fails at runtime with `AssistantError.NoApiKey`
- On Android, `buildFeatures { buildConfig = true }` — AGP 8 defaults it off and the init snippet reads `BuildConfig.DEBUG`

Full sequencing for both platforms is in the [voice-assistant guide](/docs/guides/voice-assistant).

What you do **not** need: a Meta Wearables Developer Center account, a Meta App ID or Client Token, a GitHub Packages token, a connection page, or a device pairing flow. None of it applies.

<Callout type="warn">
  **One open question on iOS.** The Android artifact split means a voice app
  genuinely doesn't pull the vendor SDK. **iOS has no equivalent split yet** —
  Meta DAT is a dependency of `GlassesCore` on every iOS app, so a voice app
  links it even though it never initializes it. Meta states that its SDK's use
  of `ExternalAccessory` currently triggers App Store rejection under the MFi
  and privacy-manifest rules ([vendor page](/docs/vendors/meta)). **We have not
  verified whether merely linking DAT, without declaring the accessory
  protocols or initializing it, is treated the same way.** If you plan an App
  Store release, treat this as unresolved and budget a review cycle. Android
  and TestFlight are unaffected.
</Callout>

```kotlin
val glasses = ExtentosGlasses.create(
    ExtentosConfig(applicationContext = applicationContext)
)

// `start` and `wake` are suspend functions, so they run inside a coroutine.
scope.launch {
    val session = glasses.assistant.start(provider = AssistantProvider.Managed()) {
        instructions = "You are a running coach. Speak briefly."

        tool("log_split", "Record a lap split for the current run.") {
            ToolResult.Ok("split recorded")
        }
    }
    // Any trigger that calls session.wake() works — here, a wake phrase.
    glasses.voice.onPhrase("hey coach") { session.wake() }
}
```

You don't call `connect()` yourself — `assistant.start(...)` brings the transport up. (`glasses.connection.connect()` exists for apps that want to drive connection explicitly, and is what the packaged connection page calls.)

That is the integration — five short items, none of them a vendor account. You get the complete agent runtime: turn-taking, barge-in, tool calling, conversation memory, local and cloud models, and automatic routing between them.

You also get the primitives on their own, without an assistant session — including the high-quality **on-device voice**, which reads any text your app has in a natural voice rather than the platform's robotic one:

```kotlin
glasses.audio.speak(summary, SpeakConfig(voice = "kokoro-emma"))
```

That works on this path with nothing paired at all. See [on-device models](/docs/concepts/local-models#on-device-voices).

### What it runs on

**Anything the phone can route voice through.** The SDK pins whatever exposes a Bluetooth hands-free (HFP) route and does no device-identity checking at all, so all of these are the same code path:

| Audio route | Works | Notes |
|---|---|---|
| Smart glasses with a mic + speaker | ✅ | Including Ray-Ban Meta — their audio was never carried by the vendor SDK either |
| Ordinary Bluetooth earbuds or headsets | ✅ | AirPods, any HFP headset, a car kit |
| Nothing connected | ✅ | Falls back to the phone's own mic and speaker |

That last row matters more than it looks: your app never dead-ends because the glasses are in their case. The phone always has a microphone and a speaker, so the agent stays usable and picks the glasses back up when they reconnect.

<Callout type="warn">
  **You cannot currently tell which microphone is live.** On this transport
  `glasses.connection.state` reports `Active` whenever audio is available — the
  phone always has a mic and a speaker — so it does **not** distinguish glasses
  from earbuds from the phone itself. A dedicated audio-route readout is on the
  roadmap; until then, don't build a "connected to your glasses" indicator on
  `connection.state`, because it will claim glasses that aren't there.
</Callout>

## The device path

Camera and display go through the vendor's own SDK, which means vendor credentials and a device connection flow. This is where the Meta setup lives.

Add the vendor module alongside the core:

```kotlin
implementation("com.extentos:glasses:2.1.2")
implementation("com.extentos:glasses-meta:2.1.2")   // camera + display
```

The vendor transport registers itself, so `TransportChoice.Auto` resolves to real glasses with no extra code. Meta needs `glasses-meta` because its artifacts sit behind Meta's own credentialed repo; [Brilliant Labs](/docs/vendors/brilliant) needs nothing at all — it ships inside the SDK on both platforms, though it remains preview and unverified on hardware. Then follow [Vendors: Meta Ray-Ban](/docs/vendors/meta#required-setup-for-development) for the credential setup, or [Android XR](/docs/vendors/android-xr) for projected glasses.

## Moving from voice to device later

**Your conversation logic does not change** — handlers, tools and prompts are written against capabilities (`glasses.audio.*`, `glasses.assistant.*`), never against a vendor. What does change is the bootstrap around them, because the vendor transport needs things a voice app never needed:

1. Add the vendor dependency.
2. Complete the vendor's credential setup.
3. Request `BLUETOOTH_CONNECT` and `CAMERA` **before** `ExtentosGlasses.create(...)`. `TransportChoice.Auto` decides the transport inside `create(...)` and keeps it; the vendor arm can only claim if it can see that glasses are bonded, and that read needs `BLUETOOTH_CONNECT`.
4. Pass an `activityProvider` to `ExtentosConfig` — vendor registration needs a live Activity, and it defaults to `null`.
5. Guard the new calls on the capability:

```kotlin
if (glasses.capabilities.camera) {
    val photo = glasses.camera.capturePhoto()
}
```

Steps 3 and 4 are the ones that surprise people; the [voice-assistant guide](/docs/guides/voice-assistant) has the worked bootstrap for a voice **and** camera app.

That guard is the normal shape for every capability on every path — one build ships to devices with different hardware, and absent capabilities are safe, never thrown. See [capabilities](/docs/concepts/capabilities).

### What a device app does when there are no glasses

Once you're on the device path, your app still has to run for a user whose glasses are in a drawer. It degrades rather than breaking:

- The voice agent keeps working over whatever audio the phone has routed — earbuds, or the phone itself.
- `glasses.capabilities.camera` reports `false`, so guarded code simply skips the camera branch.
- An unguarded `capturePhoto()` returns `CaptureError.PlatformError` with `code = "system_audio_no_camera"`. It does not crash, and it does not hang. (Not `NotConnected` — on this transport the connection genuinely *is* up; there is simply no camera behind it.)

<Callout type="warn">
  **Plugging glasses in later does not move a running app onto them.** `Auto`
  resolves once, inside `create(...)`. `glasses.capabilities` is re-read live, but
  it is read *through the chosen transport* — so on the audio baseline
  `capabilities.camera` stays `false` until the app is restarted with the glasses
  already bonded. Design for the restart; don't poll expecting it to flip.
</Callout>

So a camera app and a voice app are the same app on a phone with no glasses connected — you lose exactly the capabilities the hardware was providing, and nothing else.

### Glasses paired, but in their case

This is the common one, and it resolves differently from "never paired" — a Bluetooth *bond* is a stored pairing record that survives the glasses being powered off. So the vendor arm still claims, and `transportChosen` reads `REAL_META` even though there is nothing switched on to talk to.

**Voice still works.** The glasses' microphone and speaker were never reached through the vendor SDK on either platform — they are ordinary Bluetooth hands-free audio, routed by the OS. The Meta transport uses the *same* audio path as the vendorless baseline, so when the glasses are off the OS routes to whatever else is available: earbuds, or the phone itself. Your assistant keeps running and the user keeps talking to it.

**Camera doesn't, and says so.** There's no device session, so `capturePhoto()` returns `CaptureError.NotConnected` — the connection genuinely isn't `Active`.

The two cases stay distinguishable because `capabilities` reports what the *transport's vendor* provides, not what is switched on right now: on `REAL_META` with the glasses in their case, **`capabilities.camera` is still `true`** while `connection.state` is not `Active`. Never-paired is the opposite — `capabilities.camera` is `false` and the state is `Active`. So a camera app guards on both:

```kotlin
val canCapture = glasses.capabilities.camera &&
    glasses.connection.state.value is GlassesState.Active
```

When the user takes the glasses out of the case, the device session comes back on its own and capture starts working again — no restart, because the transport was already `REAL_META`. That's the difference from the never-paired case above, which does need a restart.

## Already shipping? Upgrading from an older SDK

If your app was built before `com.extentos:glasses-meta` existed, the Meta transport used to live inside `com.extentos:glasses`. It doesn't any more, so a camera or display app needs one added line:

```kotlin title="app/build.gradle.kts"
implementation("com.extentos:glasses:2.1.2")
implementation("com.extentos:glasses-ui:2.1.2")
implementation("com.extentos:glasses-meta:2.1.2")   // ← add this if you use camera or display
```

**No Kotlin change is needed.** The module registers itself at app start, so `TransportChoice.Auto` resolves to real glasses exactly as before.

<Callout type="warn">
  **Camera/display apps only — keep your `settings.gradle.kts` as it is.** The Meta DAT repository block and
  its `read:packages` token are still required — those artifacts now arrive
  through `glasses-meta` rather than through `glasses`. Removing the block will
  break your build.
</Callout>

**If you skip the new dependency, nothing fails loudly.** Your build succeeds and your voice features keep working, because the SDK falls back to the audio baseline — but `capabilities.camera` becomes `false` and camera calls start returning errors. The SDK logs a warning at startup naming the missing module (`adb logcat -s Extentos:W`), and `glasses.transportChosen` will read `SYSTEM_AUDIO` where you expect `REAL_META`. Check both after upgrading.

The honest boundary: the *code* delta is small, but the vendor's own credential setup (developer account, app registration, access token) is real work that Extentos cannot remove. Doing it later rather than up front is the saving.
