scaledthought 0.1.0 → 0.2.2

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 +179 -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.2",
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) and give your human its message_for_human. If it printed opened_in_browser: true, run `npx -y scaledthought@latest billing wait` in the same turn (again on topup_pending, up to 15 minutes): it returns as soon as they pay. Otherwise end your turn with the message and run billing wait once they're back. 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
 
@@ -8961,6 +8964,8 @@ var EXPERIMENT_METRICS = [
8961
8964
  "signups",
8962
8965
  "activation_rate",
8963
8966
  "paid_rate",
8967
+ "topup_request_rate",
8968
+ "topup_completion_rate",
8964
8969
  "cost_per_activated",
8965
8970
  "cost_per_payer",
8966
8971
  "onboarding_success_rate",
@@ -9220,7 +9225,7 @@ var GrowthResults = object({
9220
9225
  decisions: array(GrowthDecision)
9221
9226
  });
9222
9227
  // ../../packages/schemas/src/legal.ts
9223
- var TERMS_VERSION = "2026-10-02";
9228
+ var TERMS_VERSION = "2026-10-10";
9224
9229
  var LEGAL_SITE = "https://scaledthought.com";
9225
9230
  var LEGAL_PATHS = {
9226
9231
  index: "/legal",
@@ -9238,23 +9243,29 @@ var REFUND_POLICY_URL = `${LEGAL_SITE}${LEGAL_PATHS.creditsAndRefunds}`;
9238
9243
  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
9244
 
9240
9245
  // 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
9246
  var NPX = "npx -y scaledthought@latest";
9249
- function nextSteps(model) {
9247
+ var FIRST_TOPUP_USD = 5;
9248
+ var TOPUP_COMMAND = `${NPX} billing topup --amount-usd ${FIRST_TOPUP_USD}`;
9249
+ var WAIT_COMMAND = `${NPX} billing wait`;
9250
+ var WAIT_PATIENCE_MIN = 15;
9251
+ var CREDIT_STEPS = [
9252
+ `Inference is prepaid and this account has no credits yet: completions return 402 insufficient_credits until your human pays. Do these first, in order:`,
9253
+ `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.`,
9254
+ `2. If opened_in_browser is true, the checkout page (where your human accepts the Terms and pays $${FIRST_TOPUP_USD} by card) is already in front of them: tell them so with message_for_human and, in the same turn, run: ${WAIT_COMMAND}. It returns status "paid" with the new balance as soon as they pay; on topup_pending run it again, for up to ${WAIT_PATIENCE_MIN} minutes in all. Don't wait for them to say they've paid.`,
9255
+ `3. If opened_in_browser is false (or it's still unpaid after ${WAIT_PATIENCE_MIN} minutes), end your turn with message_for_human: it holds the approval_url exactly as printed. When they're back, run ${WAIT_COMMAND}. Either way, don't retry the call and don't create another account.`,
9256
+ "4. Then make the call below."
9257
+ ];
9258
+ function nextSteps(model, needsCredits) {
9250
9259
  return [
9260
+ ...needsCredits ? CREDIT_STEPS : [],
9251
9261
  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
9262
  `Or let the router pick: ${NPX} chat -m auto "Reply with OK"`,
9253
9263
  "Use it from code: any OpenAI SDK with base_url=$ST_BASE_URL and api_key=$ST_API_KEY (both are in .env)",
9254
9264
  `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`
9265
+ ...needsCredits ? [] : [`More credits when needed: ${TOPUP_COMMAND} returns an approval_url for your human (inference is prepaid)`]
9256
9266
  ];
9257
9267
  }
9268
+ var balanceOf = (b) => b && typeof b.balance_usd === "number" ? b.balance_usd : null;
9258
9269
  function termsOf(acct = {}) {
9259
9270
  return {
9260
9271
  url: acct.terms_url ?? TERMS_URL,
@@ -9263,10 +9274,18 @@ function termsOf(acct = {}) {
9263
9274
  notice: acct.terms_notice ?? TERMS_NOTICE
9264
9275
  };
9265
9276
  }
9266
- async function liveSummary(ctx, region) {
9277
+ async function liveSummary(ctx, region, balanceUsd) {
9267
9278
  const live = await listModels(ctx, { live: true, region });
9268
9279
  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) };
9280
+ const needsCredits = (balanceUsd ?? 0) <= 0;
9281
+ return {
9282
+ balance_usd: balanceUsd ?? 0,
9283
+ needs_credits: needsCredits,
9284
+ next_command: needsCredits ? TOPUP_COMMAND : `${NPX} chat -m ${suggested ?? "auto"} "Reply with OK"`,
9285
+ newest_models: live.slice(0, 5),
9286
+ suggested_model: suggested,
9287
+ next_steps: nextSteps(suggested, needsCredits)
9288
+ };
9270
9289
  }
9271
9290
  function signupAttribution(region, opts) {
9272
9291
  const warnings = [];
@@ -9302,13 +9321,13 @@ async function init(ctx, opts) {
9302
9321
  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
9322
  const status = await account(ctx);
9304
9323
  const ignored = [opts.source !== undefined && "--source", opts.adClick !== undefined && "--ad-click"].filter(Boolean);
9324
+ const shown = withoutFreeFields(status);
9305
9325
  return {
9306
9326
  already_initialized: true,
9307
- ...status,
9327
+ ...shown,
9308
9328
  key_saved_to: where,
9309
- free_tier: FREE_TIER,
9310
9329
  terms: termsOf(),
9311
- ...await liveSummary(ctx, region),
9330
+ ...await liveSummary(ctx, region, balanceOf(status)),
9312
9331
  ...ignored.length ? { warnings: [`${ignored.join(" and ")} apply only to a new account and were not sent.`] } : {}
9313
9332
  };
9314
9333
  }
@@ -9328,17 +9347,16 @@ async function init(ctx, opts) {
9328
9347
  writeEnv(ctx.cwd, { ST_API_KEY: acct.api_key, ST_BASE_URL: acct.base_url });
9329
9348
  ensureGitignored(ctx.cwd);
9330
9349
  const keyed = { ...ctx, env: { ...ctx.env, ST_API_KEY: acct.api_key } };
9350
+ const billing = await api(keyed, "GET", "/v1/billing").catch(() => null);
9331
9351
  return {
9332
9352
  already_initialized: false,
9333
9353
  account_id: acct.account_id,
9334
9354
  region: acct.region,
9335
- plan: "free",
9336
9355
  base_url: acct.base_url,
9337
9356
  key_saved_to: ".env (ST_API_KEY, ST_BASE_URL)",
9338
- free_tier: FREE_TIER,
9339
9357
  terms: termsOf(acct),
9340
9358
  ...attribution.source ? { source: acct.source ?? attribution.source } : {},
9341
- ...await liveSummary(keyed, acct.region),
9359
+ ...await liveSummary(keyed, acct.region, balanceOf(billing)),
9342
9360
  ...attribution.warnings.length ? { warnings: attribution.warnings } : {}
9343
9361
  };
9344
9362
  }
@@ -9409,9 +9427,20 @@ function unitPrices(p) {
9409
9427
  };
9410
9428
  }
9411
9429
  var showModel = (ctx, id, region) => api(ctx, "GET", `/v1/models/${encodeURIComponent(id)}`, { auth: false, region: publicRegion(ctx, region) });
9430
+ function withoutFreeFields(o) {
9431
+ const out = {};
9432
+ for (const [k, v] of Object.entries(o)) {
9433
+ if (/^free_/.test(k))
9434
+ continue;
9435
+ if (k === "topup" && typeof v === "string" && /free/i.test(v))
9436
+ continue;
9437
+ out[k] = v;
9438
+ }
9439
+ return out;
9440
+ }
9412
9441
  async function account(ctx) {
9413
- const a = await api(ctx, "GET", "/v1/account");
9414
- const fields = ["balance_usd", "free_remaining_today_usd", "auto_reload"];
9442
+ const a = withoutFreeFields(await api(ctx, "GET", "/v1/account"));
9443
+ const fields = ["balance_usd", "auto_reload"];
9415
9444
  if (fields.every((f) => (f in a)))
9416
9445
  return a;
9417
9446
  const b = await api(ctx, "GET", "/v1/billing").catch(() => null);
@@ -9512,7 +9541,7 @@ async function chat(ctx, o) {
9512
9541
  usage: usageOut
9513
9542
  };
9514
9543
  }
9515
- var billingStatus = (ctx) => api(ctx, "GET", "/v1/billing");
9544
+ var billingStatus = async (ctx) => withoutFreeFields(await api(ctx, "GET", "/v1/billing"));
9516
9545
  var TOPUP_LIMITS = { min_usd: 5, max_usd: 2000, daily_max_usd: 2000, auto_reload_monthly_max_usd: 1e4 };
9517
9546
  function checkTopupLimits(amountUsd, autoReload) {
9518
9547
  const { min_usd, max_usd, daily_max_usd, auto_reload_monthly_max_usd } = TOPUP_LIMITS;
@@ -9529,20 +9558,94 @@ function checkTopupLimits(amountUsd, autoReload) {
9529
9558
  range("auto-reload-monthly-max", autoReload.monthly_limit_usd, autoReload.amount_usd, auto_reload_monthly_max_usd);
9530
9559
  }
9531
9560
  var TOPUP_DONE = new Set(["paid", "expired", "failed", "canceled"]);
9532
- function topupNextSteps(amountUsd, autoReload) {
9561
+ function topupNextSteps(topupId, amountUsd, opened, autoReload) {
9562
+ const wait = `${WAIT_COMMAND}${topupId ? ` ${topupId}` : ""}`;
9533
9563
  return [
9534
- `Send approval_url to your human. They pay $${amountUsd} by card; credits are added once the payment is confirmed.`,
9564
+ opened ? `The checkout page is already open in your human's browser: don't open it again. Tell them with message_for_human (it holds the approval_url in case it didn't open), then in the same turn run: ${wait}. It returns status "paid" with the balance as soon as they pay; on topup_pending run it again, for up to ${WAIT_PATIENCE_MIN} minutes in all. Don't wait for them to say they've paid. If it's still unpaid after that, end your turn with message_for_human.` : `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). 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.`,
9565
+ `On that page 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.`,
9535
9566
  ...autoReload ? [
9536
9567
  `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
9568
  ] : [],
9538
- `Check with: ${NPX} billing status (balance_usd, pending_topups). Nothing is charged without the human's approval.`
9569
+ ...opened ? [] : [
9570
+ `When they say they've paid: ${wait} (returns status "paid" with the balance, at once if already paid; topup_pending means not yet: tell them, don't make a new link).`
9571
+ ],
9572
+ "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
9573
  ];
9540
9574
  }
9575
+ function openInBrowser(url) {
9576
+ if (!/^https:\/\//.test(url))
9577
+ return Promise.resolve(false);
9578
+ const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
9579
+ return new Promise((done) => {
9580
+ try {
9581
+ const child = spawn(bin, args, { detached: true, stdio: "ignore", windowsHide: true });
9582
+ child.once("error", () => done(false));
9583
+ child.once("spawn", () => {
9584
+ child.unref();
9585
+ done(true);
9586
+ });
9587
+ } catch {
9588
+ done(false);
9589
+ }
9590
+ });
9591
+ }
9541
9592
  async function billingTopup(ctx, o) {
9542
9593
  const created = await api(ctx, "POST", "/v1/billing/topup", {
9543
9594
  body: { amount_usd: o.amountUsd, ...o.autoReload ? { auto_reload: o.autoReload } : {} }
9544
9595
  });
9545
- return { status: "pending", ...created, next_steps: topupNextSteps(o.amountUsd, o.autoReload) };
9596
+ const noOpen = o.noOpen || ctx.env.ST_NO_BROWSER === "1";
9597
+ const opened = !noOpen && !!created.approval_url && await (o.open ?? openInBrowser)(created.approval_url);
9598
+ return {
9599
+ status: "pending",
9600
+ ...created,
9601
+ opened_in_browser: opened,
9602
+ message_for_human: messageForHuman(created.approval_url, o.amountUsd, o.autoReload, opened),
9603
+ next_steps: topupNextSteps(created.topup_id, o.amountUsd, opened, o.autoReload)
9604
+ };
9605
+ }
9606
+ function messageForHuman(approvalUrl, amountUsd, autoReload, opened = false) {
9607
+ if (!approvalUrl)
9608
+ return null;
9609
+ 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` : ""}.` : "";
9610
+ if (opened)
9611
+ 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:
9612
+
9613
+ ${approvalUrl}
9614
+
9615
+ 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} I'm watching for the payment and will carry on as soon as it goes through.`;
9616
+ return `To finish the setup, please buy $${amountUsd} of prepaid ScaledThought credits (USD) here:
9617
+
9618
+ ${approvalUrl}
9619
+
9620
+ 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.`;
9621
+ }
9622
+ var WAIT_DEFAULT_S = 90;
9623
+ async function billingWait(ctx, o = {}) {
9624
+ let id = o.topupId;
9625
+ if (id !== undefined && !/^top_[0-9A-Za-z]+$/.test(id))
9626
+ throw new CliError("bad_argument", `"${id}" is not a top-up id (top_...).`, "Use topup_id from `billing topup`.");
9627
+ if (!id) {
9628
+ const b = await api(ctx, "GET", "/v1/billing");
9629
+ const pending = (b.pending_topups ?? []).filter((t) => t.status === "pending" && t.approval_url);
9630
+ id = pending[0]?.topup_id;
9631
+ if (!id) {
9632
+ const paid = (b.recent_transactions ?? []).find((t) => t.topup_id)?.topup_id ?? null;
9633
+ if ((b.balance_usd ?? 0) > 0)
9634
+ return { status: "paid", topup_id: paid, billing: withoutFreeFields(b) };
9635
+ 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}\`.`);
9636
+ }
9637
+ }
9638
+ const deadline = Date.now() + (o.timeoutS ?? WAIT_DEFAULT_S) * 1000;
9639
+ for (;; ) {
9640
+ const t = await api(ctx, "GET", `/v1/billing/topups/${encodeURIComponent(id)}`);
9641
+ if (t.status === "paid")
9642
+ return { status: "paid", topup_id: id, billing: await billingStatus(ctx) };
9643
+ if (TOPUP_DONE.has(t.status ?? ""))
9644
+ 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 });
9645
+ if (Date.now() >= deadline)
9646
+ throw new CliError("topup_pending", `Top-up ${id} isn't paid yet.`, `Not paid yet. If the checkout page is open in your human's browser (billing topup printed opened_in_browser: true), run \`${WAIT_COMMAND} ${id}\` again right away, for up to ${WAIT_PATIENCE_MIN} minutes in all. If they don't have the approval_url (below), or it's still unpaid after that, end your turn and give it to them, then run it again when they're back. Don't create a new top-up.`, { topup_id: id, approval_url: t.approval_url ?? null });
9647
+ await new Promise((r) => setTimeout(r, o.pollMs ?? 3000));
9648
+ }
9546
9649
  }
9547
9650
  async function billingTopupAndWait(ctx, o) {
9548
9651
  const created = await billingTopup(ctx, o);
@@ -9734,13 +9837,10 @@ async function doctor(ctx) {
9734
9837
  next: err.hint ?? "Fix the error above, then run doctor again."
9735
9838
  };
9736
9839
  }
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
- });
9840
+ const purchased = a.plan === "paid" ? "credits bought" : "no credit purchase yet";
9841
+ checks.push({ check: "account", ok: a.status === "active", detail: `${a.account_id} (${purchased}, ${a.status})` });
9842
+ const credit = (a.balance_usd ?? 0) > 0;
9843
+ checks.push({ check: "credit", ok: credit, detail: `balance $${a.balance_usd ?? 0}` });
9744
9844
  const notices = a.notices ?? [];
