# DeviceFarm — complete API and automation guide

DeviceFarm rents real Android phones that run in a datacenter and stream to your
browser. This file documents everything you can do to them from code or from an
AI assistant. It is the whole reference: nothing else to download.

## If you are handing this file to an AI assistant

Paste it into the conversation and say what you want, for example:

> Here is the DeviceFarm documentation. My API key is df_live_xxx. List my
> phones, start the first one, install the automation library on it, then open
> Instagram and scroll the feed the way a person would. Stop the phone at the end.

Assistant, read these five rules before writing anything:

1. **Running phones cost money by the minute.** Stop every phone you start. When
   you run a script, leave `stop_when_done` at its default and poll until the
   response says `phone_stopped: true`.
2. **Never use `input tap`, `input swipe` or `input touchscreen`.** They
   generate no kernel input events, and any app on the device can read that as
   automation. Use the `h_*` helpers described at the end of this file.
3. **Every timing argument in that library is in milliseconds.** `h_sleep 2000
   4000` waits two to four seconds. `h_sleep 2 4` waits two milliseconds and is
   almost certainly a mistake.
4. **Call `prepare` once per boot** before running any script. Scripts open by
   sourcing `/data/local/tmp/humanize.sh`; without that call the first line fails.
5. **Deleting a phone through the API is permanent.** Ask the user first.

## Getting started in four steps

1. Create an account at https://devicefarm.io and subscribe to a plan (or use the
   free trial).
2. Add a proxy, or have proxy credentials ready — every phone needs its own IP.
3. Create an API key at https://devicefarm.io/dashboard/api-keys. It is shown
   once. Copy it somewhere safe.
4. Either call the API directly (below), or connect it to Claude in one config
   block (section "Using this from Claude").

## Authentication

Send the key as a bearer token on every request:

```bash
curl https://devicefarm.io/api/v1/phones \
  -H "Authorization: Bearer df_live_your_key_here"
```

Base URL: `https://devicefarm.io/api/v1`. Requests carrying a body are JSON.

## What a key can and cannot do

- It acts on **the account that created it, and nothing else**. A phone id
  belonging to someone else returns the same 404 as an id that never existed.
- It is bound by **your plan**. At your phone limit, creation returns
  `403 phone_limit_reached` — the API is not a way around the cap.
- It is bound by **your runtime minutes**. Starting a phone reserves the minutes
  it will need and sets an automatic shutdown deadline. With nothing funded, the
  start is refused with `409 no_budget`.
- It can be **revoked at any time** from the dashboard, with no effect on your
  other keys. Keys are stored hashed; nobody can read yours back, including us.

## Rate limit

60 requests per minute per account, across all your keys. Beyond that you get
`429 rate_limited`. The limit protects the shared capacity every customer draws
from, yours included.

## Response shape

Always the same envelope, so you branch on one field rather than on status codes:

```json
{ "data": { }, "error": null }
{ "data": null, "error": { "code": "phone_limit_reached", "message": "Phone limit reached (50/50)." } }
```

## Phone statuses

| Status | Meaning |
|---|---|
| `starting` | Booting. Poll until it turns running. |
| `running` | Up and billing by the minute. |
| `stopped` | Off. Costs nothing beyond the plan slot it occupies. |
| `error` | The last boot failed. The dashboard shows why. |

## Errors any endpoint can return

| Code | HTTP | Meaning |
|---|---|---|
| `missing_key` | 401 | No Authorization header, or it does not carry a df_live_ key. |
| `invalid_key` | 401 | The key is unknown or has been revoked. Unknown and revoked answer identically on purpose. |
| `rate_limited` | 429 | More than 60 requests in a minute on this account. Slow down and retry. |
| `rate_unavailable` | 503 | The rate limiter could not be reached, so the request was refused rather than let through unmetered. |
| `phone_not_found` | 404 | No phone with that id on your account. A phone belonging to someone else answers exactly the same. |

## Endpoints

- `GET /phones` — List your phones
- `POST /phones` — Create a phone
- `POST /phones/{id}/start` — Start a phone
- `POST /phones/{id}/stop` — Stop a phone
- `POST /phones/{id}/prepare` — Install the automation library
- `POST /phones/{id}/shell` — Run a command
- `POST /phones/{id}/script` — Run a script detached
- `GET /phones/{id}/script` — Poll a run
- `DELETE /phones/{id}` — Delete a phone

