scaledthought 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -4
  2. package/dist/index.js +173 -50
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -10,8 +10,9 @@ Agents: the full setup protocol is at <https://scaledthought.com/llms.txt>.
10
10
  npx -y scaledthought@latest init --region us # or: --region ca (data stays in Canada)
11
11
  ```
12
12
 
13
- `init` creates a free account (no card), writes `ST_API_KEY` and `ST_BASE_URL` to `./.env` (mode 0600),
14
- adds `.env` to `.gitignore`, and prints the account, the models live in your region and `next_steps`.
13
+ `init` creates an account, writes `ST_API_KEY` and `ST_BASE_URL` to `./.env` (mode 0600), adds `.env` to
14
+ `.gitignore`, and prints the account, its credit balance (`balance_usd`, `needs_credits`), the models live in your
15
+ region, `next_command` and `next_steps`.
15
16
  Running it again is safe. If a key already exists it returns `"already_initialized": true` and the
16
17
  account status instead of creating a second account.
17
18
 
@@ -25,6 +26,19 @@ ad platform). `account` and `account ad-measurement` print `ad_measurement` (tru
25
26
  account could be sent: a `us` account that signed up with an ad click and hasn't opted out; always false in `ca`) and
26
27
  `ad_measurement_opt_out` (the stored preference). Neither flag is a secret.
27
28
 
29
+ Inference is prepaid, and a new account has no credits: until your human pays, completions return
30
+ `402 insufficient_credits`. So the first thing after `init` is a top-up your human approves:
31
+
32
+ ```bash
33
+ npx -y scaledthought@latest billing topup --amount-usd 5 # prints approval_url and topup_id
34
+ # show your human the approval_url, exactly as printed; they pay by card
35
+ npx -y scaledthought@latest billing wait # returns once they've paid; on topup_pending run it again
36
+ ```
37
+
38
+ `billing wait` returns within about 90 seconds (`--timeout-s` to change it), under Claude Code's default 2-minute
39
+ shell timeout. Prefer it to `billing topup --wait`: an agent's shell tool usually shows a command's output only when it
40
+ ends, so the link would reach the human only after the payment it is waiting for.
41
+
28
42
  ```bash
29
43
  npx -y scaledthought@latest models --live # models serving in your region now
30
44
  npx -y scaledthought@latest chat -m auto "Reply with OK" # one real completion
@@ -33,7 +47,7 @@ npx -y scaledthought@latest account # plan, region, spend
33
47
  npx -y scaledthought@latest account ad-measurement off # opt out of ad measurement
34
48
  ```
35
49
 
36
- Other commands: `keys list|create|rotate|revoke|revoke-self` (`keys create --expires-in 30d --scopes inference --child`, `keys rotate <id> --grace 24h`), `usage`, `billing status|topup|auto-reload|transactions`, `batches …`,
50
+ Other commands: `keys list|create|rotate|revoke|revoke-self` (`keys create --expires-in 30d --scopes inference --child`, `keys rotate <id> --grace 24h`), `usage`, `billing status|topup|wait|auto-reload|transactions`, `batches …`,
37
51
  `fine-tune …`, `benchmarks <id>`, `status` (with your account's notices when a key is set), `doctor` (setup check),
38
52
  `webhooks add <url> [--events …]|list|remove <id>` (signed callbacks), `transcribe <file>` (speech to text, `--translate` for English,
39
53
  `--format srt|vtt|text|verbose_json`) and `speak -o out.mp3 "text"` (text to speech, streamed to the file; US
@@ -46,4 +60,4 @@ Use the key from any OpenAI SDK: `base_url = $ST_BASE_URL`, `api_key = $ST_API_K
46
60
  - The key's prefix (`st_us_…` / `st_ca_…`) picks the region's hosts. Requests never cross regions.
47
61
  - Errors look like `{"error":{"code":"…","message":"…","hint":"…"}}`. Exit code 2 means bad usage, 1 anything else.
48
62
 
49
- Free tier: $1/day of inference across the account, 60 requests/min per key, up to 5 keys, no card.
63
+ Before the first credit purchase an account has lower limits: 60 requests/min per key and up to 5 keys.
package/dist/index.js CHANGED
@@ -553,8 +553,8 @@ import { join } from "node:path";
553
553
  // package.json
