mcp-scraper 0.21.2 → 0.21.3

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 (55) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +385 -179
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +6 -4
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +1 -0
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-JBME5WJR.js → chunk-2P5VFIJM.js} +2 -2
  18. package/dist/{chunk-GMZFQRH2.js → chunk-5IU5ZFVO.js} +2 -2
  19. package/dist/chunk-5IU5ZFVO.js.map +1 -0
  20. package/dist/{chunk-ZNNYPE2U.js → chunk-7LO6KLKD.js} +4 -4
  21. package/dist/{chunk-DZBGULWW.js → chunk-H7APH5UG.js} +81 -1
  22. package/dist/chunk-H7APH5UG.js.map +1 -0
  23. package/dist/{chunk-ZJDANZTQ.js → chunk-JJ5RMEGX.js} +3 -2
  24. package/dist/chunk-JJ5RMEGX.js.map +1 -0
  25. package/dist/{chunk-2FF4GBQ4.js → chunk-OBNKOYOE.js} +44 -4
  26. package/dist/chunk-OBNKOYOE.js.map +1 -0
  27. package/dist/chunk-VYUWWIVR.js +7 -0
  28. package/dist/chunk-VYUWWIVR.js.map +1 -0
  29. package/dist/{chunk-X3NIXHHG.js → chunk-XDRHBGJR.js} +3 -3
  30. package/dist/{db-EONGTNJM.js → db-Z35S224E.js} +4 -2
  31. package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-4RO3YU4Q.js} +2 -2
  32. package/dist/index.cjs +1 -0
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.js +3 -3
  35. package/dist/{server-CHTX6YQA.js → server-PBBRSLBP.js} +268 -186
  36. package/dist/server-PBBRSLBP.js.map +1 -0
  37. package/dist/{site-extract-repository-5V5EEQ72.js → site-extract-repository-FDH6ZXKT.js} +4 -4
  38. package/dist/{worker-GE5JMHE6.js → worker-BXJQ7J4N.js} +13 -7
  39. package/dist/worker-BXJQ7J4N.js.map +1 -0
  40. package/docs/mcp-tool-manifest.generated.json +2 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-2FF4GBQ4.js.map +0 -1
  43. package/dist/chunk-DZBGULWW.js.map +0 -1
  44. package/dist/chunk-GMZFQRH2.js.map +0 -1
  45. package/dist/chunk-NBF4F4UT.js +0 -7
  46. package/dist/chunk-NBF4F4UT.js.map +0 -1
  47. package/dist/chunk-ZJDANZTQ.js.map +0 -1
  48. package/dist/server-CHTX6YQA.js.map +0 -1
  49. package/dist/worker-GE5JMHE6.js.map +0 -1
  50. /package/dist/{chunk-JBME5WJR.js.map → chunk-2P5VFIJM.js.map} +0 -0
  51. /package/dist/{chunk-ZNNYPE2U.js.map → chunk-7LO6KLKD.js.map} +0 -0
  52. /package/dist/{chunk-X3NIXHHG.js.map → chunk-XDRHBGJR.js.map} +0 -0
  53. /package/dist/{db-EONGTNJM.js.map → db-Z35S224E.js.map} +0 -0
  54. /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-4RO3YU4Q.js.map} +0 -0
  55. /package/dist/{site-extract-repository-5V5EEQ72.js.map → site-extract-repository-FDH6ZXKT.js.map} +0 -0