---

### List your phones

`GET /phones` → 200

Every phone on the account, newest first. Deleted phones are excluded.

**Query parameters**

| Field | Type | Required | Notes |
|---|---|---|---|
| `limit` | integer | no | How many to return. Default 50, maximum 200. |

**Request**

```bash
curl https://devicefarm.io/api/v1/phones \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
```

**Response**

```json
{
  "data": [
    {
      "id": "3f2b9c1e-5a44-4d0b-9c1a-7e8f2d3b4a55",
      "name": "US warmup 01",
      "status": "stopped",
      "android": "Android 14",
      "brand": "Samsung",
      "model": "Galaxy A55",
      "region": "United States-US",
      "created_at": "2026-08-11T09:14:22.481Z"
    }
  ],
  "error": null
}
```

---

### Create a phone

`POST /phones` → 201

Provisions a device and attaches a proxy to it. Counts against your plan the moment it succeeds. A proxy is mandatory — a phone without its own IP is a phone that shares an identity with every other one.

**Body**

| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | yes | Your label for the device, up to 100 characters. |
| `deviceBrand` | string | yes | e.g. "Samsung". Left to us, the device would advertise a chipset it does not render with. |
| `deviceModel` | string | yes | e.g. "Galaxy A55". Only pairs verified against the real GPU are accepted — a rejected pair comes back with the full accepted list for that Android version. |
| `androidVersion` | string | no | Android 10 through Android 16. Defaults to Android 14. |
| `proxy` | object | yes | {"mode":"custom","type":"socks5"\|"http"\|"https","host":"…","port":1080,"username":"…","password":"…"} or {"mode":"saved","proxyId":"<uuid from the dashboard>"}. |
| `datacenter` | string | no | auto, sgp, us or cn. Where the device physically runs — not its internet IP, which comes from the proxy. us hosts Android 15 only. |
| `region` | string | no | Mobile region shown by the device, e.g. "United States-US". |
| `language` | string | no | Full entry from the language list, e.g. "English (United States)_en-us". |
| `netType` | string | no | wifi or mobile — the connection type the device reports. Not supported on Android 14. |
| `tags` | string[] | no | Up to 20 tags for your own filtering. |
| `group` | string | no | Group name used to organise the fleet. |
| `note` | string | no | Free text, up to 1500 characters. |

**Request**

```bash
curl -X POST https://devicefarm.io/api/v1/phones \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "US warmup 01",
    "deviceBrand": "Samsung",
    "deviceModel": "Galaxy A55",
    "androidVersion": "Android 14",
    "proxy": {
      "mode": "custom",
      "type": "socks5",
      "host": "gate.example-proxies.net",
      "port": 1080,
      "username": "user-session-01",
      "password": "••••••"
    }
  }'
```

**Response**

