@clawhub-pelae1-92727a152e
Give your OpenClaw agent hands on a real Android phone. Tap, swipe, type, take screenshots, read the UI accessibility tree, and manage apps — all through the...
---
name: mobilerun-official
description: >
Give your OpenClaw agent hands on a real Android phone. Tap, swipe, type, take
screenshots, read the UI accessibility tree, and manage apps — all through the
official Mobilerun API (mobilerun.ai). Your agent can automate any Android app:
social media, testing, data collection, or anything you'd do manually. Uses YOUR
API key (stored securely via MOBILERUN_API_KEY env var) and YOUR device (personal
phone via Portal APK or cloud device). No data leaves your control.
metadata: { "openclaw": { "emoji": "📱", "primaryEnv": "MOBILERUN_API_KEY" } }
tags:
- mobile
- android
- automation
- ai-agent
- phone-control
- social-media
- app-testing
category: Device Control / Automation
---
# Mobilerun
Control real Android phones through an API -- tap, swipe, type, take screenshots, read the UI tree, manage apps, and more.
## Before You Start
Do NOT ask the user for an API key or to set up a device before checking. Always probe first:
1. **Resolve the API key:**
- The key is provided via the `MOBILERUN_API_KEY` environment variable (set by OpenClaw during skill loading)
- If the key is not available, ask the user to set it in their OpenClaw config or provide it when prompted
2. **Test the API key and check for devices in one call:**
Call `GET /devices` with the user's key to check device availability:
- `200` with a device in `state: "ready"` = **good to go, skip all setup, just do what the user asked**
- `200` but no devices or all `state: "disconnected"` = device issue (see step 3)
- `401` = key is bad, expired, or revoked -- ask the user to check their dashboard
3. **Only if no ready device:** tell the user the device status and suggest a fix:
- No devices at all = user hasn't connected a phone yet, guide them to Portal APK (see [setup.md](./setup.md))
- Device with `state: "disconnected"` = Portal app lost connection, ask user to reopen it
4. **Confirm device is responsive** (optional, only if first action fails):
Call `GET /devices/{deviceId}/screenshot` — if this returns a PNG image, the device is working.
**Key principle:** If the API key is set and a device is ready, go straight to executing the user's request. Don't walk them through setup they've already completed.
## Quick Reference
| Goal | Endpoint |
|------|----------|
| See the screen | `GET /devices/{id}/screenshot` |
| Read UI elements | `GET /devices/{id}/ui-state?filter=true` |
| Tap | `POST /devices/{id}/tap` -- `{x, y}` |
| Swipe | `POST /devices/{id}/swipe` -- `{startX, startY, endX, endY, duration}` |
| Type text | `POST /devices/{id}/keyboard` -- `{text, clear}` |
| Press key | `PUT /devices/{id}/keyboard` -- `{key}` (Android keycode) |
| Go back | `POST /devices/{id}/global` -- `{action: 1}` |
| Go home | `POST /devices/{id}/global` -- `{action: 2}` |
| Open app | `PUT /devices/{id}/apps/{packageName}` |
| List apps | `GET /devices/{id}/apps` |
All endpoints use base URL `https://api.mobilerun.ai/v1` with the user's API key in the Authorization header (see [setup.md](./setup.md) for auth details).
## Detailed Documentation
- **[setup.md](./setup.md)** -- Authentication, API key setup, device connectivity, troubleshooting
- **[phone-api.md](./phone-api.md)** -- Phone control API: screenshot, UI state, tap, swipe, type, app management
- **[subscription.md](./subscription.md)** -- Plans, pricing, credits, device types, and when to recommend upgrades
## Common Patterns
**Observe-Act Loop:**
Most phone control tasks follow this cycle:
1. Take a screenshot and/or read the UI state
2. Decide what action to perform
3. Execute the action (tap, type, swipe, etc.)
4. Observe again to verify the result
5. Repeat
**Finding tap coordinates:**
Use `GET /devices/{id}/ui-state?filter=true` to get the accessibility tree with element bounds, then calculate the center of the target element to get tap coordinates.
**Typing into a field:**
1. Check `phone_state.isEditable` -- if false, tap the input field first
2. Optionally clear existing text with `clear: true`
3. Send the text via `POST /devices/{id}/keyboard`
## Error Handling
| Error | Likely cause | What to do |
|-------|-------------|------------|
| `401` | Invalid or expired API key | Ask user to verify key in their Mobilerun dashboard |
| Empty device list | No device connected | Guide user to connect via Portal APK (see setup.md) |
| Device `disconnected` | Portal app closed or phone lost network | Ask user to check phone and reopen Portal |
| Billing/plan error on `POST /devices` | Free plan, cloud devices need subscription | Tell user to check plans in their dashboard (see [subscription.md](./subscription.md)) |
| Action returns error on valid device | Device may be busy, locked, or unresponsive | Try taking a screenshot first to check state |
| `403` with "limit reached" | Plan limit hit (e.g. max concurrent devices) | User needs to terminate a device or upgrade (see [subscription.md](./subscription.md)) |
FILE:_meta.json
{
"ownerId": "kn740pdc7mh9s1d3x1pkgc6h2h81cpyt",
"slug": "mobilerun-official",
"version": "1.1.0"
}
FILE:api.md
# Mobilerun Platform API
Base URL: `https://api.mobilerun.ai/v1`
Auth: Use the `MOBILERUN_API_KEY` environment variable in the Authorization header.
This document covers platform-level APIs: device provisioning, AI agent tasks, webhooks, and the app library.
For direct phone control (tap, swipe, screenshot, etc.), see [phone-api.md](./phone-api.md).
---
## Device Management
### List Devices
```
GET /devices
```
Query params:
- `state` -- filter by state (array, e.g. `state=ready&state=assigned`)
- Values: `creating`, `assigned`, `ready`, `disconnected`, `terminated`, `unknown`
- `provider` -- `personal`, `limrun`, `remote`, `roidrun`
- `type` -- `device_slot`, `dedicated_emulated_device`, `dedicated_physical_device`
- `page` (default: 1), `pageSize` (default: 20)
- `orderBy` -- `id`, `createdAt`, `updatedAt`, `assignedAt` (default: `createdAt`)
- `orderByDirection` -- `asc`, `desc` (default: `desc`)
Response: `{ items: DeviceInfo[], pagination: Meta }`
### Get Device Info
```
GET /devices/{deviceId}
```
Returns a `DeviceInfo` object:
```json
{
"id": "uuid",
"name": "string",
"state": "ready",
"stateMessage": "device ready",
"streamUrl": "wss://...",
"streamToken": "string",
"deviceType": "device_slot",
"provider": "limrun",
"apps": ["com.example.app"],
"files": [],
"country": "US",
"createdAt": "ISO datetime",
"updatedAt": "ISO datetime",
"assignedAt": "ISO datetime | null",
"terminatesAt": "ISO datetime | null",
"taskCount": 0
}
```
### Get Device Count
```
GET /devices/count
```
Returns a map of device states to counts.
### Provision a Cloud Device
Cloud devices require an active subscription. If the user's plan doesn't support it,
the API will return `403` with `"detail": "device_slot limit reached"` -- inform the user they need to terminate an existing device or upgrade at https://cloud.mobilerun.ai/billing.
See [subscription.md](./subscription.md) for plan details.
```
POST /devices
Content-Type: application/json
{
"name": "my-device",
"apps": ["com.example.app"],
"files": [],
"country": "US"
}
```
Query params:
- `provider` -- `limrun`, `physical`, `premium`, `roidrun`
- `deviceType` -- `device_slot`, `dedicated_emulated_device`, `dedicated_physical_device`, `dedicated_premium_device`
After provisioning, wait for it to become ready:
```
GET /devices/{deviceId}/wait
```
This blocks until the device state transitions to `ready`.
### Terminate a Cloud Device
```
DELETE /devices/{deviceId}
Content-Type: application/json
{}
```
Optional body fields:
- `terminateAt` -- ISO datetime for scheduled termination
- `previousDeviceId` -- for device replacement workflows
> Personal devices cannot be terminated via the API. They disconnect when the Portal app is closed.
### List Tasks for a Device
```
GET /devices/{deviceId}/tasks
```
Query params: `page`, `pageSize`, `orderBy`, `orderByDirection`
---
## Tasks (AI Agent)
Instead of controlling a phone step-by-step, you can submit a natural language goal
and let Mobilerun's AI agent execute it autonomously on the device.
Tasks require a paid subscription with credits.
If the user doesn't have an active plan, the API will return an error --
let the user know they need a subscription at https://cloud.mobilerun.ai/billing.
See [subscription.md](./subscription.md) for plan and credit details.
### Run a Task
```
POST /tasks
Content-Type: application/json
{
"task": "Open Chrome and search for weather",
"llmModel": "google/gemini-2.5-flash",
"deviceId": "uuid-of-device",
"apps": [],
"credentials": [],
"files": [],
"maxSteps": 100,
"reasoning": true,
"vision": false,
"stealth": false,
"temperature": 0.5,
"executionTimeout": 1000,
"displayId": 0,
"outputSchema": null,
"vpnCountry": "US"
}
```
**Required fields:**
- `task` -- natural language description of what to do (min 1 char)
- `llmModel` -- which model to use (see available models below)
**Optional fields:**
- `deviceId` -- UUID of the device to run on. If omitted, a device will be provisioned automatically (requires subscription)
- `apps` -- list of app package names to pre-install
- `credentials` -- list of `{ packageName, credentialNames[] }` for app logins
- `files` -- list of file identifiers to make available
- `maxSteps` -- max agent steps (default: 100)
- `reasoning` -- enable reasoning/thinking (default: true)
- `vision` -- enable vision/screenshot analysis (default: false)
- `stealth` -- enable stealth mode (default: false, requires Starter+ plan)
- `temperature` -- LLM temperature (default: 0.5)
- `executionTimeout` -- timeout in seconds (default: 1000)
- `displayId` -- device display ID (default: 0)
- `outputSchema` -- JSON schema for structured output (nullable)
- `vpnCountry` -- route through VPN in a specific country: `US`, `BR`, `FR`, `DE`, `IN`, `JP`, `KR`, `ZA`. Only use if the task specifically requires a certain region (e.g. geo-restricted content). VPN adds latency and can cause issues -- avoid unless needed.
Returns:
```json
{
"id": "uuid",
"streamUrl": "string",
"token": "string"
}
```
### Run a Streamed Task
```
POST /tasks/stream
```
Same request body as above. Returns a `text/event-stream` SSE stream of trajectory events in real-time.
### List Tasks
```
GET /tasks
```
Query params:
- `status` -- `created`, `running`, `paused`, `completed`, `failed`, `cancelled`
- `orderBy` -- `id`, `createdAt`, `finishedAt`, `status` (default: `createdAt`)
- `orderByDirection` -- `asc`, `desc` (default: `desc`)
- `query` -- search in task description (max 128 chars)
- `page` (default: 1), `pageSize` (default: 20, max: 100)
### Get Task
```
GET /tasks/{task_id}
```
### Get Task Status
```
GET /tasks/{task_id}/status
```
Returns `{ status: "created" | "running" | "paused" | "completed" | "failed" | "cancelled" }`.
### Cancel Task
```
POST /tasks/{task_id}/cancel
```
### Attach to Running Task
```
GET /tasks/{task_id}/attach
```
Returns an SSE `text/event-stream` of real-time trajectory events. Use this to follow along with a running task.
### Get Task Trajectory
```
GET /tasks/{task_id}/trajectory
```
Returns the full history of events from the task execution.
### Task Screenshots & UI States
```
GET /tasks/{task_id}/screenshots -- list all screenshot URLs
GET /tasks/{task_id}/screenshots/{index} -- get screenshot at index
GET /tasks/{task_id}/ui_states -- list all UI state URLs
GET /tasks/{task_id}/ui_states/{index} -- get UI state at index
```
### Available LLM Models
To get the current list of available models, call:
```
GET /models
```
This returns all models supported for task execution. Model availability may change — always query the endpoint for the latest list rather than hardcoding model names.
---
## App Library
### List Available Apps
```
GET /apps
```
Query params:
- `page` (default: 1), `pageSize` (default: 10)
- `source` -- `all`, `uploaded`, `store`, `queued` (default: `all`)
- `query` -- search by name
- `sortBy` -- `createdAt`, `name` (default: `createdAt`)
- `order` -- `asc`, `desc` (default: `desc`)
---
## Webhooks
Subscribe to task lifecycle events to get notified when tasks change state.
### Subscribe
```
POST /hooks/subscribe
Content-Type: application/json
{
"targetUrl": "https://your-server.com/webhook",
"events": ["completed", "failed"],
"service": "other"
}
```
Events: `created`, `running`, `completed`, `failed`, `cancelled`, `paused`
Services: `zapier`, `n8n`, `make`, `internal`, `other`
### List Hooks
```
GET /hooks
```
### Get Hook
```
GET /hooks/{hook_id}
```
### Edit Hook
```
POST /hooks/{hook_id}/edit
Content-Type: application/json
{ "events": ["completed"], "state": "active" }
```
### Unsubscribe
```
POST /hooks/{hook_id}/unsubscribe
```
FILE:phone-api.md
# Phone Control API Reference
Base URL: `https://api.mobilerun.ai/v1`
Auth: Use the `MOBILERUN_API_KEY` environment variable in the Authorization header.
This document covers how to control an Android device connected to Mobilerun.
The primary use case is controlling the user's own personal device connected via the Droidrun Portal APK.
For platform-level operations (provisioning cloud devices, AI agent tasks, webhooks), see [api.md](./api.md).
---
## Finding & Checking the Device
### List Devices
```
GET /devices
```
Returns all devices for the user. Check the `state` and `provider` fields to understand what's available.
See [setup.md](./setup.md) for device connectivity troubleshooting and Portal APK setup.
### Device States
| State | Meaning |
|----------------|---------|
| `creating` | Device is being provisioned (cloud devices only) |
| `assigned` | Device is assigned but not yet ready |
| `ready` | Device is connected and accepting commands |
| `disconnected` | Connection lost -- Portal app may be closed or phone lost network |
| `terminated` | Device has been shut down (cloud devices only) |
| `unknown` | Unexpected state |
For personal devices, you'll typically see `ready` or `disconnected`.
If `disconnected`, ask the user to check that the Portal app is open and the phone has internet.
### Get Device Info
```
GET /devices/{deviceId}
```
Returns device details including `state`, `stateMessage`, `provider`, `deviceType`, and more.
### Get Device Time
```
GET /devices/{deviceId}/time
```
Returns the current time on the device as a string.
---
## Screen Observation
These are the primary tools for understanding what's on the device screen.
### Take Screenshot
```
GET /devices/{deviceId}/screenshot
```
Query param: `hideOverlay` (default: `false`)
Returns a **PNG image** as binary data. Use this to see what's currently displayed on screen.
### Get UI State (Accessibility Tree)
```
GET /devices/{deviceId}/ui-state
```
Query param: `filter` (default: `false`) -- set to `true` to filter out non-interactive elements.
Returns an `AndroidState` object with three sections:
#### phone_state
```json
{
"keyboardVisible": false,
"packageName": "app.lawnchair",
"currentApp": "Lawnchair",
"isEditable": false,
"focusedElement": {
"className": "string",
"resourceId": "string",
"text": "string"
}
}
```
- `currentApp` -- human-readable name of the foreground app
- `packageName` -- Android package name of the foreground app
- `keyboardVisible` -- whether the soft keyboard is showing
- `isEditable` -- whether the currently focused element accepts text input
- `focusedElement` -- details about the focused UI element (if any)
#### device_context
```json
{
"screen_bounds": { "width": 720, "height": 1616 },
"screenSize": { "width": 0, "height": 0 },
"display_metrics": {
"density": 1.75,
"densityDpi": 280,
"scaledDensity": 1.75,
"widthPixels": 720,
"heightPixels": 1616
},
"filtering_params": {
"min_element_size": 5,
"overlay_offset": 0
}
}
```
- `screen_bounds` -- the actual screen resolution in pixels. All tap/swipe coordinates use this coordinate space.
- `display_metrics` -- physical display properties (density, DPI)
- `filtering_params.min_element_size` -- minimum element size in pixels (used when `filter=true`)
#### a11y_tree (Accessibility Tree)
A recursive tree of UI elements. Each node has:
```json
{
"className": "android.widget.TextView",
"packageName": "app.lawnchair",
"resourceId": "app.lawnchair:id/search_container",
"text": "Search",
"contentDescription": "",
"boundsInScreen": { "left": 48, "top": 1420, "right": 671, "bottom": 1532 },
"isClickable": true,
"isLongClickable": false,
"isEditable": false,
"isScrollable": false,
"isEnabled": true,
"isVisibleToUser": true,
"isCheckable": false,
"isChecked": false,
"isFocusable": false,
"isFocused": false,
"isSelected": false,
"isPassword": false,
"hint": "",
"childCount": 0,
"children": []
}
```
**Key node fields:**
- `text` -- the visible text on the element
- `contentDescription` -- accessibility label (useful when `text` is empty, e.g. icon buttons)
- `resourceId` -- Android resource ID (e.g. `com.app:id/button_ok`) -- useful for identifying elements
- `boundsInScreen` -- pixel coordinates as `{left, top, right, bottom}`. To tap an element, calculate its center: `x = (left + right) / 2`, `y = (top + bottom) / 2`
- `isClickable` -- whether the element responds to taps
- `isEditable` -- whether the element is a text input field
- `isScrollable` -- whether the element supports scrolling (swipe gestures)
- `children` -- nested child elements (the tree is recursive)
**Example: reading a home screen**
```
FrameLayout (0,0,720,1616)
ScrollView (0,0,720,1616) [scrollable]
FrameLayout (14,113,706,326)
LinearLayout (42,128,706,310) [clickable]
TextView (42,156,706,198) "Tap to set up"
View (0,94,720,1574) "Home"
TextView (14,1222,187,1422) "Phone" [clickable]
TextView (187,1222,360,1422) "Contacts" [clickable]
TextView (360,1222,533,1422) "Files" [clickable]
TextView (533,1222,706,1422) "Chrome" [clickable]
FrameLayout (48,1420,671,1532) "Search" [clickable]
```
To tap "Chrome": bounds are (533,1222,706,1422), so tap at x=(533+706)/2=619, y=(1222+1422)/2=1322.
Use `filter=true` for a cleaner tree focused on actionable elements (filters out non-interactive containers).
---
## Device Actions
All action endpoints take a `deviceId` path parameter.
Optional header: `X-Device-Display-ID` (integer, default: `0`) -- for multi-display devices.
### Tap
```
POST /devices/{deviceId}/tap
Content-Type: application/json
{ "x": 540, "y": 960 }
```
Taps at pixel coordinates. Use the `screen_bounds` from UI state and element bounds from the a11y tree to calculate where to tap.
### Swipe
```
POST /devices/{deviceId}/swipe
Content-Type: application/json
{
"startX": 540,
"startY": 1200,
"endX": 540,
"endY": 400,
"duration": 300
}
```
`duration` is in milliseconds (minimum: 10). Common patterns:
- **Scroll down**: swipe from bottom to top (high startY -> low endY)
- **Scroll up**: swipe from top to bottom
- **Swipe left/right**: adjust X coordinates, keep Y similar
### Global Actions
```
POST /devices/{deviceId}/global
Content-Type: application/json
{ "action": 2 }
```
| Action code | Button |
|-------------|---------|
| `1` | BACK |
| `2` | HOME |
| `3` | RECENT |
### Type Text
```
POST /devices/{deviceId}/keyboard
Content-Type: application/json
{ "text": "Hello world", "clear": false }
```
Types text into the currently focused input field.
- `clear: true` -- clears the field before typing
- Make sure an input field is focused first (check `phone_state.isEditable`)
- If the keyboard isn't visible, you may need to tap on an input field first
### Press Key
```
PUT /devices/{deviceId}/keyboard
Content-Type: application/json
{ "key": 66 }
```
Sends an Android keycode. Only text-input-related keycodes are supported -- system/hardware keys (volume, power, etc.) will fail.
Working keycodes:
| Keycode | Key |
|---------|-----|
| `4` | BACK |
| `61` | TAB |
| `66` | ENTER |
| `67` | DEL (backspace) |
| `112` | FORWARD_DEL (delete) |
For system navigation (home, back, recent), use `POST /devices/{id}/global` instead.
### Clear Input
```
DELETE /devices/{deviceId}/keyboard
```
Clears the currently focused input field.
---
## App Management (On-Device)
### List Installed Apps
```
GET /devices/{deviceId}/apps
```
Query param: `includeSystemApps` (default: `false`)
Returns an array of `AppInfo`:
```json
{
"packageName": "com.example.app",
"label": "Example App",
"versionName": "1.2.3",
"versionCode": 123,
"isSystemApp": false
}
```
### List Package Names
```
GET /devices/{deviceId}/packages
```
Query param: `includeSystemPackages` (default: `false`)
Returns a string array of package names. Lighter than the full app list.
### Install App
```
POST /devices/{deviceId}/apps
Content-Type: application/json
{ "packageName": "com.example.app" }
```
Installs an app from the Mobilerun app library (not the Play Store directly).
Takes a couple of minutes and there's no status endpoint -- you'd have to poll `GET /devices/{id}/apps` to confirm.
**Prefer manually installing via Play Store instead.** Open the Play Store app on the device, search for the app, and tap install -- this is faster and more reliable. Only use this API endpoint if the user explicitly asks for it.
> On personal devices, this endpoint may fail because Android blocks app installations from unknown sources by default. The user would need to explicitly enable "Install unknown apps" for the Portal APK in their device settings. Another reason to prefer the Play Store approach.
### Start App
```
PUT /devices/{deviceId}/apps/{packageName}
Content-Type: application/json
{}
```
Optional body: `{ "activity": "com.example.app.MainActivity" }` -- to launch a specific activity.
Usually omitting activity is fine; it launches the default/main activity.
### Stop App
```
PATCH /devices/{deviceId}/apps/{packageName}
Content-Type: application/json
{}
```
### Uninstall App
```
DELETE /devices/{deviceId}/apps/{packageName}
Content-Type: application/json
{}
```
---
## Typical Workflow
1. **Find the device**: `GET /devices`
- Look for a device with `state: "ready"`
- If a device shows `disconnected`: ask user to reopen Portal app
- If no devices at all: user needs to connect a device (see [setup.md](./setup.md))
2. **Observe**:
- `GET /devices/{id}/screenshot` -- see the screen
- `GET /devices/{id}/ui-state?filter=true` -- get actionable UI elements
3. **Act**:
- Tap: `POST /devices/{id}/tap` with `{x, y}`
- Type: `POST /devices/{id}/keyboard` with `{text, clear}`
- Swipe: `POST /devices/{id}/swipe` with coordinates + duration
- Navigate: `POST /devices/{id}/global` -- back (1), home (2), recent (3)
- Open app: `PUT /devices/{id}/apps/{packageName}`
4. **Repeat** observe + act until the goal is achieved.
---
## Error Handling
All API errors follow this format:
```json
{
"title": "Unauthorized",
"status": 401,
"detail": "Invalid API key.",
"errors": []
}
```
| Status | Meaning | What to do |
|--------|---------|------------|
| `401` | Invalid, expired, or revoked API key | Ask user to check/recreate key at https://cloud.mobilerun.ai/api-keys |
| `404` / `500` | Device not found or invalid device ID | Verify the device ID is correct, re-list devices |
| Action fails on valid device | Device may be locked, busy, or unresponsive | Take a screenshot to check device state, try again |
FILE:setup.md
# Mobilerun Setup
This document describes how Mobilerun authentication and device connectivity works,
so you can diagnose issues and guide the user through setup when needed.
---
## Authentication
### How It Works
Mobilerun uses API keys for programmatic access. All API calls go to `https://api.mobilerun.ai/v1`
with the user's key in the Authorization header.
- Keys are prefixed with `dr_sk_` -- if a user provides something without this prefix, it's not a valid Mobilerun key
- Keys are created from the Mobilerun dashboard at cloud.mobilerun.ai and can be revoked or expired
- Each key is tied to a single user account
- The key is stored securely via the `MOBILERUN_API_KEY` environment variable — never hardcode or expose it
### Getting an API Key
The user needs to:
1. Go to **https://cloud.mobilerun.ai/api-keys**
- If not logged in, the page redirects to login first (Google, GitHub, or Discord -- no email/password option)
- After login it redirects back to the API keys page
2. Click the **"New Key"** button
3. Give the key a name (anything descriptive is fine)
4. Copy the full key -- it's only shown once at creation time
The key will look like: `dr_sk_a1b2c3d4e5f6...`
### Verifying the API Key
After receiving a key from the user, verify it works by calling `GET /devices` with the key.
| Response | Meaning |
|----------|---------|
| `200` with JSON body | Key is valid. The response shows the user's devices (may be empty if no devices connected yet) |
| `401 Unauthorized` | Key is invalid, expired, or revoked |
### Troubleshooting Auth Issues
**User provides a key that doesn't start with `dr_sk_`:**
- It's not a Mobilerun API key. Ask them to copy it again from https://cloud.mobilerun.ai/api-keys
**401 on a key that previously worked:**
- The key may have been revoked or expired. Ask the user to check the API keys page and create a new one if needed
**User says they can't find the API keys page:**
- Direct them to https://cloud.mobilerun.ai/api-keys -- they need to be logged in first
**User doesn't have an account:**
- They can create one by going to https://cloud.mobilerun.ai/sign-in and signing in with Google, GitHub, or Discord. First login automatically creates an account.
---
## Device Connectivity
### Personal Devices (Portal APK)
A personal device is the user's own Android phone connected to Mobilerun via the Droidrun Portal app.
#### Installing the Portal APK
1. On the Android device, go to **https://droidrun.ai/portal** -- this redirects to the latest GitHub release
2. Download the file named `droidrun-portal-vx.x.x.apk` (the version number varies)
3. Open/install the downloaded APK
- Android may warn about installing from unknown sources -- the user needs to allow it
#### Connecting to Mobilerun
Once the Portal app is installed and opened:
1. **Grant accessibility permission**: A red banner at the top says "Accessibility Service Not Enabled" -- tap **"Enable Now"** and follow the system prompts to enable it. This Android system permission is required so the agent can read on-screen element names and positions (the UI accessibility tree). The permission is scoped to the Portal app only and does not grant access to other apps' data. The user controls when the connection is active.
2. **Connect to Mobilerun** -- two options:
- **Tap** "Connect to Mobilerun" -> opens a login page where the user signs in with their account (Google, GitHub, or Discord)
- **Long-press** "Connect to Mobilerun" -> opens a field labeled "Token" -- despite the label, the user should paste their Mobilerun API key (`dr_sk_...`) here, then tap **Connect**
3. The app connects to Mobilerun and the device should appear in `GET /devices` with `state: "ready"`.
The agent should provide the API key to the user at step 2 -- the same key used for API calls.
#### Checking Device Status
Call `GET /devices` and look for a device with `provider: "personal"`:
| Result | Meaning |
|--------|---------|
| No personal device in list | Portal APK isn't installed, not connected, or accessibility permission not granted |
| Device with `state: "ready"` | Device is connected and ready to use |
| Device with `state: "disconnected"` | Portal app lost connection. User should check that the app is open and phone has internet |
#### Common Issues
- **Device shows `disconnected`**: Portal app was closed, phone went to sleep with aggressive battery optimization, or phone lost internet. Ask user to reopen the Portal app.
- **Device was `ready` but stops responding**: The phone may have locked or the Portal app was killed by the OS. Ask user to check the phone.
- **No device appears at all**: Portal APK isn't installed, accessibility permission wasn't granted, or the user didn't connect with their API key.
- **Connection fails in Portal app**: The API key may be wrong or expired. Ask the user to verify the key.
### Cloud Devices
Cloud devices are virtual/emulated devices hosted by Mobilerun. They require a paid subscription.
If a user tries to provision a cloud device without the right plan, the API will return an error.
In that case, let them know they need to upgrade at https://cloud.mobilerun.ai.
Cloud devices go through these states after provisioning:
`creating` -> `assigned` -> `ready`
Use `GET /devices/{deviceId}/wait` to block until the device is ready (avoids polling).
---
## Accounts & Plans
- **Free accounts** can connect personal devices via Portal APK and use the Tools API to control them
- **Paid plans** add cloud device provisioning, task execution (AI agent credits), and additional features
For full plan details (Hobby, Starter, Pro, Enterprise), see [subscription.md](./subscription.md).
If an API call fails with a billing/plan error, direct the user to https://cloud.mobilerun.ai/billing.
---
## Quick Checklist
When starting a session, verify:
1. **API key works** -- `GET /devices` returns 200
2. **Device is available** -- at least one device with `state: "ready"` in the response
3. **Device is responsive** -- `GET /devices/{id}/screenshot` returns a PNG image
If all three pass, you're ready to control the phone. See [phone-api.md](./phone-api.md) for the full API reference.
FILE:subscription.md
# Mobilerun Plans & Subscriptions
Plans page: https://cloud.mobilerun.ai/billing
## Plans Overview
| Plan | Monthly | Annual | Credits | Cloud Device | Extras |
|------|---------|--------|---------|-------------|--------|
| **Hobby** | $5/mo | $4/mo ($48/yr) | 500 | Device Slot (flexible) | -- |
| **Starter** | $30/mo | $24/mo ($288/yr) | 3,000 | Emulated Device + Device Slot (flexible) | Stealth Mode |
| **Pro** | $50/mo | $40/mo ($480/yr) | 5,000 | Physical Device + Device Slot (flexible) | Advanced Stealth Mode, Priority Support |
| **Enterprise** | Custom | Custom | Custom | Premium Stealth Farm | Custom Build & Ops, Dedicated Infra & SLA |
Annual billing saves 20%.
## What Each Plan Includes
### Hobby ($5/mo)
- 500 AI agent credits
- Device Slot (flexible) -- a shared cloud device slot
- Can connect personal devices via Portal APK
- Good for getting started and experimenting
### Starter ($30/mo) -- Most Popular
- 3,000 AI agent credits
- Emulated Device -- a dedicated emulated Android device
- Device Slot (flexible)
- Stealth Mode included
- Good for regular automation use
### Pro ($50/mo)
- 5,000 AI agent credits
- Physical Device -- a dedicated real physical Android device in the cloud
- Device Slot (flexible)
- Advanced Stealth Mode included
- Priority Support
- Good for production workloads and apps that detect emulators
### Enterprise (Custom)
- Premium Stealth Farm
- Custom Build & Ops
- Dedicated Infra & SLA
- Contact sales for pricing
## Credits
Credits are consumed when using cloud devices and running tasks via the Tasks API.
Direct device control via the Tools API (tap, swipe, screenshot, etc.) on a personal device does not consume credits.
**Credit consumption:**
- **1 credit per device minute** -- while a cloud device is running
- **~0.5 credits per agent step** -- when running a task via the Tasks API
## Device Types
| Type | Description | Available on |
|------|-------------|-------------|
| Device Slot (flexible) | Shared cloud device slot | Hobby, Starter, Pro |
| Emulated Device | Dedicated emulated Android | Starter, Pro |
| Physical Device | Dedicated real physical phone | Pro |
| Premium Stealth Farm | Enterprise-grade device farm | Enterprise |
## When to Recommend an Upgrade
- **User has no plan and wants cloud devices**: Any paid plan works, recommend Hobby to start
- **User needs more credits**: Suggest moving up a tier
- **User's app detects emulators**: They need Pro (physical device) or at minimum Starter (stealth mode)
- **User needs guaranteed uptime / SLA**: Enterprise
- **User hits a billing error on `POST /devices`**: Their plan doesn't support the device type they requested
Direct the user to https://cloud.mobilerun.ai/billing to view and manage their subscription.
**Free (no plan) users** can connect their own personal device via Portal APK and use the Tools API to control it with any agent (e.g. OpenClaw). No subscription needed for direct device control on your own phone.
#TODO: free plan is still in production -- update with final details when ready
### Known Plan Limit Errors
| Status | Detail | Meaning |
|--------|--------|---------|
| `403` | `device_slot limit reached` | User has hit their concurrent cloud device limit. They need to terminate an existing device or upgrade their plan. |
#TODO: document additional plan limit errors (credit exhaustion, task limits, etc.)