Backfill
July 10, 2026 · View on GitHub
Get paid while your terminal waits. A sponsored line rides long runs: dbt run, cargo build, docker build, and the rest of the slow stuff. Advertisers bid for the slot, you keep half. Open source, and it never reads your code.

A real dbt run under bf: the per-model START/OK noise collapses into one live line that carries the ad, with the header and PASS/WARN/ERROR summary intact.
Quickstart
pip install backfill-cli # downloads + SHA-256-verifies the bf binary on first run
bf init # wrap the common slow commands; your normal runs now earn
dbt run # runs exactly as before, with a sponsored line that pays
bf init wraps a curated set (dbt, sqlmesh, cargo, docker, terraform, npm, and more). bf init --all wraps every non-interactive command on your PATH. bf wrap / bf unwrap adjust the list, bf uninit removes it. The explicit bf dbt run always works with no setup.
How it shows up
There are two main surfaces.
- Coding agents.
bf agents install claudesets Claude Code's thinking-spinner verb to a rotating batch.bf agents install droidinstalls a Factory droid statusLine. Codex can be launched throughbf spin codex. The ad, a trending dev content slot, and your running$X.XX earnedtally cycle while the agent thinks. Claude installs no status line and never touches an existing one. Claude refreshes the verb onSessionStartand each turn. - Any other wrapped command (dbt, sqlmesh, cargo, docker, make,
terraform plan, npm/pnpm/yarn/bun scripts, go). The run collapses into one live line that rotates the ad, a trending repo / HN story / tip, and your $earned tally, with a spinner and an elapsed timer. That single line replaces the scrolling output in place. On a non-zero exit the captured output is flushed, so failures are never hidden. dbt and sqlmesh also show model progress counts on that line. Package installs are the plain-output exception described below.
Interactive and full-screen commands (vim, less, ssh, sudo, gh, psql and other REPLs, terraform apply, docker run -it, npm login) are detected and run directly in your terminal, untouched. Package-manager scaffolders such as npm init also keep their native terminal interaction, then get one completion ad after a successful exit. CI and non-TTY runs exec plainly with zero overhead.
Install
Three install paths. All of them SHA-256 verify the downloaded binary.
# 1) PyPI (most common)
pip install backfill-cli
# 2) Homebrew
brew install shyamsivakumar/tap/backfill
# 3) curl installer
curl -fsSL https://backfill.sh/install.sh | sh
The pip wheel ships a thin Python entry point. On first run it fetches the matching bf release binary, verifies its SHA-256 against the published checksums, and execs it.
macOS note. Recent macOS (26 / "Tahoe") runs a Code Signing Monitor that silently kills an unsigned downloaded bf on launch, with no error to your shell. The pip and curl installers re-sign the binary ad-hoc (codesign --sign -) after download to clear this. If you copy a bf binary from somewhere else and hit it, run codesign --force --sign - /path/to/bf once and it works.
Locked-down containers (Paradime, Codespaces, read-only base env)
When the base Python env isn't writable, pip install falls back to --user and puts bf in a dir that isn't on PATH (you'll see "The script bf is installed in '.../.local/bin' which is not on PATH"). That's a pip/platform thing, not a bf bug. Two ways through it:
# bootstrap without bf on PATH (self-heals PATH into your rc for next shell):
python -m backfill_cli init && exec $SHELL
# or use the explicit form, which never needs PATH:
python -m backfill_cli dbt run --select my_model
On Paradime, the durable path is bf init. It adds a real export PATH="$HOME/.backfill/shims:$PATH" to ~/.zshrc, which the Code IDE terminal sources. Open a fresh terminal and command -v dbt should resolve to ~/.backfill/shims/dbt. Avoid setting PATH through the Code IDE env-var UI: those values are stored literally, so a ${PATH} reference won't expand and will clobber your shell's PATH.
bf init installs a pass-through shim per command into ~/.backfill/shims. Because it's a real shim and not a shell alias, it fires wherever the command runs: your shell, a Makefile, a script.
Commands
bf is a single MIT-licensed Go binary. The full surface:
| Command | What it does |
|---|---|
bf <cmd>... | Run <cmd> wrapped. No setup needed. |
bf init [cmd...] | One-time setup. Wraps a curated set of slow commands (dbt, sqlmesh, cargo, docker, terraform, npm, and more) by installing a PATH shim per command in ~/.backfill/shims and prepending that dir to your shell rc. Pass extra commands to wrap more. |
bf init --all | Wrap every non-interactive command on your PATH (skips interactive tools: editors, shells, paginators, sudo, ssh, anything that takes over the screen). |
bf wrap <cmd>... | Wrap the listed commands now (adds shims). |
bf unwrap <cmd>... | Remove the shims for the listed commands. |
bf uninit | Remove every shim bf init / bf init --all / bf wrap installed, and strip the PATH line from your rc. |
bf on / bf off | Globally pause or resume. off execs plainly with zero overhead, as if no shim is installed. |
bf status | Show what's wrapped, current on/off state, and your device id and dashboard link. |
bf claim | Print a one-time code and link to bind this device to your web account, so earnings show in your dashboard. |
bf last | Show the last run receipt, including status, command, duration, estimated earnings, and checkpoints. |
bf logs last | Print the captured log from the most recent wrapped run. |
bf refer | Print a referral install command. You earn a 10% bonus from Backfill's share, not from the referred user's share. |
bf agents install claude | Spinner-verb rotation for Claude Code. No status line. |
bf agents install droid | Factory droid statusLine integration. Refuses to overwrite an existing statusLine unless you pass --force. |
bf spin codex | Run Codex through the spinner rewriter. |
bf agents remove <name> | Remove a previously installed agent integration. |
bf agents status | Show which agent integrations are installed. |
bf wrap droid routes Factory droid through the bf spin rewriter without installing the Factory statusLine integration (handy for droid-specific sessions).
How the wrapper works
bf <cmd> runs <cmd> and collapses its output into one live line. There is no pseudo-terminal on the collapsed path, no scroll region, and no reserved row.
- A wrapped non-interactive command has its stdout and stderr piped and collapsed into the one live line. On a non-zero exit the captured output is flushed, so failures are never hidden.
- Interactive and full-screen commands (vim, less, ssh, sudo, gh, psql and other REPLs,
terraform apply,docker run -it,npm login) are detected and run directly in your terminal, untouched. Package-manager scaffolders keep native prompts and add only their success completion ad. - Exit codes pass through end to end.
- Non-TTY execs (CI, Airflow, dbt Cloud, cron) run plainly with zero overhead: the shim detects no TTY and just
execs the underlying binary.
Smart progress
For verbose commands, the per-line noise is the problem, not the wait. bf recognizes a set of command/verb pairs and collapses the run into one live line that carries the ad, your $earned tally, model progress, a spinner, and an elapsed timer.
| Command | Recognized verbs | What you see |
|---|---|---|
dbt | run, build, test, seed, snapshot | One live line like ⠹ dbt 5/8 main.fct_orders · ad …, the version header, any errors verbatim, and the final PASS/WARN/ERROR summary. |
sqlmesh | plan, run | One live line carrying the model being applied and the ad. |
In both cases the ad rides the line your eyes are already on. The header stays, errors stay, the summary stays. Everything in between collapses.
SQLMesh
SQLMesh is wrapped by bf init out of the box (or add it explicitly with bf wrap sqlmesh). Smart progress is active for sqlmesh plan and sqlmesh run: SQLMesh's per-model output collapses into one live line carrying the ad, the important output and the result stay, and the child exit code passes through. Same engine as the dbt smart progress.
⠹ sqlmesh applying prod.my_model · ad …
It works from shells, Makefiles, and scripts through the PATH shim. Non-TTY and CI runs pass through plainly.
Scaffold completion ads
After any of the following exits 0, bf prints one persistent sponsored line under the success screen (one impression, regardless of how long the command took):
npm create/npm init,pnpm create/pnpm init,yarn create/yarn init,bun create/bun initnpx create-*cargo new,cargo initdjango-admin startprojectrails newdotnet new- any binary named
create-*on yourPATHthat exits 0
Package-manager scaffolders run plainly so their prompts remain usable. The other listed scaffolders use the normal collapsed route. In both cases, the line prints once under whatever success UI the scaffolder drew. These commands finish too fast for the live line to earn, but their "you're all set, here's what's next" screen is the highest-intent moment in the session.
npm and package installs
Package installs are long waits worth monetizing. bf init wraps npm, pnpm, and yarn; add bun with bf wrap bun. Installs do not collapse: they stay attached to the terminal so the package manager's own resolving, download, and build progress remains visible.
npm install
After a successful install, bf prints one persistent sponsored completion line under the package manager's own summary. Failed installs keep their native output and do not print that line. Examples covered by this contract include npm install / i / ci / update, pnpm add, bare yarn and its install commands, bun install, pip install, and their supported install aliases. Package-manager scripts such as npm run build, npm test, pnpm dev, and bun run start still use the collapsed live line. npm login passes through untouched; npm init follows the scaffold contract above. CI and non-TTY installs pass through plainly with no completion ad.
Scaffold completions are separate (see above): after npm create / npm init, the pnpm/yarn/bun equivalents, or npx create-* exit 0, bf prints one persistent sponsored line under the "you're all set" success screen.
Coding agents
bf doesn't patch any agent's source. It uses each agent's own exposed surface.
| Agent | Integration | Install |
|---|---|---|
| Claude Code | Spinner-verb rotation (no status line) | bf agents install claude |
Factory (droid) | StatusLine integration | bf agents install droid |
| Codex | Spinner rewriter for the running command | bf spin codex |
For Factory droid spinner rewriting without a statusLine, use bf wrap droid. For Claude Code you can also install via the plugin marketplace: /plugin marketplace add shyamsivakumar/backfill then /plugin install backfill@backfill. bf agents remove claude undoes it.
What it sells that no other ad network can
- Command-level segments. Advertisers buy "developers currently running dbt," not "developers." The command name is the only targeting signal. No keywords, no profiles, no behavior graph.
- Verified dwell. A live line during a 15-minute compile is continuous, unskippable attention. There's no tab to switch away from without abandoning the build.
- CI earnings routing. Via the GitHub Action, a repo points its build-log earnings at its maintainers by passing a Backfill device id. Your CI minutes fund the dependencies you build on.
Surfaces
| Surface | What's wrapped | How |
|---|---|---|
| dbt + data stack | dbt, bq, snowsql, spark-submit, sqlmesh | bf init (curated set) or bf wrap <cmd> |
| Any CLI tool | cargo, docker, make, terraform, gradle, … | bf init covers these, or bf wrap <cmd> / bf init --all |
| Coding agents | Claude Code spinner verbs, Factory droid statusLine, Codex spinner rewrite | bf agents install … or bf spin codex |
| Scaffold screens | npm create, cargo new, rails new, … | automatic on a clean wrapped run |
| CI build logs | the GitHub Action (action/action.yml) | maintainer-directed earnings |
GitHub Action
Use the composite action to put a sponsored line in a GitHub Actions job and
credit eligible impressions to a Backfill device. The action runs the command
from run and returns the same exit code.
Run bf status locally to find the device id you want to credit. Save it as a
repository Actions secret named BACKFILL_DEVICE_ID instead of committing it
to the workflow. Then add this file at .github/workflows/backfill.yml:
name: Backfill CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests with Backfill
uses: shyamsivakumar/backfill/action@<tag-or-sha>
with:
run: make test
device: ${{ secrets.BACKFILL_DEVICE_ID }}
Replace <tag-or-sha> with a Backfill release tag or, for a fixed supply-chain
pin, a full commit SHA. The contents: read permission lets the checkout step
read the repository. The Backfill step does not use GITHUB_TOKEN and needs no
write permission.
Forks and missing device ids
GitHub does not pass repository Actions secrets to pull_request workflows
from forks. On those runs, device is empty. The action can still fetch and
render a sponsored line, then run make test, but it skips the impression POST
and credits no device. The same behavior applies whenever device is omitted.
Do not change this job to pull_request_target to expose the secret to forked
code. Keep the ordinary pull_request trigger so untrusted changes run without
repository secrets.
Failures and logs
The wrapped command controls the step result. If make test exits 7, the
Backfill step exits 7 and the job fails normally. The command's stdout and
stderr stay in the GitHub Actions log; Backfill does not send that output to
its API.
Ad response text is escaped or flattened before it is written to the log, so
server-provided text such as ::error::... cannot create a GitHub workflow
command. action/test_action.sh covers this case, missing-device crediting,
and non-zero exit pass-through.
CI privacy
The action makes these Backfill API requests:
- To fetch an ad, it sends the static command name
ciand the supplied device id, which is empty whendeviceis unset. - After a run of at least 5 seconds, it posts an impression only when both an
ad id and device id are present. That JSON contains
deviceId,adId, the staticcmdvalueci, elapsedseconds, andkind: "impression".
Beyond the device id passed explicitly, it does not transmit the run string,
repository contents, paths, other environment variables, pull request or
commit metadata, stdout, or stderr. It does not read other Actions secrets.
The optional api input defaults to https://backfill.sh. Set it only for a
self-test or staging endpoint. Normal workflows should omit it.
Privacy
The CLI is structurally incapable of reading your code, command args, command output, or environment. The only fields it ever transmits:
- device id, a random per-install id
- ad id, the campaign the server chose to serve
- command name, the bare basename, e.g.
dbtorcargo, never the path - visible seconds, how long the line was actually on screen
- event kind, a static label,
impressionorclick
No args, no paths, no filenames, no env, no stdout or stderr contents. The source is open under the MIT license, so you can verify.
Economics
- Unit: 1 impression = 5 visible seconds.
- Pricing: advertisers buy blocks of 1,000 impressions (CPM). Clicks bill higher than impressions.
- Split: users keep 50% of attributable revenue.
- Balances accrue per run and surface in
bf statusand the web dashboard. - Payouts: Stripe, once a balance crosses $25. Payout plumbing is planned, not live yet. Balances accrue today.
- Early inventory: while the first advertiser slots sell, the slot is filled with house ads at
cpm = 0. No money changes hands, but the slot is exercised and you see a real sponsored line.
Advertiser side
Advertisers self-serve through the portal at backfill.sh/advertiser:
- Sign in with a magic link, email only, no password.
- Prepay an ad budget with a Stripe deposit.
- Submit a campaign: ad text, an https link, a CPM, and optional command targeting (e.g. only on
dbtruns). - Every campaign is reviewed and approved before it serves. You only pay for verified impression and click time, billed against your deposit.
Ad selection
The hosted Backfill service gives every candidate a single unified eCPM in micros, then picks the max under frequency-cap and flight-window gating. The components:
- Direct CPM, what the advertiser pays per 1,000 impressions.
- Affiliate expected value =
payout × conversion-rate prior, converted to an eCPM equivalent. - House floor, the minimum to serve (currently 0 while house-ad inventory fills slots).
Bayesian shrinkage tempers noisy per-campaign conversion priors, so a campaign with 3 clicks doesn't outrank one with 3,000. A frequency cap stops a device from seeing the same ad back to back across runs, and flight windows gate serving to a campaign's scheduled dates. The hosted service code is not part of this CLI repo.
Repo layout
| Dir | What |
|---|---|
cli/ | bf, the Go wrapper. Runs the terminal and coding-agent surfaces. |
action/action.yml | GitHub Action: the same model for CI build logs, with maintainer-directed earnings. |
python/ | Thin Python wrapper shipped in the backfill-cli wheel: fetches + SHA-256-verifies the Go binary, re-signs it ad-hoc on macOS, then execs it. |
Tests
What's covered today:
- CLI scaffold detection, including the
create-*/cargo new/npm createallowlist and the one-line completion ad. - CLI output rotation, spinner text, receipts, captured logs, notifications, and drain behavior.
Run the CLI tests with go test ./... in cli/.
License
MIT