@@ -3602,6 +3602,7 @@ __export(db_exports, {
3602
3602
  creditMc: () => creditMc,
3603
3603
  deactivateUser: () => deactivateUser,
3604
3604
  debitMc: () => debitMc,
3605
+ debitMcIdempotent: () => debitMcIdempotent,
3605
3606
  deleteWorkflowSchedule: () => deleteWorkflowSchedule,
3606
3607
  expireOldLots: () => expireOldLots,
3607
3608
  failJob: () => failJob,
@@ -3918,6 +3919,19 @@ async function migrate() {
3918
3919
  `);
3919
3920
  await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_user_created_at ON billing_events(user_id, created_at DESC)`);
3920
3921
  await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_authorization ON billing_events(authorization_id)`);
3922
+ await db.execute(`
3923
+ CREATE TABLE IF NOT EXISTS billing_debits (
3924
+ idempotency_key TEXT PRIMARY KEY,
3925
+ user_id INTEGER NOT NULL REFERENCES users(id),
3926
+ amount_mc INTEGER NOT NULL,
3927
+ operation TEXT NOT NULL,
3928
+ description TEXT,
3929
+ status TEXT NOT NULL DEFAULT 'claimed',
3930
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3931
+ applied_at TEXT
3932
+ )
3933
+ `);
3934
+ await db.execute(`CREATE INDEX IF NOT EXISTS billing_debits_user_status ON billing_debits(user_id, status)`);
3921
3935
  await db.execute(`
3922
3936
  CREATE TABLE IF NOT EXISTS site_audit_jobs (
3923
3937
  id TEXT PRIMARY KEY,
@@ -5289,6 +5303,72 @@ async function debitMc(userId, mc, operation, description) {
5289
5303
  const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
5290
5304
  return { ok: true, balance_mc: Number(res.rows[0]?.balance_mc ?? 0) };
5291
5305
  }
5306
+ async function debitMcIdempotent(userId, mc, operation, description, idempotencyKey) {
5307
+ if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent debit amount must be a positive integer");
5308
+ if (!idempotencyKey || idempotencyKey.length > 500) throw new Error("idempotent debit key is required and must be bounded");
5309
+ const db = getDb();
5310
+ await ensureMigrated(userId);
5311
+ const results = await db.batch([
5312
+ {
5313
+ sql: "UPDATE billing_debits SET amount_mc = ?, description = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND status = 'claimed'",
5314
+ args: [mc, description, idempotencyKey, userId, operation]
5315
+ },
5316
+ {
5317
+ sql: "INSERT OR IGNORE INTO billing_debits (idempotency_key, user_id, amount_mc, operation, description, status) VALUES (?, ?, ?, ?, ?, 'claimed')",
5318
+ args: [idempotencyKey, userId, mc, operation, description]
5319
+ },
5320
+ {
5321
+ sql: "UPDATE users SET balance_mc = balance_mc - ? WHERE id = ? AND balance_mc >= ? AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed')",
5322
+ args: [mc, userId, mc, idempotencyKey, userId, mc, operation]
5323
+ },
5324
+ {
5325
+ sql: "UPDATE billing_debits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed' AND changes() = 1",
5326
+ args: [idempotencyKey, userId, mc, operation]
5327
+ },
5328
+ {
5329
+ sql: `WITH ordered AS MATERIALIZED (
5330
+ SELECT id,
5331
+ remaining_mc,
5332
+ COALESCE(SUM(remaining_mc) OVER (
5333
+ ORDER BY expires_at ASC, granted_at ASC, id ASC
5334
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
5335
+ ), 0) AS prior_mc
5336
+ FROM credit_lots
5337
+ WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')
5338
+ ), allocation AS (
5339
+ SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
5340
+ FROM ordered
5341
+ )
5342
+ UPDATE credit_lots
5343
+ SET remaining_mc = remaining_mc - COALESCE((SELECT take_mc FROM allocation WHERE allocation.id = credit_lots.id), 0)
5344
+ WHERE id IN (SELECT id FROM allocation WHERE take_mc > 0)
5345
+ AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
5346
+ args: [userId, mc, idempotencyKey]
5347
+ },
5348
+ {
5349
+ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, -amount_mc, operation, description FROM billing_debits WHERE idempotency_key = ? AND status = 'applying'",
5350
+ args: [idempotencyKey]
5351
+ },
5352
+ {
5353
+ sql: "UPDATE billing_debits SET status = 'applied', applied_at = datetime('now') WHERE idempotency_key = ? AND status = 'applying'",
5354
+ args: [idempotencyKey]
5355
+ }
5356
+ ], "write");
5357
+ const claim = await db.execute({
5358
+ sql: "SELECT user_id, amount_mc, operation, status FROM billing_debits WHERE idempotency_key = ? LIMIT 1",
5359
+ args: [idempotencyKey]
5360
+ });
5361
+ const row = claim.rows[0];
5362
+ if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation) {
5363
+ throw new Error("idempotent debit key conflicts with another debit");
5364
+ }
5365
+ const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
5366
+ return {
5367
+ ok: String(row.status) === "applied",
5368
+ balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
5369
+ duplicate: results[2]?.rowsAffected === 0 && String(row.status) === "applied"
5370
+ };
5371
+ }
5292
5372
  async function getLedger(userId, limit = 50) {
5293
5373
  const res = await getDb().execute({
5294
5374
  sql: "SELECT * FROM ledger WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
@@ -8496,6 +8576,18 @@ function vendorCostUsdToMc(costUsd, markupBps) {
8496
8576
  if (mc > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("calculated mc exceeds the safe integer range");
8497
8577
  return Number(mc);
8498
8578
  }
8579
+ function getPublicRatesPayload() {
8580
+ const starter = SUBSCRIPTION_TIER_BY_KEY.starter;
8581
+ const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
8582
+ ...cost,
8583
+ ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
8584
+ }));
8585
+ return {
8586
+ creditsPerDollarStarter: Math.round(mcToCredits(starter.credits_mc) / starter.monthly_usd),
8587
+ costs,
8588
+ scheduling: SCHEDULED_ACTIONS_RATE_POLICY
8589
+ };
8590
+ }
8499
8591
  function concurrencySlotBillingInfo() {
8500
8592
  const billingUrl = process.env.CONCURRENCY_BILLING_URL ?? "https://mcpscraper.dev/billing";
8501
8593
  return {
@@ -8525,12 +8617,14 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
8525
8617
  topup_url: topupUrl
8526
8618
  };
8527
8619
  }
8528
- var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, BILLING_MC_PER_USD_NUMERATOR, BILLING_MC_PER_USD_DENOMINATOR, BILLING_BASIS_POINTS, USD_NANOS, SCHEDULED_RUN_BASE_CREDITS, SCHEDULED_RUN_BASE_MC, SCHEDULED_AGENT_LLM_MARKUP_BPS, SCHEDULED_AGENT_LLM_MARKUP, SCHEDULED_RUN_DEFAULT_MAX_CREDITS, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_HOLD_MC, MC_PER_USD;
8620
+ var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, BILLING_MC_PER_USD_NUMERATOR, BILLING_MC_PER_USD_DENOMINATOR, BILLING_BASIS_POINTS, USD_NANOS, SCHEDULED_RUN_BASE_CREDITS, SCHEDULED_RUN_BASE_MC, SCHEDULED_AGENT_LLM_MARKUP_BPS, SCHEDULED_AGENT_LLM_MARKUP, SCHEDULED_RUN_DEFAULT_MAX_CREDITS, SCHEDULED_ACTIONS_RATE_POLICY, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_HOLD_MC, MC_PER_USD;
8529
8621
  var init_rates = __esm({
8530
8622
  "src/api/rates.ts"() {
8531
8623
  "use strict";
8532
8624
  MC_COSTS = {
8533
8625
  serp: 100,
8626
+ serp_headless: 400,
8627
+ serp_headful: 1400,
8534
8628
  paa: 300,
8535
8629
  paa_base: 5500,
8536
8630
  page_scrape: 100,
@@ -8571,14 +8665,38 @@ var init_rates = __esm({
8571
8665
  SCHEDULED_AGENT_LLM_MARKUP_BPS = 15e3;
8572
8666
  SCHEDULED_AGENT_LLM_MARKUP = SCHEDULED_AGENT_LLM_MARKUP_BPS / BILLING_BASIS_POINTS;
8573
8667
  SCHEDULED_RUN_DEFAULT_MAX_CREDITS = 5e3;
8668
+ SCHEDULED_ACTIONS_RATE_POLICY = {
8669
+ billingMode: "credits",
8670
+ entitlement: "paid_plan_included",
8671
+ separateSubscriptionRequired: false,
8672
+ chargeEvent: "started_occurrence",
8673
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
8674
+ runBaseMc: SCHEDULED_RUN_BASE_MC,
8675
+ llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP,
8676
+ llmCostMultiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
8677
+ modes: {
8678
+ connection_sync: {
8679
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
8680
+ modelCostBilling: "none",
8681
+ modelCostMultiplier: 0
8682
+ },
8683
+ agent: {
8684
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
8685
+ modelCostBilling: "actual_reported_cost",
8686
+ modelCostProvider: "openrouter",
8687
+ modelCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP,
8688
+ modelCostMultiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS
8689
+ }
8690
+ }
8691
+ };
8574
8692
  CREDIT_COST_CATALOG = [
8575
8693
  {
8576
8694
  key: "serp",
8577
8695
  label: "SERP search",
8578
8696
  aliases: ["search_serp", "serp", "google search", "organic results"],
8579
- credits: mcToCredits(MC_COSTS.serp),
8580
- unit: "per search",
8581
- notes: "Returns AI Overview, PAA snippet, videos, forums, and local pack."
8697
+ credits: mcToCredits(MC_COSTS.serp_headless),
8698
+ unit: "per search (headless); escalates to headful only when needed",
8699
+ notes: `Returns AI Overview, PAA snippet, videos, forums, and local pack. Billed ${mcToCredits(MC_COSTS.serp_headless)} credits when served headless (the common case), ${mcToCredits(MC_COSTS.serp_headful)} credits if an anti-bot escalation forces headful mode (overestimate refunded).`
8582
8700
  },
8583
8701
  {
8584
8702
  key: "paa",
@@ -8818,6 +8936,7 @@ var init_rates = __esm({
8818
8936
  PAA: "paa",
8819
8937
  PAA_REFUND: "paa_refund",
8820
8938
  SERP: "serp",
8939
+ SERP_REFUND: "serp_refund",
8821
8940
  REFUND: "refund",
8822
8941
  TRANSCRIPTION: "transcription",
8823
8942
  TRANSCRIPTION_HOLD: "transcription_hold",
@@ -11901,8 +12020,17 @@ var init_internal_memory_routes = __esm({
11901
12020
  });
11902
12021
 
11903
12022
  // src/api/scheduling-access.ts
12023
+ function schedulingAccessOverrides() {
12024
+ return new Set(
12025
+ (process.env.SCHEDULING_ACCESS_OVERRIDE_IDENTITIES ?? "").split(",").map((identity) => identity.trim().toLowerCase()).filter(Boolean)
12026
+ );
12027
+ }
11904
12028
  function hasPaidSchedulingAccess(user) {
11905
- return typeof user.subscription_tier === "string" && user.subscription_tier.trim().length > 0;
12029
+ const tier = typeof user.subscription_tier === "string" ? user.subscription_tier.trim() : "";
12030
+ const subscriptionId = typeof user.subscription_id === "string" ? user.subscription_id.trim() : "";
12031
+ if (tier && subscriptionId) return true;
12032
+ const identity = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
12033
+ return identity.length > 0 && schedulingAccessOverrides().has(identity);
11906
12034
  }
11907
12035
  function paidPlanRequiredPayload() {
11908
12036
  return {
@@ -11995,6 +12123,15 @@ function safeMetadata(value) {
11995
12123
  if (!value) return "{}";
11996
12124
  return JSON.stringify(value).slice(0, 4e3);
11997
12125
  }
12126
+ function metadataReason(row) {
12127
+ if (!row.metadata) return void 0;
12128
+ try {
12129
+ const metadata = JSON.parse(row.metadata);
12130
+ return typeof metadata.reason === "string" ? metadata.reason : void 0;
12131
+ } catch {
12132
+ return void 0;
12133
+ }
12134
+ }
11998
12135
  function mapAuthorization(row, balanceMc) {
11999
12136
  return {
12000
12137
  id: row.id,
@@ -12023,23 +12160,28 @@ async function eventByKey(idempotencyKey) {
12023
12160
  });
12024
12161
  return result.rows[0] ? result.rows[0] : null;
12025
12162
  }
12026
- function mapEvent(row, balanceMc, duplicate) {
12027
- let reason;
12028
- if (row.metadata) {
12029
- try {
12030
- const metadata = JSON.parse(row.metadata);
12031
- if (typeof metadata.reason === "string") reason = metadata.reason;
12032
- } catch {
12033
- }
12034
- }
12163
+ function mapStartEvent(row, balanceMc, duplicate) {
12164
+ return {
12165
+ id: row.id,
12166
+ authorizationId: row.authorization_id,
12167
+ status: "started",
12168
+ amountMc: SCHEDULED_RUN_BASE_MC,
12169
+ credits: SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT,
12170
+ balanceMc,
12171
+ duplicate
12172
+ };
12173
+ }
12174
+ function mapSettlementEvent(row, balanceMc, duplicate) {
12035
12175
  const rawNanos = row.raw_cost_usd_nanos == null ? null : Number(row.raw_cost_usd_nanos);
12036
- const modelMc = Math.max(0, Number(row.amount_mc) - SCHEDULED_RUN_BASE_MC);
12176
+ const modelMc = Math.max(0, Number(row.amount_mc));
12177
+ const amountMc = SCHEDULED_RUN_BASE_MC + modelMc;
12178
+ const reason = metadataReason(row);
12037
12179
  return {
12038
12180
  id: row.id,
12039
12181
  authorizationId: row.authorization_id,
12040
12182
  status: row.status === "settled" ? "settled" : "cost_pending",
12041
- amountMc: Number(row.amount_mc),
12042
- credits: Number(row.amount_mc) / MC_PER_CREDIT,
12183
+ amountMc,
12184
+ credits: amountMc / MC_PER_CREDIT,
12043
12185
  baseMc: SCHEDULED_RUN_BASE_MC,
12044
12186
  modelMc,
12045
12187
  rawCostUsd: rawNanos == null ? null : rawNanos / USD_NANOS,
@@ -12061,7 +12203,7 @@ async function authorizeScheduledRun(args) {
12061
12203
  args: [user.id, SCHEDULED_RUN_BILLING_CLASS]
12062
12204
  });
12063
12205
  if (pending.rows.length > 0) {
12064
- throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved cost", 409);
12206
+ throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved model cost", 409);
12065
12207
  }
12066
12208
  const existing = await getDb().execute({
12067
12209
  sql: "SELECT id, user_id, idempotency_key, status, minimum_amount_mc, maximum_amount_mc, token_hash, expires_at FROM billing_authorizations WHERE idempotency_key = ? LIMIT 1",
@@ -12070,14 +12212,9 @@ async function authorizeScheduledRun(args) {
12070
12212
  if (existing.rows[0]) {
12071
12213
  const row = existing.rows[0];
12072
12214
  if (Number(row.user_id) !== user.id) {
12073
- throw new UnifiedBillingError(
12074
- "invalid_request",
12075
- "the idempotency key is already in use by another billing identity",
12076
- 409
12077
- );
12215
+ throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
12078
12216
  }
12079
- const balanceMc2 = await reconcileBalanceMc(user.id, user.balance_mc);
12080
- return mapAuthorization(row, balanceMc2);
12217
+ return mapAuthorization(row, await reconcileBalanceMc(user.id, user.balance_mc));
12081
12218
  }
12082
12219
  const maximumCredits = args.maximumCredits ?? SCHEDULED_RUN_DEFAULT_MAX_CREDITS;
12083
12220
  if (!Number.isFinite(maximumCredits) || maximumCredits < SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT) {
@@ -12098,17 +12235,7 @@ async function authorizeScheduledRun(args) {
12098
12235
  const expiresAt = new Date(Date.now() + AUTHORIZATION_TTL_MS).toISOString();
12099
12236
  const inserted = await getDb().execute({
12100
12237
  sql: "INSERT OR IGNORE INTO billing_authorizations (id, user_id, idempotency_key, billing_class, status, minimum_amount_mc, maximum_amount_mc, token_hash, metadata, expires_at) VALUES (?, ?, ?, ?, 'authorized', ?, ?, ?, ?, ?)",
12101
- args: [
12102
- id,
12103
- user.id,
12104
- idempotencyKey,
12105
- SCHEDULED_RUN_BILLING_CLASS,
12106
- SCHEDULED_RUN_BASE_MC,
12107
- maximumMc,
12108
- tokenHash(token),
12109
- safeMetadata(args.metadata),
12110
- expiresAt
12111
- ]
12238
+ args: [id, user.id, idempotencyKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_BASE_MC, maximumMc, tokenHash(token), safeMetadata(args.metadata), expiresAt]
12112
12239
  });
12113
12240
  if (inserted.rowsAffected === 0) {
12114
12241
  const raced = await getDb().execute({
@@ -12118,11 +12245,7 @@ async function authorizeScheduledRun(args) {
12118
12245
  if (!raced.rows[0]) throw new Error("billing authorization insert raced without a readable winner");
12119
12246
  const row = raced.rows[0];
12120
12247
  if (Number(row.user_id) !== user.id) {
12121
- throw new UnifiedBillingError(
12122
- "invalid_request",
12123
- "the idempotency key is already in use by another billing identity",
12124
- 409
12125
- );
12248
+ throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
12126
12249
  }
12127
12250
  return mapAuthorization(row, balanceMc);
12128
12251
  }
@@ -12139,180 +12262,235 @@ async function authorizeScheduledRun(args) {
12139
12262
  reservationMode: "balance_check"
12140
12263
  };
12141
12264
  }
12142
- async function settleScheduledRun(args) {
12143
- const identity = normalizeIdentity(args.identity);
12144
- const user = await getUserByEmail(identity);
12145
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
12146
- const authorization = await authorizationById(args.authorizationId);
12265
+ async function startScheduledRun(args) {
12266
+ const user = await resolveSchedulingUser(args.identity);
12267
+ let authorization = await authorizationById(args.authorizationId);
12147
12268
  if (!authorization || Number(authorization.user_id) !== user.id) {
12148
12269
  throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
12149
12270
  }
12150
12271
  verifyToken(authorization, args.authorizationToken);
12151
- const settlementKey = authorization.id + ":settlement";
12152
- const existing = await eventByKey(settlementKey);
12153
- if (existing) {
12154
- const balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
12155
- if (existing.status === "settling") {
12156
- throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
12157
- }
12158
- return mapEvent(existing, balanceMc, true);
12272
+ const startKey = authorization.id + ":start";
12273
+ const existing = await eventByKey(startKey);
12274
+ if (existing?.status === "settled") {
12275
+ await getDb().execute({
12276
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting')",
12277
+ args: [authorization.id]
12278
+ });
12279
+ return mapStartEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
12159
12280
  }
12160
- if (authorization.status !== "authorized") {
12161
- throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer open", 409);
12281
+ if (!hasPaidSchedulingAccess(user)) {
12282
+ throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
12162
12283
  }
12163
- if (Date.parse(authorization.expires_at) <= Date.now()) {
12284
+ if (authorization.status === "authorized") {
12285
+ if (Date.parse(authorization.expires_at) <= Date.now()) {
12286
+ await getDb().execute({
12287
+ sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
12288
+ args: [authorization.id]
12289
+ });
12290
+ throw new UnifiedBillingError("authorization_expired", "billing authorization expired before execution started", 409);
12291
+ }
12164
12292
  await getDb().execute({
12165
- sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
12293
+ sql: "UPDATE billing_authorizations SET status = 'starting' WHERE id = ? AND status = 'authorized'",
12166
12294
  args: [authorization.id]
12167
12295
  });
12168
- throw new UnifiedBillingError("authorization_expired", "billing authorization expired before settlement", 409);
12169
- }
12170
- const rawCostUsd = args.rawCostUsd ?? null;
12171
- if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
12172
- throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
12296
+ authorization = await authorizationById(authorization.id);
12173
12297
  }
12174
- if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
12175
- throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
12298
+ if (!authorization || !["starting", "started"].includes(authorization.status)) {
12299
+ throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer startable", 409);
12176
12300
  }
12177
- const eventId = (0, import_node_crypto4.randomUUID)();
12178
- const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
12179
- const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
12180
- const totalMc = SCHEDULED_RUN_BASE_MC + modelMc;
12181
- const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : totalMc > Number(authorization.maximum_amount_mc) ? "max_credits_per_run_exceeded" : null;
12182
- if (pendingReason) {
12301
+ const eventId = existing?.id ?? (0, import_node_crypto4.randomUUID)();
12302
+ if (!existing) {
12183
12303
  await getDb().execute({
12184
- sql: "INSERT INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'cost_pending', ?, ?, ?, ?, ?, ?, ?, ?)",
12304
+ sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, multiplier_bps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'capturing', ?, ?, ?)",
12185
12305
  args: [
12186
12306
  eventId,
12187
12307
  user.id,
12188
12308
  authorization.id,
12189
- settlementKey,
12309
+ startKey,
12190
12310
  SCHEDULED_RUN_BILLING_CLASS,
12191
12311
  SCHEDULED_RUN_SOURCE_SURFACE,
12192
- totalMc,
12193
- rawCostNanos,
12312
+ SCHEDULED_RUN_BASE_MC,
12194
12313
  SCHEDULED_AGENT_LLM_MARKUP_BPS,
12195
- Math.max(0, Math.round(args.inputTokens ?? 0)),
12196
- Math.max(0, Math.round(args.outputTokens ?? 0)),
12197
- Math.max(0, Math.round(args.cachedTokens ?? 0)),
12198
- Math.max(0, Math.round(args.modelSteps ?? 0)),
12199
- safeMetadata({ ...args.metadata, reason: pendingReason })
12314
+ safeMetadata({ ...args.metadata, billingPhase: "start", chargeMode: "base_capture" })
12200
12315
  ]
12201
12316
  });
12202
- await getDb().execute({
12203
- sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
12204
- args: [authorization.id]
12205
- });
12206
- return {
12207
- id: eventId,
12208
- authorizationId: authorization.id,
12209
- status: "cost_pending",
12210
- amountMc: totalMc,
12211
- credits: totalMc / MC_PER_CREDIT,
12212
- baseMc: SCHEDULED_RUN_BASE_MC,
12213
- modelMc,
12214
- rawCostUsd,
12215
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
12216
- balanceMc: await reconcileBalanceMc(user.id, user.balance_mc),
12217
- reason: pendingReason,
12218
- duplicate: false
12219
- };
12220
- }
12221
- const claimed = await getDb().execute({
12222
- sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'settling', ?, ?, ?, ?, ?, ?, ?, ?)",
12223
- args: [
12224
- eventId,
12225
- user.id,
12226
- authorization.id,
12227
- settlementKey,
12228
- SCHEDULED_RUN_BILLING_CLASS,
12229
- SCHEDULED_RUN_SOURCE_SURFACE,
12230
- totalMc,
12231
- rawCostNanos,
12232
- SCHEDULED_AGENT_LLM_MARKUP_BPS,
12233
- Math.max(0, Math.round(args.inputTokens ?? 0)),
12234
- Math.max(0, Math.round(args.outputTokens ?? 0)),
12235
- Math.max(0, Math.round(args.cachedTokens ?? 0)),
12236
- Math.max(0, Math.round(args.modelSteps ?? 0)),
12237
- safeMetadata(args.metadata)
12238
- ]
12239
- });
12240
- if (claimed.rowsAffected === 0) {
12241
- const raced = await eventByKey(settlementKey);
12242
- if (!raced) throw new Error("billing settlement raced without a readable winner");
12243
- if (raced.status === "settling") {
12244
- throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
12245
- }
12246
- return mapEvent(raced, await reconcileBalanceMc(user.id, user.balance_mc), true);
12247
12317
  }
12248
- const debit = await debitMc(
12318
+ const debit = await debitMcIdempotent(
12249
12319
  user.id,
12250
- totalMc,
12320
+ SCHEDULED_RUN_BASE_MC,
12251
12321
  LedgerOperation.SCHEDULED_RUN,
12252
- "scheduled-run:" + authorization.id
12322
+ "scheduled-run:" + authorization.id + ":base",
12323
+ startKey
12253
12324
  );
12254
12325
  if (!debit.ok) {
12255
- const metadata = safeMetadata({ ...args.metadata, reason: "insufficient_credits_at_settlement" });
12256
- await getDb().execute({
12257
- sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ?",
12258
- args: [metadata, eventId]
12259
- });
12260
12326
  await getDb().execute({
12261
- sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
12327
+ sql: "UPDATE billing_authorizations SET status = 'authorized' WHERE id = ? AND status = 'starting'",
12262
12328
  args: [authorization.id]
12263
12329
  });
12264
- return {
12265
- id: eventId,
12266
- authorizationId: authorization.id,
12267
- status: "cost_pending",
12268
- amountMc: totalMc,
12269
- credits: totalMc / MC_PER_CREDIT,
12270
- baseMc: SCHEDULED_RUN_BASE_MC,
12330
+ throw new UnifiedBillingError(
12331
+ "insufficient_credits",
12332
+ "the wallet could not capture the scheduled-run base charge",
12333
+ 402,
12334
+ { balanceMc: debit.balance_mc, requiredMc: SCHEDULED_RUN_BASE_MC }
12335
+ );
12336
+ }
12337
+ await getDb().execute({
12338
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE idempotency_key = ? AND status = 'capturing'",
12339
+ args: [startKey]
12340
+ });
12341
+ await getDb().execute({
12342
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting', 'started')",
12343
+ args: [authorization.id]
12344
+ });
12345
+ const started = await eventByKey(startKey);
12346
+ if (!started) throw new Error("scheduled-run base capture completed without an event receipt");
12347
+ return mapStartEvent(started, debit.balance_mc, debit.duplicate);
12348
+ }
12349
+ function validateModelCost(args) {
12350
+ const rawCostUsd = args.rawCostUsd ?? null;
12351
+ if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
12352
+ throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
12353
+ }
12354
+ if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
12355
+ throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
12356
+ }
12357
+ return rawCostUsd;
12358
+ }
12359
+ async function recoverSettlement(row, user, authorization, duplicate) {
12360
+ const modelMc = Math.max(0, Number(row.amount_mc));
12361
+ let balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
12362
+ if (modelMc > 0) {
12363
+ const debit = await debitMcIdempotent(
12364
+ user.id,
12271
12365
  modelMc,
12272
- rawCostUsd,
12273
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
12274
- balanceMc: debit.balance_mc,
12275
- reason: "insufficient_credits_at_settlement",
12276
- duplicate: false
12277
- };
12366
+ LedgerOperation.SCHEDULED_RUN,
12367
+ "scheduled-run:" + authorization.id + ":model",
12368
+ authorization.id + ":model"
12369
+ );
12370
+ balanceMc = debit.balance_mc;
12371
+ if (!debit.ok) {
12372
+ await getDb().execute({
12373
+ sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ? AND status = 'settling'",
12374
+ args: [safeMetadata({ reason: "insufficient_credits_for_model_delta", billingPhase: "settlement", chargeMode: "model_delta" }), row.id]
12375
+ });
12376
+ await getDb().execute({
12377
+ sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
12378
+ args: [authorization.id]
12379
+ });
12380
+ const pending = await eventByKey(authorization.id + ":settlement");
12381
+ if (!pending) throw new Error("scheduled-run pending settlement lost its event");
12382
+ return mapSettlementEvent(pending, balanceMc, duplicate);
12383
+ }
12278
12384
  }
12279
12385
  await getDb().execute({
12280
- sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
12281
- args: [eventId]
12386
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status = 'settling'",
12387
+ args: [row.id]
12282
12388
  });
12283
12389
  await getDb().execute({
12284
- sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
12390
+ sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('started', 'cost_pending', 'settled')",
12285
12391
  args: [authorization.id]
12286
12392
  });
12287
- return {
12288
- id: eventId,
12289
- authorizationId: authorization.id,
12290
- status: "settled",
12291
- amountMc: totalMc,
12292
- credits: totalMc / MC_PER_CREDIT,
12293
- baseMc: SCHEDULED_RUN_BASE_MC,
12393
+ const settled = await eventByKey(authorization.id + ":settlement");
12394
+ if (!settled) throw new Error("scheduled-run settlement completed without an event receipt");
12395
+ return mapSettlementEvent(settled, balanceMc, duplicate);
12396
+ }
12397
+ async function settleScheduledRun(args) {
12398
+ const user = await resolveSchedulingUser(args.identity);
12399
+ let authorization = await authorizationById(args.authorizationId);
12400
+ if (!authorization || Number(authorization.user_id) !== user.id) {
12401
+ throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
12402
+ }
12403
+ verifyToken(authorization, args.authorizationToken);
12404
+ const rawCostUsd = validateModelCost(args);
12405
+ const startEvent = await eventByKey(authorization.id + ":start");
12406
+ if (!startEvent || startEvent.status !== "settled") {
12407
+ throw new UnifiedBillingError("authorization_closed", "scheduled-run settlement requires a captured start", 409);
12408
+ }
12409
+ if (authorization.status === "starting") {
12410
+ await getDb().execute({
12411
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status = 'starting'",
12412
+ args: [authorization.id]
12413
+ });
12414
+ authorization = await authorizationById(authorization.id);
12415
+ }
12416
+ if (!authorization || !["started", "cost_pending", "settled"].includes(authorization.status)) {
12417
+ throw new UnifiedBillingError("authorization_closed", "billing authorization is not open for settlement", 409);
12418
+ }
12419
+ const settlementKey = authorization.id + ":settlement";
12420
+ let existing = await eventByKey(settlementKey);
12421
+ if (existing?.status === "settled") {
12422
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
12423
+ }
12424
+ if (existing?.status === "settling") {
12425
+ return recoverSettlement(existing, user, authorization, true);
12426
+ }
12427
+ const hadExistingClaim = Boolean(existing);
12428
+ const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
12429
+ const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
12430
+ const maximumModelMc = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
12431
+ const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
12432
+ const metadata = safeMetadata({
12433
+ ...args.metadata,
12434
+ billingPhase: "settlement",
12435
+ chargeMode: "model_delta",
12436
+ ...pendingReason ? { reason: pendingReason } : {}
12437
+ });
12438
+ const eventId = existing?.id ?? (0, import_node_crypto4.randomUUID)();
12439
+ const status = pendingReason ? "cost_pending" : "settling";
12440
+ const eventValues = [
12294
12441
  modelMc,
12295
- rawCostUsd,
12296
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
12297
- balanceMc: debit.balance_mc,
12298
- duplicate: false
12299
- };
12442
+ rawCostNanos,
12443
+ SCHEDULED_AGENT_LLM_MARKUP_BPS,
12444
+ Math.max(0, Math.round(args.inputTokens ?? 0)),
12445
+ Math.max(0, Math.round(args.outputTokens ?? 0)),
12446
+ Math.max(0, Math.round(args.cachedTokens ?? 0)),
12447
+ Math.max(0, Math.round(args.modelSteps ?? 0)),
12448
+ metadata
12449
+ ];
12450
+ if (existing) {
12451
+ await getDb().execute({
12452
+ sql: "UPDATE billing_events SET status = ?, amount_mc = ?, raw_cost_usd_nanos = ?, multiplier_bps = ?, input_tokens = ?, output_tokens = ?, cached_tokens = ?, model_steps = ?, metadata = ?, settled_at = NULL WHERE id = ? AND status = 'cost_pending'",
12453
+ args: [status, ...eventValues, existing.id]
12454
+ });
12455
+ } else {
12456
+ await getDb().execute({
12457
+ sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
12458
+ args: [eventId, user.id, authorization.id, settlementKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_SOURCE_SURFACE, status, ...eventValues]
12459
+ });
12460
+ }
12461
+ existing = await eventByKey(settlementKey);
12462
+ if (!existing) throw new Error("scheduled-run settlement claim was not readable");
12463
+ const duplicateClaim = hadExistingClaim || eventId !== existing.id;
12464
+ if (existing.status === "settled") {
12465
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
12466
+ }
12467
+ if (existing.status === "cost_pending") {
12468
+ await getDb().execute({
12469
+ sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
12470
+ args: [authorization.id]
12471
+ });
12472
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), duplicateClaim);
12473
+ }
12474
+ if (existing.status !== "settling") {
12475
+ throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement could not be claimed", 409);
12476
+ }
12477
+ return recoverSettlement(existing, user, authorization, duplicateClaim);
12300
12478
  }
12301
12479
  async function voidScheduledRunAuthorization(args) {
12302
- const identity = normalizeIdentity(args.identity);
12303
- const user = await getUserByEmail(identity);
12304
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
12480
+ const user = await resolveSchedulingUser(args.identity);
12305
12481
  const authorization = await authorizationById(args.authorizationId);
12306
12482
  if (!authorization || Number(authorization.user_id) !== user.id) {
12307
12483
  throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
12308
12484
  }
12309
12485
  verifyToken(authorization, args.authorizationToken);
12310
12486
  if (authorization.status === "authorized") {
12311
- await getDb().execute({
12487
+ const result = await getDb().execute({
12312
12488
  sql: "UPDATE billing_authorizations SET status = 'void', metadata = ?, settled_at = datetime('now') WHERE id = ? AND status = 'authorized'",
12313
12489
  args: [safeMetadata({ reason: args.reason ?? "voided_before_execution" }), authorization.id]
12314
12490
  });
12315
- return { ok: true, status: "void" };
12491
+ if (result.rowsAffected > 0) return { ok: true, status: "void" };
12492
+ const raced = await authorizationById(authorization.id);
12493
+ return { ok: true, status: raced?.status ?? "closed" };
12316
12494
  }
12317
12495
  return { ok: true, status: authorization.status };
12318
12496
  }
@@ -12326,7 +12504,7 @@ var init_unified_billing = __esm({
12326
12504
  init_scheduling_access();
12327
12505
  SCHEDULED_RUN_BILLING_CLASS = "scheduled_agent_run";
12328
12506
  SCHEDULED_RUN_SOURCE_SURFACE = "mastra_scheduler";
12329
- AUTHORIZATION_TTL_MS = 30 * 60 * 1e3;
12507
+ AUTHORIZATION_TTL_MS = 24 * 60 * 60 * 1e3;
12330
12508
  UnifiedBillingError = class extends Error {
12331
12509
  constructor(code, message, httpStatus, details = {}) {
12332
12510
  super(message);
@@ -12551,7 +12729,7 @@ function billingError(error) {
12551
12729
  status: 500
12552
12730
  };
12553
12731
  }
12554
- var import_hono3, import_zod12, internalBillingApp, metadataSchema, schedulingAccessSchema, authorizeSchema, settleSchema, voidSchema;
12732
+ var import_hono3, import_zod12, internalBillingApp, metadataSchema, schedulingAccessSchema, authorizeSchema, settleSchema, startSchema, voidSchema;
12555
12733
  var init_internal_billing_routes = __esm({
12556
12734
  "src/api/internal-billing-routes.ts"() {
12557
12735
  "use strict";
@@ -12585,6 +12763,12 @@ var init_internal_billing_routes = __esm({
12585
12763
  modelSteps: import_zod12.z.number().finite().nonnegative().optional(),
12586
12764
  metadata: metadataSchema
12587
12765
  });
12766
+ startSchema = import_zod12.z.object({
12767
+ identity: import_zod12.z.string().min(1).max(320),
12768
+ authorizationId: import_zod12.z.string().uuid(),
12769
+ authorizationToken: import_zod12.z.string().min(20).max(500),
12770
+ metadata: metadataSchema
12771
+ });
12588
12772
  voidSchema = import_zod12.z.object({
12589
12773
  identity: import_zod12.z.string().min(1).max(320),
12590
12774
  authorizationId: import_zod12.z.string().uuid(),
@@ -12658,6 +12842,19 @@ var init_internal_billing_routes = __esm({
12658
12842
  return c.json(response.body, response.status);
12659
12843
  }
12660
12844
  });
12845
+ internalBillingApp.post("/scheduled-run/start", async (c) => {
12846
+ const parsed = startSchema.safeParse(await c.req.json().catch(() => null));
12847
+ if (!parsed.success) {
12848
+ return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduled-run start request" }, 400);
12849
+ }
12850
+ try {
12851
+ const start = await startScheduledRun(parsed.data);
12852
+ return c.json({ ok: true, start });
12853
+ } catch (error) {
12854
+ const response = billingError(error);
12855
+ return c.json(response.body, response.status);
12856
+ }
12857
+ });
12661
12858
  internalBillingApp.post("/scheduled-run/void", async (c) => {
12662
12859
  const parsed = voidSchema.safeParse(await c.req.json().catch(() => null));
12663
12860
  if (!parsed.success) {
@@ -12944,6 +13141,7 @@ var init_BrowserDriver = __esm({
12944
13141
  else if (config.headlessMode === "headless") useHeadless = true;
12945
13142
  else useHeadless = costCtx?.forceHeadless === true || process.env.KERNEL_HEADLESS_DISABLED !== "true" && !!costCtx?.op && HEADLESS_OPS.has(costCtx.op);
12946
13143
  this.kernelHeadlessSent = useHeadless ? true : null;
13144
+ if (costCtx?.headlessSentOut) costCtx.headlessSentOut.value = this.kernelHeadlessSent;
12947
13145
  const kernelBrowser = await this.kernelClient.browsers.create({
12948
13146
  stealth: true,
12949
13147
  timeout_seconds: timeoutSeconds,
@@ -28499,7 +28697,7 @@ var PACKAGE_VERSION;
28499
28697
  var init_version = __esm({
28500
28698
  "src/version.ts"() {
28501
28699
  "use strict";
28502
- PACKAGE_VERSION = "0.21.2";
28700
+ PACKAGE_VERSION = "0.21.3";
28503
28701
  }
28504
28702
  });
28505
28703
 
@@ -42307,8 +42505,9 @@ async function processJob(job) {
42307
42505
  running++;
42308
42506
  try {
42309
42507
  const opts = typeof job.options === "string" ? JSON.parse(job.options) : job.options;
42508
+ const headlessSentOut = { value: null };
42310
42509
  const result = await runWithCostContext(
42311
- { op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id) },
42510
+ { op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id), headlessSentOut },
42312
42511
  () => harvest({
42313
42512
  ...opts,
42314
42513
  kernelApiKey: browserServiceApiKey(),
@@ -42325,6 +42524,11 @@ async function processJob(job) {
42325
42524
  const diff = opts.billingHoldMc - actualCost;
42326
42525
  if (diff > 0) await creditMc(job.user_id, diff, "paa_refund", "overestimate refund");
42327
42526
  else if (diff < 0) await debitMc(job.user_id, -diff, "paa", opts.query ?? job.query);
42527
+ } else if (opts.serpOnly && typeof opts.billingHoldMc === "number") {
42528
+ const actualCost = headlessSentOut.value === true ? MC_COSTS.serp_headless : MC_COSTS.serp_headful;
42529
+ const diff = opts.billingHoldMc - actualCost;
42530
+ if (diff > 0) await creditMc(job.user_id, diff, "serp_refund", "headless-mode pricing settle");
42531
+ else if (diff < 0) await debitMc(job.user_id, -diff, "serp", opts.query ?? job.query);
42328
42532
  }
42329
42533
  if (job.callback_url) {
42330
42534
  await deliverWebhook(job.callback_url, { job_id: job.id, status: "done", result, attempts });
@@ -42923,11 +43127,7 @@ var init_server = __esm({
42923
43127
  return c.json(CATALOG);
42924
43128
  });
42925
43129
  app.get("/rates", (c) => {
42926
- const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
42927
- ...cost,
42928
- ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
42929
- }));
42930
- return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
43130
+ return c.json(getPublicRatesPayload());
42931
43131
  });
42932
43132
  app.get("/me", async (c) => {
42933
43133
  const token = (0, import_cookie2.getCookie)(c, "session");
@@ -43842,7 +44042,7 @@ var init_server = __esm({
43842
44042
  serpOnly: body.serpOnly ?? false,
43843
44043
  pages: Math.min(2, Math.max(1, body.pages ?? 1))
43844
44044
  };
43845
- const harvestCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount2(options.maxQuestions);
44045
+ const harvestCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount2(options.maxQuestions);
43846
44046
  const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(user.id, harvestCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
43847
44047
  if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
43848
44048
  const jobId = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
@@ -43878,7 +44078,7 @@ var init_server = __esm({
43878
44078
  serpOnly: body.serpOnly ?? false,
43879
44079
  pages: Math.min(2, Math.max(1, body.pages ?? 1))
43880
44080
  };
43881
- const syncCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount2(options.maxQuestions);
44081
+ const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount2(options.maxQuestions);
43882
44082
  const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
43883
44083
  if (!syncOk) return c.json(insufficientBalanceResponse(syncBal, syncCost), 402);
43884
44084
  const jobId = await createRunningJob(user.id, options.query, options);
@@ -43888,10 +44088,11 @@ var init_server = __esm({
43888
44088
  c.req.raw.signal,
43889
44089
  AbortSignal.timeout(syncTimeoutMs)
43890
44090
  ]);
44091
+ const headlessSentOut = { value: null };
43891
44092
  try {
43892
44093
  const harvestCtx = currentCostContext();
43893
44094
  const result = await runWithCostContext(
43894
- { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
44095
+ { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id, headlessSentOut },
43895
44096
  () => harvest({
43896
44097
  ...options,
43897
44098
  kernelApiKey: browserServiceApiKey(),
@@ -43910,6 +44111,11 @@ var init_server = __esm({
43910
44111
  const diff = syncCost - actualCost;
43911
44112
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.PAA_REFUND, "overestimate refund");
43912
44113
  else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.PAA, options.query);
44114
+ } else {
44115
+ const actualCost = headlessSentOut.value === true ? MC_COSTS.serp_headless : MC_COSTS.serp_headful;
44116
+ const diff = syncCost - actualCost;
44117
+ if (diff > 0) await creditMc(user.id, diff, LedgerOperation.SERP_REFUND, "headless-mode pricing settle");
44118
+ else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.SERP, options.query);
43913
44119
  }
43914
44120
  return c.json({ job_id: jobId, status: "done", result: sanitizeHarvestResult(result), attempts: sanitizeAttempts(attempts) });
43915
44121
  } catch (err) {