```json
{
  "data": {
    "id": "3f2b9c1e-5a44-4d0b-9c1a-7e8f2d3b4a55",
    "name": "US warmup 01",
    "status": "stopped"
  },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `invalid_body` | 400 | A field is missing or invalid. The message names the field and, for a rejected device, lists every accepted brand/model pair. |
| `phone_limit_reached` | 403 | You already hold as many phones as your plan allows. The message shows usage and limit. |
| `create_failed` | 502 | Provisioning failed. Nothing is charged and no phone is left behind. |

---

### Start a phone

`POST /phones/{id}/start` → 202

Boots the device and reserves the runtime minutes it will need. Returns 202 immediately — booting takes tens of seconds, so poll the list until status is running. Runtime bills by the minute from this call until the phone stops.

**Request**

```bash
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/start \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
```

**Response**

```json
{
  "data": { "id": "3f2b9c1e-…", "status": "starting" },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `phone_not_found` | 404 | No phone with that id on your account. |
| `no_budget` | 409 | Not enough runtime minutes for this boot. Either the balance is empty, or what is left cannot fund another phone alongside the ones already running — stop one, or buy a time add-on. The message says which. |
| `trial_exhausted` | 409 | Free trial minutes used up. |
| `trial_max_running` | 409 | Trial accounts run one phone at a time. |
| `upstream_busy` | 503 | Transient capacity limit. Retry in a few seconds. |
| `start_failed` | 502 | The device did not boot. Nothing is billed; retry. |

---

### Stop a phone

`POST /phones/{id}/stop` → 202

Shuts the device down and settles the minutes used. Safe to call on a phone that is already stopped. Call it as soon as your work is done — an idle running phone bills exactly like a busy one.

**Request**

```bash
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/stop \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
```

**Response**

```json
{
  "data": { "id": "3f2b9c1e-…", "status": "stopped" },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `upstream_busy` | 503 | Transient capacity limit. Retry in a few seconds. |
| `stop_failed` | 502 | The device did not stop. Retry; if it persists the idle reaper will stop it. |

---

### Install the automation library

`POST /phones/{id}/prepare` → 200

Writes the human-input library to /data/local/tmp/humanize.sh on a running phone. Every script below opens by sourcing that file, so call this once per boot before running anything. Idempotent. Pass the packages you are about to automate and the accessibility flag is masked for them.

**Body**

| Field | Type | Required | Notes |
|---|---|---|---|
| `packages` | string[] | no | Up to 20 package names, e.g. ["com.instagram.android"]. Masking is not available on Android 16 — the response still echoes the list, so read it as "requested", not "confirmed", on that version. |

**Request**

```bash
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/prepare \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"packages": ["com.instagram.android"]}'
```

**Response**

```json
{
  "data": {
    "id": "3f2b9c1e-…",
    "library": "/data/local/tmp/humanize.sh",
    "bytes": 9214,
    "accessibility_hidden": ["com.instagram.android"]
  },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `invalid_body` | 400 | Expected {"packages": ["com.example"]} or an empty body. |
| `phone_not_running` | 409 | Start the phone first. |
| `upstream_busy` | 503 | Transient capacity limit. Retry in a few seconds. |
| `prepare_failed` | 502 | The library could not be installed on the device. |

---

### Run a command

`POST /phones/{id}/shell` → 200

Runs one command on the device as the shell user. No ADB, no credentials to store. A command that exits non-zero is a result, not an API error — you get its output either way. Times out after about ten seconds; use a script for anything longer.

**Body**

| Field | Type | Required | Notes |
|---|---|---|---|
| `cmd` | string | yes | The command, up to 8192 characters. |

**Request**

```bash
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/shell \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cmd": ". /data/local/tmp/humanize.sh && h_tap 540 1200"}'
```

**Response**

```json
{
  "data": { "ok": true, "output": "" },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `invalid_body` | 400 | Expected {"cmd": "<command>"}. |
| `phone_not_running` | 409 | Start the phone first. |
| `upstream_busy` | 503 | Transient capacity limit. Retry in a few seconds. |
| `device_unreachable` | 502 | The device did not respond. |

---

### Run a script detached

`POST /phones/{id}/script` → 202

Uploads a full flow and runs it in the background. Prefer this over a long chain of shell calls: one request instead of one per gesture, no ten-second ceiling, and the pacing comes from the script rather than from network latency.

**Body**

| Field | Type | Required | Notes |
|---|---|---|---|
| `script` | string | yes | The whole shell script, up to 64 KB. Open it with `. /data/local/tmp/humanize.sh`. Every timing argument in that library is in MILLISECONDS. |
| `stop_when_done` | boolean | no | Default true — the phone is shut down on the poll that observes the run finishing. Keep polling until it reports phone_stopped, otherwise the device stays up and billing until the idle sweep catches it. |

**Request**

```bash
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/script \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "script": ". /data/local/tmp/humanize.sh\nam start -n com.android.settings/.Settings\nh_sleep 2000 4000\nh_swipe 540 1600 540 700\n",
    "stop_when_done": true
  }'
