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.

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:

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:

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:

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 dry run never executes anything. An endpoint whose spec declares no price is refused with not_quotable and nothing is sent, since probing a free endpoint would run it. Call such an endpoint directly, with maxAmountUsd to bound it.

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.