Appetize.io is a trademark of its respective owner, used here for identification and migration comparison only. No endorsement, partnership, or affiliation is implied.
POST /v1/apps), then embed and drive the device in the browser with the JavaScript SDK
(client.startSession(), session.tap, session.type, session.screenshot). It’s a great way to
demo, test, and automate a mobile build. But an Appetize account is also a standalone device
vendor that sits apart from everything else your agent touches:
- Every device lives behind a single org
X-API-KEY, disconnected from wherever the agent’s cards, email, phone number, and KYC live. - The token is coarse and ungoverned: any code holding it can upload a build, spin up a device, and drive it — there is no approval step between “the agent has the key” and “the agent is tapping around a live device,” and nothing scopes one tenant’s devices away from another beyond conventions you maintain yourself.
- “Which agent spun up that device, who approved it, and what else can this agent do?” is answered in the Appetize dashboard for sessions, and in unrelated systems for everything else. The device has no shared accountability with the rest of the agent’s footprint.
mobile primitive gives the agent the same capability — a
hosted cloud Android/iOS device you can put an app on, stream, and control — but the device is
rooted in one governed identity with permissions enforced at execution time:
- Naive owns the upstream Mobilerun operator key and scopes every device to your tenant — the agent never holds a device-cloud key (the same operator model as Compute with AWS).
- Provisioning a device, its lifecycle (reboot/reset/terminate), proxy changes, running/stopping
tasks, uploading apps, and mutating wildcard calls are approval-gated — an agent call may
return
{ status: "pending_approval" }until a human approves it, so a leaked agent key can’t silently spin up or drive a device. - Naive adds a natural-language agent driver:
naive.mobile.run({ task, deviceId })executes “Open Settings and enable dark mode” on-device — where Appetize gives you a raw device plus per-action control that you script yourself. - The tenant user whose Account Kit enables
mobileis the same user that owns the agent’s cards, its email inboxes, its phone number, and its vault — one identity, one audit trail, revocable in one place.
mobile primitive, shows the smallest working
swap, and is explicit about the Appetize features that don’t map yet.
Tested against: the Appetize REST API v1 (base
https://api.appetize.io/v1, X-API-KEY
header — POST /v1/apps, GET /v1/apps, GET /v1/usageSummary), the Appetize JavaScript SDK
(js.appetize.io/embed.js → window.appetize.getClient(selector, config), client.startSession,
session.tap / session.type / session.screenshot / session.playAction), and the automation
package @appetize/playwright v1.6.0 — and
the Naive mobile primitive over the Naive API (base https://api.usenaive.ai/v1), the
naive.mobile.* SDK sub-client (@usenaive-sdk/server), and the
Naive CLI. Docs snapshot July 2026.Version notes:- Appetize v1 identifies a build by its
publicKey; the v2 API renames itbuildId(the two are interchangeable in v1). Naive addresses a device by its Mobilerundevice_idand an app by its library entry — see the concept map. - Appetize control is client-side, per-action: you load
embed.js, get aclient, start asession, and script eachtap/type/screenshot. Naive’s core driver is a server-side natural-language agent task (naive.mobile.run) plus raw device tools reached through the wildcardsearch/callpair. - On Naive, provision, device lifecycle,
run,stop,uploadApp, and mutatingcalls are approval-gated by default — Appetize has no equivalent gate; the org token does everything. - Naive phones are billed per minute from your credits and there is no upstream auto-stop —
always
terminatea device you’re done with (credit exhaustion and a max-runtime failsafe auto-terminate as a backstop).
Concept map
| Appetize.io | Naive | Notes |
|---|---|---|
X-API-KEY — one org token uploads, runs and controls | nv_sk_ agent key; Naive holds the Mobilerun operator key — the agent never holds a device-cloud key | Same operator model, but per-tenant scoped and approval-gated |
POST /v1/apps (multipart file/url, platform) → { publicKey } | naive.mobile.uploadApp({ name, fileName }) → signed upload URL | Upload an app (APK/IPA) into the library — approval-gated |
GET /v1/apps (list your apps) | naive.mobile.apps() | The app library |
getClient(sel, { buildId, device, osVersion }) + iframe appetize.io/embed/{publicKey} | naive.mobile.provision({ country }) → { phone, credits_per_minute } + naive.mobile.waitReady(id) | Naive provisions a real cloud device — approval-gated; you don’t embed by build |
client.startSession() → session (device tied to a build) | naive.mobile.provision(...) then naive.mobile.stream(id) → { stream_url, stream_token, console_url } | A device you can display live |
session.tap / type / swipe / playAction / waitForElement (you script each action) | naive.mobile.run({ task, deviceId }) → an NL agent task; task / screenshots / trajectory / stop | Naive adds a natural-language driver; raw device tools via wildcard call |
session.screenshot('base64') | naive.mobile.call("take-screenshot", { path: { deviceId } }) | Raw device tool via the wildcard |
client.setConfig({ device, osVersion }) (device/OS matrix) | naive.mobile.provision({ type?, profile_id?, country }) | Device selected at provision — see gaps |
session.end() / session timeout | naive.mobile.terminate(id) — stops the per-minute meter | Both metered by the minute |
GET /v1/usageSummary (per-app session minutes) | naive.forUser(id).logs.query() + plan usage | Usage on the identity’s one timeline |
@appetize/playwright (toHaveElement, toMatchSnapshot) | (none) | Test-assertion framework — see gaps |
| (no approval step — the token does everything) | provision / lifecycle / run / uploadApp / mutating call → pending_approval | The execution-time permission win — see gain #2 |
| Org token scopes Appetize and nothing else | Account Kit enables mobile per user; metered against the plan | Governance on the identity — see gain #2 |
| Appetize dashboard sessions | naive.forUser(id).logs.query() — one per-user timeline | Device events beside cards, email, phone — see gain #3 |
Before / after: the core path
The path that matters for almost every device-backed agent is get a cloud device with your app on it, stream it, then drive it. Here it is on both platforms.- The agent never holds a device-cloud key. Appetize drives the device with the same org token
it uploads with. On Naive, Naive holds the Mobilerun operator key and scopes every device to your
tenant; the agent only ever holds its
nv_sk_key. - Provisioning and driving are approval-gated.
provision, lifecycle,run,uploadApp, and mutating wildcardcalls can require a human approval before they execute — a leaked agent key can’t silently spin up or drive a device. - Driving is natural-language, not per-action scripting. Appetize expects you to script each
tap/type/screenshot. Naive’srunexecutes a plain-English task on-device (the raw device tools are still there via the wildcardcall). - The device is your identity, not a separate account. The same tenant user that owns this device owns the agent’s cards, email, phone, and vault.
Approvals: the execution-time gate
This is what “governed identity” means in practice. On Appetize, the org token spins up and drives devices with no gate. On Naive, the sensitive operations are approval-gated, so an agent’s request to provision or drive a device can require a human to approve it first.- Sensitive operations are gated server-side. Provision, reboot/reset/terminate, proxy changes,
run/stop,uploadApp, and mutating wildcardcalls can require human approval depending on the user’s Account Kit — you don’t police it in app code. - A leaked agent key can’t spin up or drive a device. It can read status and list, but the device-provisioning and device-driving actions land in the Approvals queue.
- The meter is yours to stop. Phones are billed per device-minute from credits with no upstream
auto-stop;
terminatestops the meter (credit exhaustion and a max-runtime failsafe are the only backstops).
Minimal viable migration
The smallest swap that keeps a working device-backed agent alive is provision a device → put the app on it → drive it → terminate.Install the SDK / set your key
NAIVE_API_KEY (a server-side nv_sk_ key from the
dashboard). You can drop APPETIZE_API_KEY for this path —
Naive holds the upstream device-cloud key for you.Enable the mobile primitive for the user
Whether an agent may use
mobile at all is policy on its identity. Enable mobile in the
user’s Account Kit, and confirm your approval posture (which
sensitive operations require a human) — this is the deliberate change from Appetize, where the
org token needs no permission to act.Swap the provision + upload calls
Replace
getClient(...) + client.startSession(...) with naive.mobile.provision({ country })
then naive.mobile.waitReady(device_id). Replace POST /v1/apps with
naive.mobile.uploadApp({ name, fileName }). Keep the returned device_id in place of
Appetize’s publicKey — you now address a device, not a build.Swap the control path
Map your scripted
session.tap / type / screenshot sequence to a single
naive.mobile.run({ task, deviceId }) natural-language task (then read task, screenshots,
trajectory). If you need a specific raw device tool, reach it with the wildcard
naive.mobile.call("take-screenshot", { path: { deviceId } }). To display the device, read
naive.mobile.stream(device_id) (stream_url, stream_token, console_url) instead of the
Appetize iframe embed.Always terminate
Replace
session.end() with naive.mobile.terminate(device_id). This stops the per-minute
meter — there is no upstream auto-stop, so terminate every device you finish with.Consolidate further once you’re on Naive
This is where the migration pays for itself. On Appetize, the org token scopes devices in one Appetize account and nothing else. On Naive, the unit of isolation is a tenant user, and the device is one of many primitives that identity owns.Gain #1 — one identity across primitives
- With Appetize, an agent’s devices are a slice of one account, and per-customer separation is a
naming convention you maintain yourself. With Naive,
naive.forUser(acme.id)is a single handle to the identity whose Account Kit governs mobile and cards and email and phone and vault. - Each tenant only ever sees the devices it created through Naive, and each tenant’s credits fund only its own phones — isolation is the platform’s job, not yours.
Gain #2 — execution-time governance
- Whether an agent may use
mobileat all is policy on the identity — toggled per user in the Account Kit — and device-minutes are metered against the tenant’s plan quota. This is on top of the per-operation approval gate that Appetize has no equivalent for.
- The agent’s device code path stays the same for every tier. What changes at execution time:
- Disabled primitive → the call is refused. If the Account Kit doesn’t grant
mobile, the provision never runs. - Sensitive operation → approval-gated. Provision, lifecycle,
run,uploadApp, and mutatingcalls can returnpending_approvaluntil a human approves them. - Quota exceeded → capped. Once device-minutes cross the plan’s
mobilequota for the period, further provisioning is rejected — no surprise bill from a device left running. - No key in the agent → nothing to leak. Naive holds the Mobilerun operator key; the agent
only ever holds its scoped
nv_sk_key.
- Disabled primitive → the call is refused. If the Account Kit doesn’t grant
Gain #3 — unified accountability
- Every provision, task, upload, and terminate for a customer lands in one per-user activity log — alongside their cards, email, phone, and vault events, not in a separate vendor dashboard:
- That is the question that is hard to answer when devices live in Appetize, cards live in Stripe, the phone number lives in a carrier console, and email lives somewhere else. Under Naive it is a single query.
What does not map yet
A migration guide that hides gaps is worse than none. The core path (provision a device → put an app on it → stream it → drive it → terminate) maps cleanly, and Naive adds the approval gate, per-tenant scoping, and a natural-language agent driver. But Appetize is a mature app-testing and in-browser device platform, and the following capabilities have no direct equivalent on Naive’smobile primitive today. Check this list against your app before you commit.
| Appetize.io feature | Status on Naive | Workaround |
|---|---|---|
@appetize/playwright test framework (toHaveElement, toMatchSnapshot, snapshot tests) | Not provided | Drive with naive.mobile.run + read trajectory/screenshots and assert in your own harness |
Typed per-action session control (session.tap by element selector / accessibilityIdentifier, type, swipe, playAction, waitForElement) | Partial | Prefer NL run; reach raw device tools (tap/swipe/screenshot) via the wildcard call |
Arbitrary device + OS matrix (device: "pixel7", osVersion: "13.0", iOS simulators) | Partial | Provision dedicated_premium_device with country (type / profile_id where available); no free device/OS picker today |
In-browser iframe embed of the device in your own page (appetize.io/embed/...) | Partial | Naive returns stream_url / stream_token / console_url; build your display around those |
Session queue / concurrency events (queue, queueEnd while waiting for a free slot) | Not provided | Devices are provisioned per tenant on demand and metered per minute — no shared session pool |
| Network intercept / debug logs / adb passthrough in-session | Partial | Some device tools are reachable via the wildcard call; verify the specific Mobilerun operation |
| Enterprise private / self-hosted Appetize instances (custom domain) | Not applicable | Naive runs the managed Mobilerun operator surface |
Where to go next
mobileprimitive — provision, stream, run agent tasks, apps, wildcardsearch/call, billing- Mobile API reference — every REST endpoint, including the approval-gated ones
naive.mobile.*SDK sub-client — the typed client used above- Approvals — the human gate that provision / run / upload flow through
- Compute — the same operator-key model applied to sandboxed code execution
- Account Kits — the policy model that enables/disables
mobileper user - Customer billing — the plan + quota that meters device-minutes at execution time
- Tenant users — the identity that owns the device, cards, email, phone, and vault