Skip to content

Commit aee8432

Browse files
claude[bot]github-actions[bot]claude
authored
docs: document provider credits/pricing, app releases, and extension download (#4717)
Seven HTTP routes had no mention on any documentation page. Adds them to the api-reference.md endpoint matrix with a worked example each, plus the env var one of them reads. Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent f69a853 commit aee8432

3 files changed

Lines changed: 225 additions & 0 deletions

File tree

docs/api-reference.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ For detailed schemas, see [Chat API](chat-api.md) and [Workflow API](workflow-ap
3737
| Examples | `/api/workflows/examples/thumbnails/{filename}` | `GET` | none | no | Example thumbnail; `.jpg` and `.png` only |
3838
| Assets | `/api/assets/{id}/extract-audio` | `POST` | Depends on `AUTH_PROVIDER` | no | Extract a video asset's audio track into a new WAV asset |
3939
| Assets | `/api/assets/packages/{package}/{file}` | `GET` | none | streaming | Bytes behind a `package://` ref, from a node pack's assets directory |
40+
| Apps | `/api/applications/{id}/released-document` | `GET` | Depends on `AUTH_PROVIDER` | no | The snapshot a published app should run, with each operation's pinned graph; `null` when nothing is published |
41+
| Providers | `/api/fal/credits` | `GET` | Depends on `AUTH_PROVIDER` | no | The server's fal.ai account balance; `204` when no `FAL_API_KEY` is configured |
42+
| Providers | `/api/fal/pricing` | `GET` | Depends on `AUTH_PROVIDER` | no | Unit price per fal.ai endpoint, one or more `?endpoint_id=`; cached an hour |
43+
| Providers | `/api/kie/credits` | `GET` | Depends on `AUTH_PROVIDER` | no | The server's kie.ai credit balance; `204` when no `KIE_API_KEY` is configured |
44+
| Providers | `/api/kie/pricing` | `GET` | Depends on `AUTH_PROVIDER` | no | Credit price per kie.ai model, one or more `?model_id=`; cached an hour |
45+
| Providers | `/api/kie/resolve-dynamic-schema` | `POST` | Depends on `AUTH_PROVIDER` | no | Pasted kie.ai model docs to a node's dynamic properties, inputs, and outputs |
46+
| Extension | `/api/extension/download` | `GET` | Depends on `AUTH_PROVIDER` | no | The built Chrome extension as a zip; `404` when the server has no build |
4047
| Workflow WS | `/ws` | WebSocket | Bearer header or `api_key` query when enforced | yes | Workflow execution, chat, job control, live updates (MessagePack or JSON) |
4148
| Agent WS | `/ws/agent` | WebSocket | Bearer header or `api_key` query when enforced | yes | Agent runtime |
4249
| Extension WS | `/ws/extension` | WebSocket | Follows global auth settings | yes | Browser extension channel |
@@ -284,6 +291,212 @@ are rejected. Responses carry
284291
`cache-control: public, max-age=31536000, immutable` and an ETag, since a
285292
package's assets change only when the package version does.
286293

294+
### Provider Credits
295+
296+
`GET /api/fal/credits` and `GET /api/kie/credits` report what the account behind
297+
the server's API key has left. The editor shows the number next to the provider;
298+
call them yourself to watch a budget from outside the UI.
299+
300+
Both read the key the **server** holds — the stored `FAL_API_KEY` / `KIE_API_KEY`
301+
secret, falling back to the same environment variable — not anything the caller
302+
sends. With no key configured the answer is `204 No Content`.
303+
304+
```bash
305+
curl "http://localhost:7777/api/fal/credits" \
306+
-H "Authorization: Bearer YOUR_TOKEN"
307+
```
308+
309+
```json
310+
{
311+
"credit_balance": { "amount": 42.5, "currency": "USD" },
312+
"username": "your-fal-account"
313+
}
314+
```
315+
316+
kie.ai bills in credits rather than dollars, so its `currency` is the literal
317+
string `"credits"`:
318+
319+
```json
320+
{ "credit_balance": { "amount": 1200, "currency": "credits" } }
321+
```
322+
323+
A provider that refuses the key still answers `200`, with the reason spelled out
324+
— the SPA treats a gateway status as a bug in NodeTool, so the failure is carried
325+
in the body instead:
326+
327+
```json
328+
{
329+
"unavailable": true,
330+
"detail": "Invalid API key or malformed Authorization header",
331+
"credit_balance": null
332+
}
333+
```
334+
335+
Reading a fal.ai balance needs an **Admin** key (create one at
336+
<https://fal.ai/dashboard/keys>); an ordinary key gets `403` from fal.ai, which
337+
comes back as an `unavailable` body saying so.
338+
339+
### Provider Pricing
340+
341+
`GET /api/fal/pricing` returns the unit price of one or more fal.ai endpoints.
342+
Repeat `endpoint_id` for each; omitting it entirely is a `400`. Prices are cached
343+
per endpoint for an hour, and the route answers `204` when no `FAL_API_KEY` is
344+
configured.
345+
346+
```bash
347+
curl "http://localhost:7777/api/fal/pricing?endpoint_id=fal-ai/flux/schnell&endpoint_id=fal-ai/flux/dev" \
348+
-H "Authorization: Bearer YOUR_TOKEN"
349+
```
350+
351+
```json
352+
{
353+
"byEndpointId": {
354+
"fal-ai/flux/schnell": {
355+
"unit_price": 0.003,
356+
"billing_unit": "megapixel",
357+
"currency": "USD"
358+
}
359+
},
360+
"fetched_at": "2026-08-05T08:21:49.251Z"
361+
}
362+
```
363+
364+
`GET /api/kie/pricing` is the same shape over kie.ai model ids (`?model_id=`,
365+
repeatable, `400` when absent). It needs no API key — kie.ai publishes its
366+
pricing pages openly — and returns a per-model summary:
367+
368+
```json
369+
{
370+
"byModelId": {
371+
"flux-2/pro-text-to-image": {
372+
"model_id": "flux-2/pro-text-to-image",
373+
"unit_price": 9,
374+
"billing_unit": "second",
375+
"currency": "credits",
376+
"usd_price": 0.045,
377+
"tier_count": 4,
378+
"pricing_url": "https://kie.ai/flux-2"
379+
}
380+
},
381+
"fetched_at": "2026-08-05T08:19:54.783Z"
382+
}
383+
```
384+
385+
Most kie.ai models are priced in tiers. `unit_price` is the cheapest of them,
386+
`tier_count` says how many there were, and `billing_unit` is `"varies"` when the
387+
tiers are not billed by the same unit.
388+
389+
An id with no published price is absent from the map rather than an error, so a
390+
request for five models can come back with three.
391+
392+
### Resolving a KIE Model's Schema
393+
394+
kie.ai adds models faster than a node can be written for each, so `KieAINode`
395+
takes its shape from the model's documentation. `POST
396+
/api/kie/resolve-dynamic-schema` does that parse: give it the docs page as
397+
`model_info`, get back the node's dynamic properties, inputs, and outputs. The
398+
editor calls it when you paste docs into the node; call it directly to check what
399+
a page would produce before wiring anything.
400+
401+
```bash
402+
curl -X POST "http://localhost:7777/api/kie/resolve-dynamic-schema" \
403+
-H "Content-Type: application/json" \
404+
-H "Authorization: Bearer YOUR_TOKEN" \
405+
-d '{"model_info": "| **Format** | `bytedance/seedance-2` |\n\n### input Object Parameters\n\n#### prompt\n- **Type**: `string`\n- **Required**: No\n- **Description**: The text prompt for the video.\n"}'
406+
```
407+
408+
```json
409+
{
410+
"model_id": "bytedance/seedance-2",
411+
"dynamic_properties": { "prompt": "" },
412+
"dynamic_inputs": {
413+
"prompt": {
414+
"type": "str",
415+
"type_args": [],
416+
"optional": true,
417+
"description": "The text prompt for the video."
418+
}
419+
},
420+
"dynamic_outputs": {
421+
"video": { "type": "video", "type_args": [], "optional": false }
422+
}
423+
}
424+
```
425+
426+
The model id comes from the docs' **Format** row; parameters are read from the
427+
`#### <name>` headings under **input Object Parameters**, so an excerpt missing
428+
that section resolves to a node with no inputs.
429+
430+
Docs with no recognizable model id are a `400`
431+
(`{"code": "INVALID_INPUT", "detail": "Could not find model ID in documentation"}`),
432+
as is a missing or empty `model_info`.
433+
434+
### What a Published App Runs
435+
436+
Publishing a mini app freezes a snapshot: the document as it stood, plus the
437+
graph of every workflow its operations call. `GET
438+
/api/applications/{id}/released-document` returns that snapshot, so a runtime can
439+
serve the published app without reading the draft the author is still editing.
440+
441+
```bash
442+
curl "http://localhost:7777/api/applications/<application_id>/released-document" \
443+
-H "Authorization: Bearer YOUR_TOKEN"
444+
```
445+
446+
```json
447+
{
448+
"id": "548770b4c014436ba8549509575e9be6",
449+
"applicationId": "76a381309a584a13b823b297cbd9b4b1",
450+
"version": 1,
451+
"document": {
452+
"schemaVersion": 3,
453+
"ui": { "root": { "props": {} }, "content": [], "zones": {} },
454+
"operations": [],
455+
"resources": [],
456+
"variables": []
457+
},
458+
"capabilities": { "workflows": [], "resources": [] },
459+
"released": true,
460+
"createdAt": "2026-08-05T08:19:40.275Z",
461+
"workflows": [
462+
{
463+
"workflowId": "wf_abc123",
464+
"version": 4,
465+
"graphHash": "9f2c…",
466+
"graph": { "nodes": [], "edges": [] }
467+
}
468+
]
469+
}
470+
```
471+
472+
Each entry in `workflows` is the graph as the release froze it. `version` and
473+
`graph` are `null` on a snapshot published before releases pinned anything — a
474+
runtime that meets one falls back to the live workflow.
475+
476+
An app with nothing published answers `200` with a body of `null`; an app you do
477+
not own is a `404`. Publishing itself is a tRPC call (`applications.publish`),
478+
not a REST route.
479+
480+
### Downloading the Chrome Extension
481+
482+
`GET /api/extension/download` zips up the browser extension build the server can
483+
find and hands it over, so you can load it unpacked without cloning the repo.
484+
485+
```bash
486+
curl "http://localhost:7777/api/extension/download" \
487+
-H "Authorization: Bearer YOUR_TOKEN" \
488+
-o nodetool-chrome-extension.zip
489+
```
490+
491+
The response is `application/zip` with
492+
`content-disposition: attachment; filename="nodetool-chrome-extension.zip"`. The
493+
server looks for the build at `NODETOOL_EXTENSION_DIST` (set by the desktop app
494+
to its bundled copy), then walks up from its own directory and the working
495+
directory looking for `chrome-extension/dist/manifest.json`. When none of those
496+
holds a build, the answer is `404` with
497+
`{"detail": "Extension build not found"}` — build it first, per
498+
[Chrome Extension](chrome-extension.md#installing).
499+
287500
### Health Check
288501

289502
```bash

docs/chrome-extension.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ The extension icon appears in your toolbar. Click it to open the popup, which sh
6767

6868
> **Standalone package**: `chrome-extension/` is intentionally outside the root npm workspace, Turbo pipeline, and CI — it has its own `package.json` and is built on demand. `npm run build:packages` at the repo root does **not** build it.
6969
70+
### Downloading a prebuilt copy
71+
72+
A server that already has the extension built will hand it to you zipped, which saves cloning the repo on the machine running Chrome — the desktop app's install helper uses this.
73+
74+
```bash
75+
curl "http://localhost:7777/api/extension/download" -o nodetool-chrome-extension.zip
76+
unzip nodetool-chrome-extension.zip -d nodetool-extension
77+
```
78+
79+
Load `nodetool-extension/` with **Load unpacked**. A server with no build to hand out answers `404` with `{"detail": "Extension build not found"}` — build from source as above, or point the server at an existing build with `NODETOOL_EXTENSION_DIST`. See [API Reference](api-reference.md#downloading-the-chrome-extension).
80+
7081
### Development commands
7182

7283
```bash

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Security notes:
171171
| `NODETOOL_WS_DRAIN_TIMEOUT_MS` | How long a send waits for a slow reader before it is dropped | no | Default `30000`; the drop uses code `1001` so clients reconnect |
172172
| `NODETOOL_WS_MAX_QUEUED_FRAMES` | Undelivered inbound frames per connection before it is closed | no | Default `2000`; closes with code `1008` |
173173
| `NODETOOL_DISABLE_TRIGGERS` | Skip trigger ingestion on this process (no dispatcher, scheduler, file watcher, or webhook route) | no | Ingestion is **on** by default. Set to `1` when a second server shares one database, or for an embedded server that must not start background work |
174+
| `NODETOOL_EXTENSION_DIST` | Directory holding the built Chrome extension served by `/api/extension/download` | no | Set by the desktop app to its bundled copy. When unset (or pointing at a directory with no `manifest.json`), the server walks up from its own directory and the working directory looking for `chrome-extension/dist`. See [Chrome Extension](chrome-extension.md#downloading-a-prebuilt-copy) |
174175
| `LOG_LEVEL` / `NODETOOL_LOG_LEVEL` | Logging level | no | Defaults to `info` (`NODETOOL_LOG_LEVEL` takes precedence) |
175176
| `SECRETS_MASTER_KEY` | Master key for secret encryption | yes | See [Secret Storage and Master Key](#secret-storage-and-master-key) |
176177
| `RUNPOD_API_KEY` | RunPod deployments | yes | Used by CLI and providers |

0 commit comments

Comments
 (0)