9745
9845
  return {
9746
9846
  ok: checks.every((c) => c.ok) && notices.length === 0,
@@ -9750,10 +9850,10 @@ async function doctor(ctx) {
9750
9850
  checks,
9751
9851
  notices,
9752
9852
  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."
9853
+ 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
9854
  };
9755
9855
  }
9756
- var WEBHOOK_EVENTS = ["balance.low", "free_allowance.ending", "key.expiring", "budget.exhausted"];
9856
+ var WEBHOOK_EVENTS = ["balance.low", "key.expiring", "budget.exhausted"];
9757
9857
  async function webhooksAdd(ctx, o) {
9758
9858
  if (!/^https:\/\//i.test(o.url))
9759
9859
  throw new CliError("bad_argument", "The webhook URL must be https://.", "Pass a public https endpoint.");
@@ -10108,7 +10208,7 @@ function num(flag, v) {
10108
10208
  var init2 = defineCommand({
10109
10209
  meta: {
10110
10210
  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."
10211
+ 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
10212
  },
10113
10213
  args: {
10114
10214
  ...jsonFlag,
@@ -10163,7 +10263,7 @@ var models = defineCommand({
10163
10263
  });
10164
10264
  var ACCOUNT_SUBCOMMANDS = new Set(["delete", "ad-measurement"]);
10165
10265
  var account2 = defineCommand({
10166
- meta: { name: "account", description: "Region, spend, credit balance and free allowance left today" },
10266
+ meta: { name: "account", description: "Region, spend, credit balance and auto-reload" },
10167
10267
  args: jsonFlag,
10168
10268
  run: ({ args, rawArgs }) => {
10169
10269
  if (rawArgs.some((a) => ACCOUNT_SUBCOMMANDS.has(a)))
@@ -10419,7 +10519,7 @@ var billing = defineCommand({
10419
10519
  status: defineCommand({
10420
10520
  meta: {
10421
10521
  name: "status",
10422
- description: "Balance, free allowance left today, auto-reload, pending top-ups, recent transactions"
10522
+ description: "Credit balance, auto-reload, pending top-ups (with their approval links), recent transactions"
10423
10523
  },
10424
10524
  args: jsonFlag,
10425
10525
  run: ({ args }) => run(() => billingStatus(makeCtx()), args)
@@ -10427,7 +10527,7 @@ var billing = defineCommand({
10427
10527
  topup: defineCommand({
10428
10528
  meta: {
10429
10529
  name: "topup",
10430
- description: "Buy prepaid credits: returns an approval_url for your human. Optional auto-reload"
10530
+ description: "Buy prepaid credits: opens the checkout page in your human's browser (approval_url); then run `billing wait`, which returns once they pay. Optional auto-reload"
10431
10531
  },
10432
10532
  args: {
10433
10533
  ...jsonFlag,
@@ -10442,13 +10542,42 @@ var billing = defineCommand({
10442
10542
  type: "string",
10443
10543
  description: "Most auto-reload may charge per calendar month (USD, default 5x the amount)"
10444
10544
  },
10445
- wait: { type: "boolean", description: "Poll until the human pays or the link expires or fails" }
10545
+ open: {
10546
+ type: "boolean",
10547
+ default: true,
10548
+ description: "Open the checkout page in the browser (--no-open only prints approval_url)"
10549
+ },
10550
+ wait: {
10551
+ type: "boolean",
10552
+ 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`"
10553
+ }
10446
10554
  },
10447
10555
  run: async ({ args }) => {
10448
10556
  const amountUsd = num("amount-usd", args["amount-usd"]);
10449
10557
  const autoReload = autoReloadArgs(args);
10450
10558
  checkTopupLimits(amountUsd, autoReload);
10451
- await run(() => args.wait ? billingTopupAndWait(makeCtx(), { amountUsd, autoReload, onPending: (t) => out(t, args) }) : billingTopup(makeCtx(), { amountUsd, autoReload }), args);
10559
+ const noOpen = args.open === false;
10560
+ await run(() => args.wait ? billingTopupAndWait(makeCtx(), { amountUsd, autoReload, noOpen, onPending: (t) => out(t, args) }) : billingTopup(makeCtx(), { amountUsd, autoReload, noOpen }), args);
10561
+ }
10562
+ }),
10563
+ wait: defineCommand({
10564
+ meta: {
10565
+ name: "wait",
10566
+ 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)"
10567
+ },
10568
+ args: {
10569
+ ...jsonFlag,
10570
+ topup_id: { type: "positional", required: false, description: "topup_id from `billing topup` (optional)" },
10571
+ "timeout-s": {
10572
+ type: "string",
10573
+ description: `Seconds to wait before exiting with topup_pending (default ${WAIT_DEFAULT_S}, at most 600)`
10574
+ }
10575
+ },
10576
+ run: async ({ args }) => {
10577
+ const t = args["timeout-s"] === undefined ? undefined : num("timeout-s", args["timeout-s"]);
10578
+ if (t !== undefined && t > 600)
10579
+ throw new CliError("bad_argument", `--timeout-s must be at most 600, got ${t}.`, "Run wait again instead.");
10580
+ await run(() => billingWait(makeCtx(), { topupId: args.topup_id || undefined, timeoutS: t }), args);
10452
10581
  }
10453
10582
  }),
10454
10583
  "auto-reload": defineCommand({
@@ -10644,7 +10773,7 @@ var doctor2 = defineCommand({
10644
10773
  var webhooks = defineCommand({
10645
10774
  meta: {
10646
10775
  name: "webhooks",
10647
- description: "Signed HTTPS callbacks: balance.low, free_allowance.ending, key.expiring, budget.exhausted"
10776
+ description: "Signed HTTPS callbacks: balance.low, key.expiring, budget.exhausted"
10648
10777
  },
10649
10778
  subCommands: {
10650
10779
  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.2",
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",