Run it every day
Three recipes for work that repeats, such as a nightly APK smoke test in GitHub Actions, a cron job with the SDK, and a scheduled agent on a signed-in phone. Each checks the credit first and always parks the phone.
Some phone work is worth doing once. Some repeats by nature: testing every build, a daily check of a mobile flow, a post or a report that goes out every morning. For that kind of work, keep one phone for the job, park it between runs, and start it on a schedule. A parked phone keeps its apps, files and signed-in accounts, and costs nothing while it waits.
Each recipe below does the same three things around the work itself:
- Checks the credit first. It reads the account, and when there isn't enough for the run it stops with a message that has
top_up_url, a page where a person signs in and pays for credit, instead of failing quietly. Aninsufficient_creditserror carries the same link indetails.top_up_url. - Starts a parked phone with a short
max_duration, so a run that hangs can't keep billing. - Always parks the phone, even when the run failed.
A run of up to 10 minutes reserves $0.60 when it starts, and you pay only for the seconds it runs. $20 of credit is about 5½ phone-hours, or about 33 ten-minute runs.
Make the phone once
Create the phone by hand, note its ID, and park it. Every run then starts this same phone.
phonebox create --name nightly-smoke --idempotency-key nightly-smoke-1 --no-wait
phonebox status ph_7kx2m6q4v3ta --wait
phonebox park ph_7kx2m6q4v3taThe scheduled job needs the phone's ID and an API key. Create an Agent key under API keys for the job, rather than reusing your own, so you can revoke it on its own.
Nightly APK smoke test in GitHub Actions
This workflow builds your debug APK every night, installs it on the parked phone, opens it, waits for a screen you expect, saves a screenshot, and parks the phone. Store the key as the repository secret PHONEBOX_API_KEY and the phone's ID as the repository variable PHONEBOX_PHONE.
name: Nightly phone smoke test
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
PHONEBOX_API_KEY: ${{ secrets.PHONEBOX_API_KEY }}
PHONEBOX_PHONE: ${{ vars.PHONEBOX_PHONE }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "17" }
- uses: actions/setup-node@v4
with: { node-version: "22" }
- run: npm install -g https://phonebox.dev/downloads/phonebox-0.1.0.tgz
- name: Check Phonebox credit
run: |
phonebox account > account.json
available=$(jq -r .available_usd account.json)
if awk -v a="$available" 'BEGIN { exit !(a < 0.60) }'; then
echo "::error::Phonebox credit is low (\$$available available). Add credit: $(jq -r .top_up_url account.json)"
exit 1
fi
- run: ./gradlew assembleDebug
- name: Smoke test on the phone
run: |
phonebox start "$PHONEBOX_PHONE" --max 10m
phonebox apps "$PHONEBOX_PHONE" install app/build/outputs/apk/debug/app-debug.apk
phonebox open "$PHONEBOX_PHONE" com.example.app
phonebox wait "$PHONEBOX_PHONE" --text "Welcome" --timeout 30s
phonebox screenshot "$PHONEBOX_PHONE" smoke.jpg
- name: Park the phone
if: always()
run: phonebox park "$PHONEBOX_PHONE"
- uses: actions/upload-artifact@v4
if: always()
with: { name: smoke-screenshot, path: smoke.jpg, if-no-files-found: ignore }- Replace
com.example.appand"Welcome"with your app's package and a text its first screen shows. - A failed check fails the job, and GitHub emails you about a failed scheduled run. When credit is short, the error has the top-up link, and a refused
phonebox startprintsinsufficient_creditswith the same link indetails.top_up_url. - The top-up link lets whoever has it pay for this project's credit, and shows the project's name. In a public repository, print
https://phonebox.dev/app/billinginstead, so the link stays out of public logs. - To test on every push as well, add
pushunderon. Two runs at once would share the phone, so add aconcurrencygroup to the job.
A cron job with the SDK
On a server or a machine that is always on, a short script with the SDK can run a daily check. This one opens a page in the phone's browser, waits for the text it should show and saves a screenshot. It prints nothing when all is well, so cron only emails you when something needs you.
npm install https://phonebox.dev/downloads/phonebox-0.1.0.tgzimport { writeFile } from "node:fs/promises";
import { Phonebox, PhoneboxError } from "phonebox";
const pb = new Phonebox(); // reads PHONEBOX_API_KEY
const account = await pb.account();
if (Number(account.available_usd) < 0.6) {
console.error(`Phonebox credit is low ($${account.available_usd} available). Add credit: ${account.top_up_url}`);
process.exit(1);
}
const phone = await pb.phones.get(process.env.PHONEBOX_PHONE);
try {
await phone.start({ maxDuration: 600 }); // waits until it is ready, with its apps and sign-ins kept
await phone.open("https://example.com/pricing");
await phone.waitFor({ text: "Pricing" }, { timeoutMs: 20_000 });
await writeFile(`check-${new Date().toISOString().slice(0, 10)}.jpg`, await phone.screenshot({ format: "jpeg" }));
} catch (error) {
if (error instanceof PhoneboxError && error.code === "insufficient_credits") console.error(`Phonebox credit ran out. Add credit: ${error.details.top_up_url}`);
throw error;
} finally {
await phone.park();
}MAILTO=you@example.com
PHONEBOX_API_KEY=pbx_…
PHONEBOX_PHONE=ph_7kx2m6q4v3ta
0 7 * * * cd /opt/phone-checks && node daily-check.mjsKeep the crontab readable only by you, since it holds the key. A failed waitFor throws, so the run ends with an error that cron emails, and the phone is parked either way.
A scheduled agent on a signed-in phone
Some daily work needs judgment rather than a fixed script, such as posting today's update in an app that has no API, or checking an inbox only the mobile app can reach. A coding agent you can run without a person, such as Claude Code with claude -p, can do it over MCP on a phone that is already signed in.
-
Sign the phone in once. Make the phone as above, start it, and create a live view link with
phonebox live ph_7kx2m6q4v3ta --expires 30m. Open it, sign in to the app yourself, thenphonebox park ph_7kx2m6q4v3ta. The phone stays signed in while it is parked. -
Write the task down, with the credit check and the park in it:
daily-task.md Use the Phonebox MCP tools. 1. Call get_account. If available_usd is below 0.60, do nothing else and reply: "Phonebox credit is low. Add credit: <top_up_url>". Then stop. 2. Call start_phone with phone_id ph_7kx2m6q4v3ta and max_duration 900. 3. Open the app and post today's update from updates/today.md. Observe after every action. 4. Call park_phone with phone_id ph_7kx2m6q4v3ta, even if a step failed. 5. Reply with one line: what you posted, or what stopped you. -
Connect the agent to Phonebox with a config file,
phonebox.mcp.json, that reads the key from the environment:{ "mcpServers": { "phonebox": { "type": "http", "url": "https://phonebox.dev/mcp", "headers": { "Authorization": "Bearer ${PHONEBOX_API_KEY}" } } } } -
Schedule it. cron emails you the agent's one-line reply every morning, the top-up link included when credit is low:
crontab -e MAILTO=you@example.com PHONEBOX_API_KEY=pbx_… 0 8 * * * cd /opt/daily-agent && claude -p "$(cat daily-task.md)" --mcp-config phonebox.mcp.json --allowedTools "mcp__phonebox"
--allowedTools "mcp__phonebox" lets the agent use Phonebox's tools without asking, and nothing else. An idle phone parks itself 5 minutes after its last call, so a run the agent abandons stops billing on its own; max_duration caps the rest.
Keep runs cheap and safe
- Give every run a short
max_duration. A phone also parks itself when it has been idle for itsidle_timeout, 5 minutes by default. - Run one job per phone. Two jobs on one phone act on screens they didn't read.
- Set a monthly spending limit in Settings as a ceiling. A run past it is refused with
spend_limit_reached, whosedetails.settings_urlis where you raise it. - Keep costs down has more.
Test your Android build with a coding agent
Install the APK your coding agent just built on a Phonebox phone in one command, check its version, then have the agent walk the new screens and report what it saw.
Build a multi-tenant product
Give each of your end users their own phone, tag phones with metadata, limit keys to one customer's phones, and park phones between uses.