554
554
  var package_default = {
555
555
  name: "scaledthought",
556
- version: "0.1.0",
557
- description: "Headless open-model inference, run by your agent. Free to start. US or Canada.",
556
+ version: "0.2.1",
557
+ description: "Headless open-model inference, run by your agent. Prepaid, pay per token. US or Canada.",
558
558
  license: "MIT",
559
559
  keywords: [
560
560
  "llm",
@@ -631,6 +631,7 @@ function readDotEnv(cwd) {
631
631
  }
632
632
  return out;
633
633
  }
634
+ var CREDITS_HINT = "This account has no credits for this request (inference is prepaid). Run `npx -y scaledthought@latest billing topup --amount-usd 5` (creating it charges nothing), end your turn giving your human its message_for_human (the approval_url), and once they say they've paid run `npx -y scaledthought@latest billing wait`, then retry. Don't retry before that or create another account.";
634
635
  var HINTS = {
635
636
  no_key: "Run `npx -y scaledthought@latest init --region us|ca` in this directory, or set ST_API_KEY.",
636
637
  bad_key: "ST_API_KEY must look like st_{us|ca}_{live|test}_<40 chars>. Re-run init in a directory without one.",
@@ -641,17 +642,16 @@ var HINTS = {
641
642
  expiry_exceeds_parent: "A key can't outlive the key that creates it. Pass a shorter `--expires-in`.",
642
643
  budget_exceeds_parent: "A sub-agent key's daily budget can't exceed what its parent has left today. Pass a smaller `--budget-usd`.",
643
644
  budget_exceeded: "This key's daily budget is spent. It resets at 00:00 UTC, or raise it with MCP budget_manage (accounts with credits).",
644
- insufficient_credits: "The free $1/day is used up and there aren't enough prepaid credits. Run `billing topup --amount-usd 5` and send your human the approval_url, or wait for 00:00 UTC.",
645
- free_tier_unavailable: "No free allowance right now (the first 30 days are over, or the region's free usage is used up until 00:00 UTC). Run `billing topup --amount-usd 20` and send your human the approval_url.",
646
- credits_required_for_batches: "Batches are paid from prepaid credits, not the free allowance. Run `billing topup --amount-usd 5` and send your human the approval_url, then retry.",
647
- credits_required_for_fine_tuning: "Fine-tuning is paid from prepaid credits, not the free allowance. Run `billing topup --amount-usd 5` and send your human the approval_url, then retry.",
645
+ insufficient_credits: CREDITS_HINT,
646
+ credits_required_for_batches: `Batches are paid from prepaid credits. ${CREDITS_HINT}`,
647
+ credits_required_for_fine_tuning: `Fine-tuning is paid from prepaid credits. ${CREDITS_HINT}`,
648
648
  approval_required: "This change needs your human's approval. Send them the approval_url from `billing topup`; disabling or lowering auto-reload needs none.",
649
649
  daily_cap_exceeded: "An operator-set daily cap on this account is reached. It resets at 00:00 UTC. Tell your human.",
650
- free_tier_limit: "Free accounts can't go above the free tier. Run `billing topup --amount-usd 5` for prepaid credits to lift limits.",
651
- free_tier_key_limit: "Free accounts have at most 5 active keys. Revoke one with `keys revoke <id>`, or buy credits with `billing topup`.",
650
+ free_tier_limit: "Accounts that haven't bought credits have lower limits. The first credit purchase lifts them: run `billing topup --amount-usd 5` and show your human the approval_url.",
651
+ free_tier_key_limit: "Accounts that haven't bought credits have at most 5 active keys. Revoke one with `keys revoke <id>`, or buy credits with `billing topup`.",
652
652
  too_many_pending_topups: "Two approval links are still unpaid. Ask your human to use one (see `billing status` pending_topups), or wait for them to expire.",
653
653
  daily_topup_limit: "Top-ups are limited to $2,000 per account per rolling 24 hours (open links and auto-reloads included). Use a smaller amount (see available_usd) or wait.",
654
- billing_upgrade_removed: "Run `billing topup --amount-usd 5` and send your human the approval_url.",
654
+ billing_upgrade_removed: "Run `billing topup --amount-usd 5` and show your human the approval_url.",
655
655
  gone: "This endpoint was removed. For credits use `billing topup --amount-usd 5`.",
656
656
  fine_tuning_requires_self_hosted: "This base model can't be fine-tuned in your region (it isn't served on our own GPUs). Choose another base model.",
657
657
  rate_limit_exceeded: "Wait the number of seconds in retry_after, then retry.",
@@ -756,6 +756,8 @@ async function responseError(res) {
756
756
  const retry = res.headers.get("retry-after");
757
757
  if (retry)
758
758
  extra.retry_after = Number(retry);
759
+ if (!hint && res.status === 402 && !HINTS[code])
760
+ hint = CREDITS_HINT;
759
761
  return new CliError(code, data.error?.message ?? (res.statusText || `HTTP ${res.status}`), hint, extra);
760
762
  }
761
763
  async function api(ctx, method, path, opts = {}) {
@@ -902,6 +904,7 @@ async function speak(ctx, o) {
902
904
  }
903
905
 
904
906
  // src/commands.ts
907
+ import { spawn } from "node:child_process";
905
908
  import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
906
909
  import { join as join3, resolve as resolve2 } from "node:path";
907
910
 
@@ -9220,7 +9223,7 @@ var GrowthResults = object({
9220
9223
  decisions: array(GrowthDecision)
9221
9224
  });
9222
9225
  // ../../packages/schemas/src/legal.ts
9223
- var TERMS_VERSION = "2026-10-02";
9226
+ var TERMS_VERSION = "2026-10-10";
9224
9227
  var LEGAL_SITE = "https://scaledthought.com";
9225
9228
  var LEGAL_PATHS = {
9226
9229
  index: "/legal",
@@ -9238,23 +9241,28 @@ var REFUND_POLICY_URL = `${LEGAL_SITE}${LEGAL_PATHS.creditsAndRefunds}`;
9238
9241
  var TERMS_NOTICE = `By creating this account you accept the ScaledThought Terms of Service (${TERMS_URL}) and Privacy Policy ` + `(${PRIVACY_URL}) on behalf of the person or organisation you act for, who is the customer.`;
9239
9242
 
9240
9243
  // src/commands.ts
9241
- var FREE_TIER = {
9242
- usd_per_day: 1,
9243
- allowance_days: 30,
9244
- requests_per_min: 60,
9245
- max_keys: 5,
9246
- card_required: false
9247
- };
9248
9244
  var NPX = "npx -y scaledthought@latest";
9249
- function nextSteps(model) {
9245
+ var FIRST_TOPUP_USD = 5;
9246
+ var TOPUP_COMMAND = `${NPX} billing topup --amount-usd ${FIRST_TOPUP_USD}`;
9247
+ var WAIT_COMMAND = `${NPX} billing wait`;
9248
+ var CREDIT_STEPS = [
9249
+ `Inference is prepaid and this account has no credits yet: completions return 402 insufficient_credits until your human pays. Do these first, in order:`,
9250
+ `1. Run: ${TOPUP_COMMAND}. It opens the checkout page in your human's browser and prints approval_url, topup_id and message_for_human. Creating it charges nothing: don't ask first.`,
9251
+ `2. End your turn with message_for_human (it says the checkout page is open and holds the approval_url exactly as printed, in case it didn't open): the one-time payment page where your human accepts the Terms and pays $${FIRST_TOPUP_USD} by card. Don't run anything that waits before they have it, don't retry the call and don't create another account.`,
9252
+ `3. When they say they've paid, run: ${WAIT_COMMAND}. It returns status "paid" with the new balance (at once if already paid); topup_pending means not yet.`,
9253
+ "4. Then make the call below."
9254
+ ];
9255
+ function nextSteps(model, needsCredits) {
9250
9256
  return [
9257
+ ...needsCredits ? CREDIT_STEPS : [],
9251
9258
  model ? `Verify with one real completion: ${NPX} chat -m ${model} "Reply with OK" (or POST $ST_BASE_URL/chat/completions with that model)` : `No chat model is live in this region right now. Check again with: ${NPX} models --live`,
9252
9259
  `Or let the router pick: ${NPX} chat -m auto "Reply with OK"`,
9253
9260
  "Use it from code: any OpenAI SDK with base_url=$ST_BASE_URL and api_key=$ST_API_KEY (both are in .env)",
9254
9261
  `Optional, connect MCP: ${NPX} mcp`,
9255
- `Past the free $${FREE_TIER.usd_per_day}/day (for the first ${FREE_TIER.allowance_days} days): ${NPX} billing topup --amount-usd 5 returns an approval_url for your human to buy prepaid credits`
9262
+ ...needsCredits ? [] : [`More credits when needed: ${TOPUP_COMMAND} returns an approval_url for your human (inference is prepaid)`]
9256
9263
  ];
9257
9264
  }
9265
+ var balanceOf = (b) => b && typeof b.balance_usd === "number" ? b.balance_usd : null;
9258
9266
  function termsOf(acct = {}) {
9259
9267
  return {
9260
9268
  url: acct.terms_url ?? TERMS_URL,
@@ -9263,10 +9271,18 @@ function termsOf(acct = {}) {
9263
9271
  notice: acct.terms_notice ?? TERMS_NOTICE
9264
9272
  };
9265
9273
  }
9266
- async function liveSummary(ctx, region) {
9274
+ async function liveSummary(ctx, region, balanceUsd) {
9267
9275
  const live = await listModels(ctx, { live: true, region });
9268
9276
  const suggested = live.find((m) => m.category !== "embedding" && m.modality === "chat")?.id ?? null;
9269
- return { newest_models: live.slice(0, 5), suggested_model: suggested, next_steps: nextSteps(suggested) };
9277
+ const needsCredits = (balanceUsd ?? 0) <= 0;
9278
+ return {
9279
+ balance_usd: balanceUsd ?? 0,
9280
+ needs_credits: needsCredits,
9281
+ next_command: needsCredits ? TOPUP_COMMAND : `${NPX} chat -m ${suggested ?? "auto"} "Reply with OK"`,
9282
+ newest_models: live.slice(0, 5),
9283
+ suggested_model: suggested,
9284
+ next_steps: nextSteps(suggested, needsCredits)
9285
+ };
9270
9286
  }
9271
9287
  function signupAttribution(region, opts) {
9272
9288
  const warnings = [];
@@ -9302,13 +9318,13 @@ async function init(ctx, opts) {
9302
9318
  throw new CliError("region_mismatch", `A ${region} key already exists (${where}) but --region ${opts.region} was requested. Nothing was created.`, `Accounts are pinned to one region. Use the existing ${region} account, or run init in a directory with no ST_API_KEY (and no ST_API_KEY in the environment) to create a separate ${opts.region} account.`, { existing_region: region, requested_region: opts.region });
9303
9319
  const status = await account(ctx);
9304
9320
  const ignored = [opts.source !== undefined && "--source", opts.adClick !== undefined && "--ad-click"].filter(Boolean);
9321
+ const shown = withoutFreeFields(status);
9305
9322
  return {
9306
9323
  already_initialized: true,
9307
- ...status,
9324
+ ...shown,
9308
9325
  key_saved_to: where,
9309
- free_tier: FREE_TIER,
9310
9326
  terms: termsOf(),
9311
- ...await liveSummary(ctx, region),
9327
+ ...await liveSummary(ctx, region, balanceOf(status)),
9312
9328
  ...ignored.length ? { warnings: [`${ignored.join(" and ")} apply only to a new account and were not sent.`] } : {}
9313
9329
  };
9314
9330
  }
@@ -9328,17 +9344,16 @@ async function init(ctx, opts) {
9328
9344
  writeEnv(ctx.cwd, { ST_API_KEY: acct.api_key, ST_BASE_URL: acct.base_url });
9329
9345
  ensureGitignored(ctx.cwd);
9330
9346
  const keyed = { ...ctx, env: { ...ctx.env, ST_API_KEY: acct.api_key } };
9347
+ const billing = await api(keyed, "GET", "/v1/billing").catch(() => null);
9331
9348
  return {
9332
9349
  already_initialized: false,
9333
9350
  account_id: acct.account_id,
9334
9351
  region: acct.region,
9335
- plan: "free",
9336
9352
  base_url: acct.base_url,
9337
9353
  key_saved_to: ".env (ST_API_KEY, ST_BASE_URL)",
9338
- free_tier: FREE_TIER,
9339
9354
  terms: termsOf(acct),
9340
9355
  ...attribution.source ? { source: acct.source ?? attribution.source } : {},
9341
- ...await liveSummary(keyed, acct.region),
9356
+ ...await liveSummary(keyed, acct.region, balanceOf(billing)),
9342
9357
  ...attribution.warnings.length ? { warnings: attribution.warnings } : {}
9343
9358
  };
9344
9359
  }
@@ -9409,9 +9424,20 @@ function unitPrices(p) {
9409
9424
  };
9410
9425
  }
9411
9426
  var showModel = (ctx, id, region) => api(ctx, "GET", `/v1/models/${encodeURIComponent(id)}`, { auth: false, region: publicRegion(ctx, region) });
9427
+ function withoutFreeFields(o) {
9428
+ const out = {};
9429
+ for (const [k, v] of Object.entries(o)) {
9430
+ if (/^free_/.test(k))
9431
+ continue;
9432
+ if (k === "topup" && typeof v === "string" && /free/i.test(v))
9433
+ continue;
9434
+ out[k] = v;
9435
+ }
9436
+ return out;
9437
+ }
9412
9438
  async function account(ctx) {
9413
- const a = await api(ctx, "GET", "/v1/account");
9414
- const fields = ["balance_usd", "free_remaining_today_usd", "auto_reload"];
9439
+ const a = withoutFreeFields(await api(ctx, "GET", "/v1/account"));
9440
+ const fields = ["balance_usd", "auto_reload"];
9415
9441
  if (fields.every((f) => (f in a)))
9416
9442
  return a;
9417
9443
  const b = await api(ctx, "GET", "/v1/billing").catch(() => null);
@@ -9512,7 +9538,7 @@ async function chat(ctx, o) {
9512
9538
  usage: usageOut
9513
9539
  };
9514
9540
  }
9515
- var billingStatus = (ctx) => api(ctx, "GET", "/v1/billing");
9541
+ var billingStatus = async (ctx) => withoutFreeFields(await api(ctx, "GET", "/v1/billing"));
9516
9542
  var TOPUP_LIMITS = { min_usd: 5, max_usd: 2000, daily_max_usd: 2000, auto_reload_monthly_max_usd: 1e4 };
9517
9543
  function checkTopupLimits(amountUsd, autoReload) {
9518
9544
  const { min_usd, max_usd, daily_max_usd, auto_reload_monthly_max_usd } = TOPUP_LIMITS;
@@ -9529,20 +9555,91 @@ function checkTopupLimits(amountUsd, autoReload) {
9529
9555
  range("auto-reload-monthly-max", autoReload.monthly_limit_usd, autoReload.amount_usd, auto_reload_monthly_max_usd);
9530
9556
  }
9531
9557
  var TOPUP_DONE = new Set(["paid", "expired", "failed", "canceled"]);
9532
- function topupNextSteps(amountUsd, autoReload) {
9558
+ function topupNextSteps(topupId, amountUsd, opened, autoReload) {
9533
9559
  return [
9534
- `Send approval_url to your human. They pay $${amountUsd} by card; credits are added once the payment is confirmed.`,
9560
+ ...opened ? ["The checkout page is already open in your human's browser: don't open it again or paste the link on its own."] : [],
9561
+ `Give your human message_for_human now and end your turn there: it holds the approval_url exactly as printed (a one-time payment link; don't shorten or reformat it). They accept the Terms and the Credits & Refunds policy and pay $${amountUsd} by card; credits are added once the payment is confirmed. Nothing is charged without their approval. Don't run billing wait (or anything else that waits) before they have the link: they may not see your messages until your turn ends.`,
9535
9562
  ...autoReload ? [
9536
9563
  `Approving also saves the card for auto-reload: $${autoReload.amount_usd} whenever the balance drops below $${autoReload.threshold_usd}${autoReload.monthly_limit_usd ? `, at most $${autoReload.monthly_limit_usd} a month` : ""}.`
9537
9564
  ] : [],
9538
- `Check with: ${NPX} billing status (balance_usd, pending_topups). Nothing is charged without the human's approval.`
9565
+ `When they say they've paid: ${WAIT_COMMAND}${topupId ? ` ${topupId}` : ""} (returns status "paid" with the balance, at once if already paid; topup_pending means not yet: tell them, don't make a new link).`,
9566
+ "Once paid, retry the call that returned 402. Don't create another top-up while this one is pending: the link stays valid until expires_at."
9539
9567
  ];
9540
9568
  }
9569
+ function openInBrowser(url) {
9570
+ if (!/^https:\/\//.test(url))
9571
+ return Promise.resolve(false);
9572
+ const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
9573
+ return new Promise((done) => {
9574
+ try {
9575
+ const child = spawn(bin, args, { detached: true, stdio: "ignore", windowsHide: true });
9576
+ child.once("error", () => done(false));
9577
+ child.once("spawn", () => {
9578
+ child.unref();
9579
+ done(true);
9580
+ });
9581
+ } catch {
9582
+ done(false);
9583
+ }
9584
+ });
9585
+ }
9541
9586
  async function billingTopup(ctx, o) {
9542
9587
  const created = await api(ctx, "POST", "/v1/billing/topup", {
9543
9588
  body: { amount_usd: o.amountUsd, ...o.autoReload ? { auto_reload: o.autoReload } : {} }
9544
9589
  });
9545
- return { status: "pending", ...created, next_steps: topupNextSteps(o.amountUsd, o.autoReload) };
9590
+ const noOpen = o.noOpen || ctx.env.ST_NO_BROWSER === "1";
9591
+ const opened = !noOpen && !!created.approval_url && await (o.open ?? openInBrowser)(created.approval_url);
9592
+ return {
9593
+ status: "pending",
9594
+ ...created,
9595
+ opened_in_browser: opened,
9596
+ message_for_human: messageForHuman(created.approval_url, o.amountUsd, o.autoReload, opened),
9597
+ next_steps: topupNextSteps(created.topup_id, o.amountUsd, opened, o.autoReload)
9598
+ };
9599
+ }
9600
+ function messageForHuman(approvalUrl, amountUsd, autoReload, opened = false) {
9601
+ if (!approvalUrl)
9602
+ return null;
9603
+ const reload = autoReload ? ` It also turns on auto-reload: $${autoReload.amount_usd} whenever the balance drops below $${autoReload.threshold_usd}${autoReload.monthly_limit_usd ? `, at most $${autoReload.monthly_limit_usd} a month` : ""}.` : "";
9604
+ if (opened)
9605
+ return `To finish the setup, I've opened the ScaledThought checkout page in your browser to buy $${amountUsd} of prepaid credits (USD). If it didn't open, use this link:
9606
+
9607
+ ${approvalUrl}
9608
+
9609
+ On that page you accept the Terms of Service and the Credits & Refunds policy and pay by card. Nothing is charged until you approve.${reload} Tell me when it's done and I'll continue.`;
9610
+ return `To finish the setup, please buy $${amountUsd} of prepaid ScaledThought credits (USD) here:
9611
+
9612
+ ${approvalUrl}
9613
+
9614
+ On that page you accept the Terms of Service and the Credits & Refunds policy and pay by card. Nothing is charged until you approve.${reload} Tell me when it's done and I'll continue.`;
9615
+ }
9616
+ var WAIT_DEFAULT_S = 90;
9617
+ async function billingWait(ctx, o = {}) {
9618
+ let id = o.topupId;
9619
+ if (id !== undefined && !/^top_[0-9A-Za-z]+$/.test(id))
9620
+ throw new CliError("bad_argument", `"${id}" is not a top-up id (top_...).`, "Use topup_id from `billing topup`.");
9621
+ if (!id) {
9622
+ const b = await api(ctx, "GET", "/v1/billing");
9623
+ const pending = (b.pending_topups ?? []).filter((t) => t.status === "pending" && t.approval_url);
9624
+ id = pending[0]?.topup_id;
9625
+ if (!id) {
9626
+ const paid = (b.recent_transactions ?? []).find((t) => t.topup_id)?.topup_id ?? null;
9627
+ if ((b.balance_usd ?? 0) > 0)
9628
+ return { status: "paid", topup_id: paid, billing: withoutFreeFields(b) };
9629
+ throw new CliError("no_pending_topup", "There is no top-up waiting for payment and the balance is $0.", `Run \`${TOPUP_COMMAND}\` and show your human the approval_url, then run \`${WAIT_COMMAND}\`.`);
9630
+ }
9631
+ }
9632
+ const deadline = Date.now() + (o.timeoutS ?? WAIT_DEFAULT_S) * 1000;
9633
+ for (;; ) {
9634
+ const t = await api(ctx, "GET", `/v1/billing/topups/${encodeURIComponent(id)}`);
9635
+ if (t.status === "paid")
9636
+ return { status: "paid", topup_id: id, billing: await billingStatus(ctx) };
9637
+ if (TOPUP_DONE.has(t.status ?? ""))
9638
+ throw new CliError(`topup_${t.status}`, t.status === "expired" ? `Top-up ${id} expired before it was paid.` : `Top-up ${id} was not paid (${t.status}${t.failure_code ? `: ${t.failure_code}` : ""}).`, `Run \`${TOPUP_COMMAND}\` again and show your human the new approval_url.`, { topup_id: id });
9639
+ if (Date.now() >= deadline)
9640
+ throw new CliError("topup_pending", `Top-up ${id} isn't paid yet.`, `Not paid yet. If your human doesn't have the approval_url (below) yet, end your turn now and give it to them. Otherwise tell them it's still unpaid and run \`${WAIT_COMMAND} ${id}\` again once they say they've paid. Don't create a new top-up.`, { topup_id: id, approval_url: t.approval_url ?? null });
9641
+ await new Promise((r) => setTimeout(r, o.pollMs ?? 3000));
9642
+ }
9546
9643
  }
9547
9644
  async function billingTopupAndWait(ctx, o) {
9548
9645
  const created = await billingTopup(ctx, o);
@@ -9734,13 +9831,10 @@ async function doctor(ctx) {
9734
9831
  next: err.hint ?? "Fix the error above, then run doctor again."
9735
9832
  };
9736
9833
  }
9737
- checks.push({ check: "account", ok: a.status === "active", detail: `${a.account_id} (${a.plan}, ${a.status})` });
9738
- const credit = (a.balance_usd ?? 0) > 0 || (a.free_remaining_today_usd ?? 0) > 0;
9739
- checks.push({
9740
- check: "credit",
9741
- ok: credit,
9742
- detail: `balance $${a.balance_usd ?? 0}, free left today $${a.free_remaining_today_usd ?? 0}`
9743
- });
9834
+ const purchased = a.plan === "paid" ? "credits bought" : "no credit purchase yet";
9835
+ checks.push({ check: "account", ok: a.status === "active", detail: `${a.account_id} (${purchased}, ${a.status})` });
9836
+ const credit = (a.balance_usd ?? 0) > 0;
9837
+ checks.push({ check: "credit", ok: credit, detail: `balance $${a.balance_usd ?? 0}` });
9744
9838
  const notices = a.notices ?? [];
9745
9839
  return {
9746
9840
  ok: checks.every((c) => c.ok) && notices.length === 0,
@@ -9750,10 +9844,10 @@ async function doctor(ctx) {
9750
9844
  checks,
9751
9845
  notices,
9752
9846
  notice_text: a.notice_text ?? [],
9753
- next: notices.length ? "Tell your human about each notice (notice_text)." : credit ? null : "No credit: run `npx scaledthought billing topup --amount-usd 20` and send your human the approval_url."
9847
+ next: notices.length ? "Tell your human about each notice (notice_text)." : credit ? null : `No credits: run \`${TOPUP_COMMAND}\`, show your human the approval_url, then run \`${WAIT_COMMAND}\`.`
9754
9848
  };
9755
9849
  }
9756
- var WEBHOOK_EVENTS = ["balance.low", "free_allowance.ending", "key.expiring", "budget.exhausted"];
9850
+ var WEBHOOK_EVENTS = ["balance.low", "key.expiring", "budget.exhausted"];
9757
9851
  async function webhooksAdd(ctx, o) {
9758
9852
  if (!/^https:\/\//i.test(o.url))
9759
9853
  throw new CliError("bad_argument", "The webhook URL must be https://.", "Pass a public https endpoint.");
@@ -10108,7 +10202,7 @@ function num(flag, v) {
10108
10202
  var init2 = defineCommand({
10109
10203
  meta: {
10110
10204
  name: "init",
10111
- description: "Create a free account (no card), save ST_API_KEY and ST_BASE_URL to ./.env, list live models. Idempotent. " + "Creating an account accepts the Terms of Service (scaledthought.com/legal/terms) for the human you act for."
10205
+ description: "Create an account, save ST_API_KEY and ST_BASE_URL to ./.env, list live models and the next steps (a new account has no credits: top up first). Idempotent. " + "Creating an account accepts the Terms of Service (scaledthought.com/legal/terms) for the human you act for."
10112
10206
  },
10113
10207
  args: {
10114
10208
  ...jsonFlag,
@@ -10163,7 +10257,7 @@ var models = defineCommand({
10163
10257
  });
10164
10258
  var ACCOUNT_SUBCOMMANDS = new Set(["delete", "ad-measurement"]);
10165
10259
  var account2 = defineCommand({
10166
- meta: { name: "account", description: "Region, spend, credit balance and free allowance left today" },
10260
+ meta: { name: "account", description: "Region, spend, credit balance and auto-reload" },
10167
10261
  args: jsonFlag,
10168
10262
  run: ({ args, rawArgs }) => {
10169
10263
  if (rawArgs.some((a) => ACCOUNT_SUBCOMMANDS.has(a)))
@@ -10419,7 +10513,7 @@ var billing = defineCommand({
10419
10513
  status: defineCommand({
10420
10514
  meta: {
10421
10515
  name: "status",
10422
- description: "Balance, free allowance left today, auto-reload, pending top-ups, recent transactions"
10516
+ description: "Credit balance, auto-reload, pending top-ups (with their approval links), recent transactions"
10423
10517
  },
10424
10518
  args: jsonFlag,
10425
10519
  run: ({ args }) => run(() => billingStatus(makeCtx()), args)
@@ -10427,7 +10521,7 @@ var billing = defineCommand({
10427
10521
  topup: defineCommand({
10428
10522
  meta: {
10429
10523
  name: "topup",
10430
- description: "Buy prepaid credits: returns an approval_url for your human. Optional auto-reload"
10524
+ description: "Buy prepaid credits: opens the checkout page in your human's browser (approval_url), then run `billing wait`. Optional auto-reload"
10431
10525
  },
10432
10526
  args: {
10433
10527
  ...jsonFlag,
@@ -10442,13 +10536,42 @@ var billing = defineCommand({
10442
10536
  type: "string",
10443
10537
  description: "Most auto-reload may charge per calendar month (USD, default 5x the amount)"
10444
10538
  },
10445
- wait: { type: "boolean", description: "Poll until the human pays or the link expires or fails" }
10539
+ open: {
10540
+ type: "boolean",
10541
+ default: true,
10542
+ description: "Open the checkout page in the browser (--no-open only prints approval_url)"
10543
+ },
10544
+ wait: {
10545
+ type: "boolean",
10546
+ description: "Poll until the human pays or the link expires or fails. Only if your human sees your terminal live: an agent's shell tool shows the link only after the command ends. Prefer `billing wait`"
10547
+ }
10446
10548
  },
10447
10549
  run: async ({ args }) => {
10448
10550
  const amountUsd = num("amount-usd", args["amount-usd"]);
10449
10551
  const autoReload = autoReloadArgs(args);
10450
10552
  checkTopupLimits(amountUsd, autoReload);
10451
- await run(() => args.wait ? billingTopupAndWait(makeCtx(), { amountUsd, autoReload, onPending: (t) => out(t, args) }) : billingTopup(makeCtx(), { amountUsd, autoReload }), args);
10553
+ const noOpen = args.open === false;
10554
+ await run(() => args.wait ? billingTopupAndWait(makeCtx(), { amountUsd, autoReload, noOpen, onPending: (t) => out(t, args) }) : billingTopup(makeCtx(), { amountUsd, autoReload, noOpen }), args);
10555
+ }
10556
+ }),
10557
+ wait: defineCommand({
10558
+ meta: {
10559
+ name: "wait",
10560
+ description: "Wait until your human pays a top-up you showed them (default: the newest pending one). Returns status paid; exits topup_pending after --timeout-s (run it again)"
10561
+ },
10562
+ args: {
10563
+ ...jsonFlag,
10564
+ topup_id: { type: "positional", required: false, description: "topup_id from `billing topup` (optional)" },
10565
+ "timeout-s": {
10566
+ type: "string",
10567
+ description: `Seconds to wait before exiting with topup_pending (default ${WAIT_DEFAULT_S}, at most 600)`
10568
+ }
10569
+ },
10570
+ run: async ({ args }) => {
10571
+ const t = args["timeout-s"] === undefined ? undefined : num("timeout-s", args["timeout-s"]);
10572
+ if (t !== undefined && t > 600)
10573
+ throw new CliError("bad_argument", `--timeout-s must be at most 600, got ${t}.`, "Run wait again instead.");
10574
+ await run(() => billingWait(makeCtx(), { topupId: args.topup_id || undefined, timeoutS: t }), args);
10452
10575
  }
10453
10576
  }),
10454
10577
  "auto-reload": defineCommand({
@@ -10644,7 +10767,7 @@ var doctor2 = defineCommand({
10644
10767
  var webhooks = defineCommand({
10645
10768
  meta: {
10646
10769
  name: "webhooks",
10647
- description: "Signed HTTPS callbacks: balance.low, free_allowance.ending, key.expiring, budget.exhausted"
10770
+ description: "Signed HTTPS callbacks: balance.low, key.expiring, budget.exhausted"
10648
10771
  },
10649
10772
  subCommands: {
10650
10773
  add: defineCommand({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scaledthought",
3
- "version": "0.1.0",
4
- "description": "Headless open-model inference, run by your agent. Free to start. US or Canada.",
3
+ "version": "0.2.1",
4
+ "description": "Headless open-model inference, run by your agent. Prepaid, pay per token. US or Canada.",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "llm",