```

**Response**

```json
{
  "data": {
    "phone_id": "3f2b9c1e-…",
    "run_id": "a41f9c2b7e08",
    "status": "running"
  },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `invalid_body` | 400 | Missing script, or larger than 64 KB. |
| `phone_not_running` | 409 | Start the phone first. |
| `script_failed` | 502 | The script could not be launched on the device. |

---

### Poll a run

`GET /phones/{id}/script` → 200

Progress and accumulated output. `running: false` with an exit_code means it finished. Keep polling until it does: the poll that observes the end is what shuts the phone down, so stopping early leaves it billing.

**Query parameters**

| Field | Type | Required | Notes |
|---|---|---|---|
| `run_id` | string | yes | The run_id returned when the script was launched. |

**Request**

```bash
curl "https://devicefarm.io/api/v1/phones/$PHONE_ID/script?run_id=$RUN_ID" \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
```

**Response**

```json
{
  "data": {
    "phone_id": "3f2b9c1e-…",
    "run_id": "a41f9c2b7e08",
    "running": false,
    "exit_code": 0,
    "output": "Starting: Intent { cmp=com.android.settings/.Settings }",
    "phone_stopped": true
  },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `invalid_run_id` | 400 | The run_id is missing or malformed. |
| `status_failed` | 502 | The run could not be read. |

---

### Delete a phone

`DELETE /phones/{id}` → 200

Releases the device and frees the plan slot immediately. Permanent — there is no undo, exactly like the delete button in the dashboard. A running phone is stopped and its session billed first, and the accounts you created on it are kept in your dashboard.

**Request**

```bash
curl -X DELETE https://devicefarm.io/api/v1/phones/$PHONE_ID \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
```

**Response**

```json
{
  "data": { "id": "3f2b9c1e-…", "deleted": true },
  "error": null
}
```

**Errors**

| Code | HTTP | Meaning |
|---|---|---|
| `delete_failed` | 502 | The phone could not be released. Nothing changed — retry. |


---

## A complete run, end to end

```bash
export KEY="df_live_your_key_here"
API="https://devicefarm.io/api/v1"

# 1. Create a phone with your proxy
PHONE=$(curl -s -X POST "$API/phones" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"worker-01","deviceBrand":"Samsung","deviceModel":"Galaxy A55",
       "proxy":{"mode":"custom","type":"socks5","host":"gate.example.net",
                "port":1080,"username":"u","password":"p"}}' \
  | jq -r '.data.id')

# 2. Boot it, and wait until it is really up
curl -s -X POST "$API/phones/$PHONE/start" -H "Authorization: Bearer $KEY"
until [ "$(curl -s "$API/phones" -H "Authorization: Bearer $KEY" \
        | jq -r --arg p "$PHONE" '.data[] | select(.id==$p) | .status')" = "running" ]; do
  sleep 5
done

# 3. Install the human-input library (once per boot)
curl -s -X POST "$API/phones/$PHONE/prepare" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"packages":[]}'

# 4. Run a flow detached
RUN=$(curl -s -X POST "$API/phones/$PHONE/script" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"script":". /data/local/tmp/humanize.sh\nam start -n com.android.settings/.Settings\nh_sleep 2000 4000\nh_swipe 540 1600 540 700\n"}' \
  | jq -r '.data.run_id')

# 5. Poll until it finishes — this is also what shuts the phone down
while true; do
  OUT=$(curl -s "$API/phones/$PHONE/script?run_id=$RUN" -H "Authorization: Bearer $KEY")
  [ "$(echo "$OUT" | jq -r '.data.running')" = "false" ] && break
  sleep 5
done
echo "$OUT" | jq '.data'
```

## Using this from Claude

Instead of writing the calls yourself, connect DeviceFarm to Claude once and ask
in plain language. This uses MCP, the standard that lets an assistant call real
tools. It adds no privileges: every call still carries your key and hits the same
checks described above.

**Claude desktop app** — Settings → Developer → Edit config, paste this, replace
the key, restart Claude:

```json
{
  "mcpServers": {
    "devicefarm": {
      "command": "npx",
      "args": [
        "-y",
        "--package=https://devicefarm.io/mcp/devicefarm-mcp-0.1.4.tgz",
        "devicefarm-mcp"
      ],
      "env": { "DEVICEFARM_API_KEY": "df_live_…" }
    }
  }
}
```

**Claude Code** — one command:

```bash
claude mcp add devicefarm \
  --env DEVICEFARM_API_KEY=df_live_… \
  -- npx -y --package=https://devicefarm.io/mcp/devicefarm-mcp-0.1.4.tgz devicefarm-mcp
