# Building apps powered by taskfuel

taskfuel is a gateway: your app POSTs the URL it wants to call, the gateway pays the upstream's HTTP-402 charge from a prepaid USD balance, and the upstream's response comes back verbatim. No wallet, no payment protocol, no per-provider signup, no SDK.

This guide is for wiring a deployed app to it. If you are an agent setting yourself up in a user's session, read https://app.taskfuel.ai/llms.txt instead.

## Give the app its own key

Never ship the user's own key inside an app. Mint a separate one through the device-code flow so it can be revoked on its own, and so spend from the app is distinguishable from spend by a person.

```bash
curl -fsSL -X POST https://app.taskfuel.ai/v1/connect/start
# → {"code":"…","verification_url":"https://app.taskfuel.ai/connect?code=…","expires_in":…,"poll_interval":2}
```

Show the URL and code to the user. They approve it in a browser, then you poll:

```bash
curl -fsSL "https://app.taskfuel.ai/v1/connect/poll?code=CODE"
# pending   → {"status":"pending"}
# approved  → {"status":"approved","key":"sk-402-…"}
# afterwards→ 404
```

**The key is delivered exactly once.** The approved response is served by a delete-and-return, so a second poll gets a 404 and the key is gone for good. Write the raw response to disk *before* parsing it — a poller that looks for the wrong field name consumes the delivery and loses the key permanently. This has actually happened; this pattern is verified:

```bash
OUT=.secrets/taskfuel-key.json
touch "$OUT" && chmod 600 "$OUT"
for i in $(seq 1 300); do
  resp=$(curl -fsSL "https://app.taskfuel.ai/v1/connect/poll?code=$CODE" 2>/dev/null)
  # save ANY non-pending response BEFORE inspecting it
  if [ -n "$resp" ] && ! echo "$resp" | grep -q '"pending"'; then
    printf '%s' "$resp" > "$OUT"
    grep -q '"approved"' "$OUT" && { echo "APPROVED"; exit 0; } \
      || { echo "NON-PENDING RESPONSE SAVED (inspect $OUT)"; exit 2; }
  fi
  sleep 2
done
echo "TIMED OUT"; exit 1
```

Store the key chmod 600, out of the repo, and inject it as an environment variable. It is a bearer credential for real money: server-side only, never in client code, a mobile binary, or a browser bundle.

If you run that poller in the background and later kill it with `pkill -f <name>`, the pattern can match the shell command doing the killing and take it down too. Kill from a command that doesn't contain the script's name.

## Call a paid service

One endpoint, plain fetch:

```js
const r = await fetch('https://app.taskfuel.ai/v1/call', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.TASKFUEL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://blockrun.ai/api/v1/chat/completions',
    method: 'POST',
    body: {
      model: 'zai/glm-5.2',
      messages: [{ role: 'user', content: 'Hello' }],
      max_tokens: 500,
    },
    maxAmountUsd: 0.02,   // hard ceiling for this call — always set it
  }),
});

const data = await r.json();               // upstream response, passed through
r.headers.get('x-taskfuel-cost');          // USD actually charged
r.headers.get('x-taskfuel-balance');       // USD remaining
r.headers.get('x-taskfuel-request-id');    // quote this when reporting a problem
r.headers.get('x-taskfuel-result-url');    // where to fetch this response back, free
```

Find what you can call with `GET /v1/discover?q=…`, which searches every provider's spec. Only the domains it lists are callable; anything else is refused.

Binary responses (image and audio generation) pass through as well: check `content-type` and stream to disk rather than assuming JSON.

## Know the price before you ship

Add `dryRun: true` in development to read the real price off the upstream's 402 challenge without paying. It is the authoritative price for that exact payload, not an estimate.

Don't leave `dryRun` in production code. It doubles your requests. Quote once while developing, then set a fixed `maxAmountUsd` at the known price plus headroom.

A free endpoint never issues a 402, so its dry run executes the request and returns the response at $0. That is the only way to learn a price is zero.

## Limits and failures

| What | Behaviour |
|---|---|
| Per-call ceiling | $10, enforced by the gateway. `maxAmountUsd` can only tighten it |
| Request body | 1 MiB |
| Rate limited | `429`: back off, don't hammer |
| Empty balance | `402`: surface it; the user tops up at https://app.taskfuel.ai |
| Upstream error | Passed through with its own status. Failed calls are not charged |
| Timeout on your side | The call is not cancelled. It completes and is charged, and the response stays fetchable for an hour |

You are only charged when the upstream delivers and the payment settles, so an upstream 500 doesn't cost you. A successful call is charged whatever the response says, though: an empty or unhelpful result is a real charge, not a billing error.

A timeout in your own client is the case to code for. The gateway holds the connection open for the whole upstream call, and a generative endpoint can outrun a default HTTP timeout, so your request can fail while the call succeeds and settles. Re-issuing it buys the same work a second time. Fetch the response back for free instead, with `GET /v1/calls/{request_id}` (the id is in `x-taskfuel-request-id`), or `GET /v1/calls` to list what is still available.

## Spending safely

The key can spend the whole balance. Nobody is watching at call time, so the guardrails have to be in the code:

- Set `maxAmountUsd` on every call, always.
- Cap anything a user can trigger. A paid call behind an unauthenticated endpoint is a way to drain a balance.
- Track `x-taskfuel-cost` and enforce your own daily budget, then fail closed when it's hit.
- Stub the taskfuel layer in tests. A test suite in CI that makes real paid calls bills real money on every push.
- Log `x-taskfuel-request-id` alongside cost so a surprising bill can be traced to a call.

## Hosting

Any host works. The integration is one HTTPS call. For a throwaway or agent-provisioned deployment, vm402 sells metered VMs that are themselves bought through taskfuel (`taskfuel call https://vm402.com/vms --method POST --body '{"hours":24}'`), which keeps the app and its hosting on one balance. Its docs are at https://vm402.com/docs; the VM sleeps when idle and runs no code while asleep, so anything periodic has to reconcile on the next request rather than on a timer.
