# Reverse-Engineering Fireworks Credits

> This doc is AI-generated by the [pi](https://pi.dev/) coding agent.

Most providers in pi-credits expose a documented HTTP endpoint that returns a balance. [Fireworks](https://fireworks.ai/) does not. Its dashboard shows account credit, but there is no public REST route for it. This note records how the balance API was found by reverse-engineering the official `firectl` CLI, and how the provider is implemented on top of it.

## Where the binary came from

Fireworks ships a command-line tool, [`firectl`](https://docs.fireworks.ai/tools-sdks/firectl/). The [Homebrew tap formula](https://github.com/fw-ai/homebrew-firectl/blob/main/Formula/firectl.rb) points at per-platform binaries; the `darwin-amd64` build was used here.

It is a Go binary, and crucially an **unstripped** one, which keeps the symbol table, the `pclntab` (function/line metadata), and the full module dependency list. None of that yields original source, but it exposes the program's structure in detail.

```sh
go version -m firectl        # build info: firectl 1.7.22, Go 1.25.0, module path
go tool nm firectl           # 81k symbols, including package-qualified names
go tool objdump -s SYM firectl   # disassembly annotated with file:line
go run github.com/goretk/redress@latest source firectl   # folder/file/function tree
```

## Locating the credit feature

The CLI is a Cobra app that is a thin client over a gRPC service named `gateway.Gateway`. Listing the RPC method paths embedded in the binary surfaced the relevant calls:

```sh
strings firectl | grep -oE '/gateway\.Gateway/[A-Za-z]+' | sort -u
# ... /gateway.Gateway/GetBalance
# ... /gateway.Gateway/GetLedger
# ... /gateway.Gateway/ListCreditRedemptions
# ... /gateway.Gateway/ListAccounts
```

`GetBalance` is exactly what the dashboard shows, but checking the symbol table revealed that **no CLI command is wired to it** — it exists in the gateway client yet is not reachable through any `firectl` subcommand. So the goal became calling it directly.

## Working out authentication

Two facts had to be recovered: the host and the credential format.

The binary contains several Fireworks hostnames. The inference API (`api.fireworks.ai`) is the well-known one, but the control plane uses a separate host:

```sh
strings firectl | grep -oE '[a-z0-9.-]+\.fireworks\.ai' | sort -u
# api.fireworks.ai          (inference)
# gateway.fireworks.ai      (control-plane gRPC)  <- this one
# app.fireworks.ai, device-auth.fireworks.ai
```

For the credential, the disassembly of the client's auth interceptor showed it sets gRPC metadata and references both `authorization` (Bearer) and `x-api-key`. The Bearer slot expects an OIDC ID token, which is what interactive `firectl signin` produces. API-key auth uses the `x-api-key` header instead.

This matched what happened on the wire while probing the endpoint with a saved API key:

- `api.fireworks.ai:443` returned HTTP 404 — wrong host for this service.
- `gateway.fireworks.ai:443` with `authorization: Bearer <key>` returned `UNAUTHENTICATED: invalid id token` — the key is not a JWT.
- `gateway.fireworks.ai:443` with `x-api-key: <key>` authenticated, then returned `INVALID_ARGUMENT` — auth worked; the request body was incomplete.

## Reconstructing the messages

The Go protobuf structs carry their field tags as string literals, so the message shapes can be read straight out of the binary:

```sh
strings firectl | grep -oE 'protobuf:"[^"]*name=(name|money|currency_code|units|nanos)[^"]*"'
```

That, plus the generated getter names (`(*Balance).GetMoney`, `(*GetBalanceRequest).GetName`), gives:

```proto
message GetBalanceRequest { string name = 1; }     // "accounts/<account_id>"
message Balance           { Money money = 1; }
message Money { string currency_code = 1; int64 units = 2; int32 nanos = 3; }  // google.type.Money
```

`GetBalance` needs a `name` like `accounts/<account_id>`. Rather than ask the user for their account id, `ListAccounts` (which the same API key can call) returns it:

```proto
message Account              { string name = 1; string display_name = 2; }
message ListAccountsResponse { repeated Account accounts = 1; }
```

## Confirming end to end

A minimal Node.js client using `@grpc/grpc-js` and `@grpc/proto-loader` confirmed the full flow against the live service:

1. `ListAccounts` with `x-api-key` → `accounts/<id>`.
2. `GetBalance { name }` → `Balance { money: { currency_code, units, nanos } }`.

The amount is `units + nanos / 1e9` (the standard `google.type.Money` encoding), e.g., `units=9, nanos=694293840` → `$9.69`.

## How the provider is implemented in pi-credits

The provider lives in [`src/providers/fireworks.ts`](../src/providers/fireworks.ts) and follows the same `CreditsProvider` contract as the HTTP-based providers; only the transport differs.

- **Transport.** A single, reused `@grpc/grpc-js` client dials `gateway.fireworks.ai:443` over TLS. The service is declared in [`fireworks.proto`](../src/providers/fireworks.proto) — the minimal slice needed, loaded at runtime with `@grpc/proto-loader`. The `.proto` is resolved relative to the module via `import.meta.url`, so it ships and loads from the package directory.
- **Auth.** Each call sends `x-api-key: <key>` in gRPC metadata. The key comes from pi's `modelRegistry.getApiKeyForProvider("fireworks")` (env `FIREWORKS_API_KEY`), handled by the manager.
- **Account resolution.** `ListAccounts` is called once and the resolved `accounts/<id>` is cached per API key, so steady-state refreshes are a single `GetBalance` round trip.
- **Cancellation.** The shared `AbortSignal` cancels the in-flight gRPC call, and a defensive deadline bounds each request.
- **Result.** `Money` is converted to a dollar amount and returned as `{ type: "balance", remaining }`, which the status line renders like any other balance provider.

## Caveats

`GetBalance` is an internal API: it is intentionally absent from `firectl`, undocumented, and not part of any public contract. Its host, request shape, and behavior can change at any time without notice, which would break this provider. The narrow `.proto` keeps the blast radius small if that happens.