```

Node 18 or newer is the only requirement. Once connected, Claude has these tools:

| Tool | What it does |
|---|---|
| `list_phones` | Your phones, with their id and current status. |
| `create_phone` | Provision a device with your proxy. Brand and model are required — pass proxy credentials inline, or the id of a proxy saved in the dashboard. |
| `start_phone` | Boot a device. Returns immediately; Claude polls until it is running. |
| `stop_phone` | Shut it down and stop the per-minute billing. Safe to call twice. |
| `prepare_phone` | Install the human-input library on a running phone, and mask the accessibility flag for the apps you are about to automate. |
| `shell` | Run one command on the device. No ADB, nothing to store. |
| `run_script` | Run a whole flow detached — no ten-second ceiling, and the phone shuts down by itself when the flow ends. |
| `script_status` | Poll a run's progress and output until it finishes. |
| `delete_phone` | Release a phone for good and free its plan slot. Permanent, with no undo — Claude asks before using it. |

---

# Writing automation for a DeviceFarm phone

Paste this file into your AI assistant (Claude Code, Cursor, ChatGPT) at the
start of the conversation, before asking it to write an automation script. It
tells the assistant which primitives to use and which to never use.

---

## The phone you are automating

A DeviceFarm phone is a real Android device reachable two ways:

- **the shell API** — `POST /api/v1/phones/{id}/shell` with `{cmd}`, no ADB
  needed, works while ADB is switched off
- **ADB** — enable it in the dashboard, then `adb connect <ip>:<port>`

Both run commands as the `shell` user on the device. Everything below applies
identically to either transport.

## The one rule that matters

**Never use `input tap`, `input swipe` or `input touchscreen` for gestures.**

Measured on a DeviceFarm phone (Android 14, 2026-08-14): a tap issued with
`input` produced **zero** events on `/dev/input`, while the same gesture issued
with `sendevent` produced a full event stream on the real touchscreen.

That difference is visible to any app, with no permission required:

| | `input tap` | `sendevent` (this kit) |
|---|---|---|
| Reaches the kernel input layer | no | yes |
| `MotionEvent.getDeviceId()` | `-1` (virtual) | the real touchscreen id |
| `getPressure()` | pinned to `1.0` | varies, like a finger |
| `getSize()` / contact area | constant | varies |
| Path of a swipe | perfectly straight | curved |
| Timing | machine-regular | irregular |

An app reading `getDeviceId() == -1` knows the touch was injected. That is a
one-line check, and it is the first thing an anti-fraud SDK looks at.

Use `humanize.sh` instead. It writes raw evdev events, so the framework sees
exactly what it sees from a physical finger.

## Setup at the top of every script

```sh
. /data/local/tmp/humanize.sh
```

Sourcing it probes the touchscreen, reads its axis ranges and the screen size,
and exports the `h_*` functions. Coordinates you pass are **screen pixels** —
the library converts them to the digitizer's own scale, which is usually
different (on the measured phone: 1080×2400 screen, 720×1080 digitizer).

## Available functions

| Function | What it does |
|---|---|
| `h_tap X Y` | Tap at screen pixel X,Y. Jitters ±6 px, holds 55–145 ms, rising then easing pressure, slight drift while held. |
| `h_swipe X1 Y1 X2 Y2 [ms]` | Swipe along a quadratic Bézier that bows sideways, with ease-in-out velocity, 26–46 steps, per-step pressure variation. |
| `h_scroll_down` / `h_scroll_up` | A swipe sized to the screen, with randomised start and end columns. |
| `h_sleep [minMs] [maxMs]` | Pause drawn from a right-skewed distribution (default 600–2400 ms) with an occasional long tail. |
| `h_type "text"` | Types character by character with 70–210 ms gaps and occasional longer pauses. |

## Finding what to tap — and the trap in it

Never hardcode coordinates read off a screenshot: the layout shifts between app
versions and locales. Read the view tree instead — but read it carefully.

```sh
F=$(h_dump)                                   # never `uiautomator dump` directly
grep -o 'text="Follow"[^>]*bounds="[^"]*"' "$F"
```

`bounds="[left,top][right,bottom]"` — tap the centre and let `h_tap` add its own
jitter. Do not compute a "random" point yourself; the library already does it
with the right distribution.

**The trap.** Measured on 2026-08-14: while `uiautomator dump` runs,
`Settings.Secure.accessibility_enabled` flips from `0` to `1` for about a
second, and `AccessibilityManager.isEnabled()` is readable by any app with no
permission at all. It returns to `0` afterwards.

That is the loudest signal this kit still produces, and it comes from
uiautomator — not from the gestures. Three ways to live with it, in order of
preference:

1. **Dump while the target app is not in the foreground.** Read the screen
   before you open it, or send it to the background first.
2. **Dump rarely.** Once per screen, not once per action. Cache the bounds.
3. **Mask it.** The platform exposes an accessibility-hiding capability for
   exactly this — it is what the built-in automations rely on. Make sure it is
   applied to the target package before automating.

`h_dump` also writes to `/data/local/tmp` and removes
`/sdcard/window_dump.xml`, which a bare `uiautomator dump` leaves sitting in
shared storage as a standing marker of automation.

## Typing

`h_type` uses `input text`, and that has **no hardware path**: these phones
expose only two input devices, the digitizer and the power key. There is no
keyboard, so the characters are injected into the framework and reach the app
with `KeyEvent.getDeviceId() == -1` — readable by the app.

Use `h_type` only for throwaway fields. For anything that matters — logins,
search boxes inside the app you care about — **tap the on-screen keyboard with
`h_tap`**. Find the key bounds once with `h_dump` while the keyboard is open,
cache them, and tap. It is slower, and it is what a person actually does.

## Rules for the generated script

1. Source `humanize.sh` and call only `h_*` for gestures.
2. Never emit `input tap` / `input swipe` / `sendevent` directly. If a gesture
   is missing from the library, add it to the library rather than inlining raw
   events in the script.
3. Never use a fixed `sleep`. Use `h_sleep`, and vary the range with the
   context: reading a post is slower than dismissing a dialog.
4. Re-read the screen with `uiautomator dump` after every navigation instead of
   assuming a delay was enough.
5. Do not loop an identical action sequence. Vary the order, skip steps
   occasionally, scroll past things without acting on them.
6. Sessions should end. A phone that acts for 14 hours straight is not a person.

## Measured behaviour

Everything below was measured on a DeviceFarm Android 14 phone on 2026-08-14,
by capturing `/dev/input/event1` while the library ran:

| | Measured |
|---|---|
| Tap duration | 473 ms |
| Swipe duration | 1301 ms |
| Distinct X values across vertical swipes | 26 — the path really is curved |
| Distinct pressure values | 12 |
| Distinct contact sizes | 13 |
| Contacts opened vs released | balanced — no stuck finger |

## The one limitation you should know about

Each event is one `sendevent` process, and a process costs roughly 37 ms on
these containers. That caps the sample rate: a swipe emits about 13 frames over
1.3 s, so roughly **10 samples per second**. A physical digitizer reports 60 to
240.

How much that matters is bounded: Android batches input to the display refresh
and most apps read only the latest sample, so the values an app sees are all
plausible. But an app that inspects `MotionEvent.getHistorySize()` would find
fewer intermediate samples than a real finger produces.

Going past this needs a small native helper that opens the input device once
and writes frames without spawning a process each time. Pure shell cannot do
it — `printf` truncates at the first NUL byte, so the event structs cannot be
written directly from the shell.

If your target is a banking-grade app, assume this ceiling is visible. For the
social platforms this kit is aimed at, the gesture *shape* — curvature,
pressure variation, timing spread — is what gets read, and that part is right.

## What this kit does NOT protect against

It makes the **gestures** look like a finger. It does nothing about
server-side behavioural analysis:

- posting and following cadence
- the shape of your follow graph
- session hours, session length, timezone coherence
- similarity between your own accounts — same actions, same order, same hours,
  across a hundred phones

Those get accounts banned regardless of how the touches look. No input library
can help there, and any tool claiming otherwise is selling you something.

Treat this kit as removing the loudest, most mechanically-read signals. It is a
necessary condition, not a sufficient one.

## Example prompt

> Read AUTOMATION.md. Write a shell script for a DeviceFarm phone that opens
> the Play Store, searches for "Signal", and stops at the install screen without
> tapping install. Use only the `h_*` primitives, re-read the UI between steps,
> and keep the whole run under two minutes.


---

*Reference for https://devicefarm.io — see https://devicefarm.io/api-docs for the
same content as a web page, and https://devicefarm.io/mcp for the Claude setup.*
