Skip to content
pilots
Dashboard
All pages

Billing and usage

A machine is charged for its processor and memory while it is awake and for its disk while it exists, and for nothing else. The prices are on the pricing page. This page is how the metering works and how to read it.

What is metered, and when

Quantity Charged Counted while
vCPU-seconds $0.000005 each The machine is running. Never while it is suspended, stopped or being created.
GiB-seconds of memory $0.000002 each The machine is running, at the memory it was given.
Disk, in GB-months $0.04 per GB-month Always. A volume at its provisioned size, whether or not it is full, as on Fly, so create it at the size you need. Plus what the machine’s checkpoints and its own disk hold in storage. The image a machine was made from is shared and is not counted.

Each host meters its own machines, opening and closing an interval on every start, sleep and stop, so a machine that wakes for half a second is charged for half a second. A machine that moves to another host is closed on one and opened on the other at the same moment, and is never counted twice. Outbound transfer is not charged, and nothing is charged per machine, app, request or person.

The trial and the free allowance

A new person starts with a one-time trial: $5 of usage at list price, spent before anything else, for 30 days from their first team. After that, every person gets 40 vCPU-hours, 80 GiB-hours and 5 GB-months each calendar month (UTC), with no card. Both are shared by every team that person owns, so opening more teams does not open more of either. A GitHub account younger than 30 days gets a fifth of both until it is that old.

With a card on file the allowance still applies first, and only what goes past it is charged, at the end of the month. A month that comes to less than $1 is not charged.

When the allowance is used, or a budget is reached

A team with no card that has used the allowance is put on hold, and so is a team whose bill reaches the monthly budget it set itself. On hold, its machines sleep: the running ones are put to sleep, a sleeping one is not woken by a request or a command, and nothing new is created, deployed, built or restored for it. A request to one of its addresses answers 402, and the API answers billing_hold (errors).

  • Nothing is deleted. Every disk, volume, checkpoint and address stays where it was.
  • Reading, listing and deleting still work, so a team can make room on its own.
  • The hold lifts when a card is added, when the budget is raised or cleared, or when the month turns. The next request wakes each machine as usual.
  • A monthly charge that fails is shown in the dashboard and in pilot billing, with a link to pay it. If it is still unpaid seven days later, the team is held the same way until it is paid.
  • There is no other limit. No team is held to a number of machines, apps, volumes, builds or checkpoints.

Removing a card

pilot billing card remove (or --remove) asks first, then charges what the team owes so far this month beyond the free allowance and takes the card off. The team is back on the free allowance at once, and if it has already used it, its machines sleep until a card is added or the month turns. Nothing is deleted.

Invoiced and internal teams

Some teams are billed by invoice by arrangement instead of by card. Their usage is metered and shown exactly the same way, and they are never put on hold. The operator of a fleet can also mark its own teams as internal, which are metered and shown and never charged. Only an operator sets either, with pilot billing mode and an admin key, and every change is recorded with who made it and why.

From the terminal

pilot billing                    # this month: used, free left, cost, budget, invoices
pilot billing card               # opens the page to add a card, then waits for it
pilot billing card remove        # charges what is owed so far, then back to the free allowance
pilot billing budget 50          # sleep this team's machines once the month reaches 50 dollars
pilot billing budget --clear
pilot usage --period 2026-10 --group-by label:customer

pilot billing card opens the payment page in your browser, because a card is something you approve yourself. It is the only step that needs a browser, and the command waits until the card is on file. A budget can be set by an owner or an admin of the team.

From the API

curl -H "Authorization: Bearer $PILOT_API_KEY" \
  "https://api.pilotrun.app/v1/usage?period=2026-10&group_by=label:customer&scope=fleet"

curl "https://api.pilotrun.app/v1/pricing"
Parameter What it does
period One calendar month in UTC, as YYYY-MM. Or pass since and until in unix seconds.
group_by machine, or label:<key> for the value of one label.
scope fleet sums every host. Without it the answer is the host that received the request, and complete says whether every host answered.

Every row carries the metered seconds, the billable vcpu_seconds, gib_seconds and disk_gb_months, and a cost at list price before the allowance. A key sees its own team’s usage and nobody else’s. GET /v1/pricing is the price list, the trial and the allowance, and needs no key.

Billing your own customers

A product built on pilots usually keeps every customer’s machines in its own team. Label each machine with the customer it belongs to when you create it, and read usage grouped by that label: each customer’s row is exactly their machines, priced the same way your invoice is. The label is recorded with every interval, so a machine that has been destroyed still counts toward the customer it served. Machines without the label are grouped under an empty value rather than dropped.

Labels count from the moment they are set. To hand a machine you made earlier to a customer, for example one from a pool of ready machines, change its labels with PATCH /v1/machines/{id} and a body such as {"labels": {"customer": "c1", "pool": ""}}. The new labels are merged into the old ones, and an empty value removes a label. Usage before the change stays with the old labels and usage after it goes to the new ones, so changing a label never moves usage that was already recorded.

JavaScript
import { PilotsClient } from '@pilots/sdk'

const pilots = new PilotsClient(process.env.PILOT_API_KEY)
const usage = await pilots.usage.get({ period: '2026-10', groupBy: 'label:customer', fleet: true })
const [team] = Object.keys(usage.orgs)
for (const [customer, row] of Object.entries(usage.groups?.[team] ?? {})) {
  console.log(customer, row.vcpu_seconds, row.gib_seconds, row.disk_gb_months, row.cost?.total)
}
const prices = await pilots.usage.pricing()