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.
- package/README.md +1 -1
- package/dist/bin/api-server.cjs +385 -179
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +6 -4
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +5 -5
- package/dist/bin/paa-harvest.cjs +1 -0
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-JBME5WJR.js → chunk-2P5VFIJM.js} +2 -2
- package/dist/{chunk-GMZFQRH2.js → chunk-5IU5ZFVO.js} +2 -2
- package/dist/chunk-5IU5ZFVO.js.map +1 -0
- package/dist/{chunk-ZNNYPE2U.js → chunk-7LO6KLKD.js} +4 -4
- package/dist/{chunk-DZBGULWW.js → chunk-H7APH5UG.js} +81 -1
- package/dist/chunk-H7APH5UG.js.map +1 -0
- package/dist/{chunk-ZJDANZTQ.js → chunk-JJ5RMEGX.js} +3 -2
- package/dist/chunk-JJ5RMEGX.js.map +1 -0
- package/dist/{chunk-2FF4GBQ4.js → chunk-OBNKOYOE.js} +44 -4
- package/dist/chunk-OBNKOYOE.js.map +1 -0
- package/dist/chunk-VYUWWIVR.js +7 -0
- package/dist/chunk-VYUWWIVR.js.map +1 -0
- package/dist/{chunk-X3NIXHHG.js → chunk-XDRHBGJR.js} +3 -3
- package/dist/{db-EONGTNJM.js → db-Z35S224E.js} +4 -2
- package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-4RO3YU4Q.js} +2 -2
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-CHTX6YQA.js → server-PBBRSLBP.js} +268 -186
- package/dist/server-PBBRSLBP.js.map +1 -0
- package/dist/{site-extract-repository-5V5EEQ72.js → site-extract-repository-FDH6ZXKT.js} +4 -4
- package/dist/{worker-GE5JMHE6.js → worker-BXJQ7J4N.js} +13 -7
- package/dist/worker-BXJQ7J4N.js.map +1 -0
- package/docs/mcp-tool-manifest.generated.json +2 -2
- package/package.json +1 -1
- package/dist/chunk-2FF4GBQ4.js.map +0 -1
- package/dist/chunk-DZBGULWW.js.map +0 -1
- package/dist/chunk-GMZFQRH2.js.map +0 -1
- package/dist/chunk-NBF4F4UT.js +0 -7
- package/dist/chunk-NBF4F4UT.js.map +0 -1
- package/dist/chunk-ZJDANZTQ.js.map +0 -1
- package/dist/server-CHTX6YQA.js.map +0 -1
- package/dist/worker-GE5JMHE6.js.map +0 -1
- /package/dist/{chunk-JBME5WJR.js.map → chunk-2P5VFIJM.js.map} +0 -0
- /package/dist/{chunk-ZNNYPE2U.js.map → chunk-7LO6KLKD.js.map} +0 -0
- /package/dist/{chunk-X3NIXHHG.js.map → chunk-XDRHBGJR.js.map} +0 -0
- /package/dist/{db-EONGTNJM.js.map → db-Z35S224E.js.map} +0 -0
- /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-4RO3YU4Q.js.map} +0 -0
- /package/dist/{site-extract-repository-5V5EEQ72.js.map → site-extract-repository-FDH6ZXKT.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -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.
|
|
8580
|
-
unit: "per search",
|
|
8581
|
-
notes:
|
|
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
|
-
|
|
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
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
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)
|
|
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
|
|
12042
|
-
credits:
|
|
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
|
-
|
|
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
|
|
12143
|
-
const
|
|
12144
|
-
|
|
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
|
|
12152
|
-
const existing = await eventByKey(
|
|
12153
|
-
if (existing) {
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
}
|
|
12158
|
-
return
|
|
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 (
|
|
12161
|
-
throw new UnifiedBillingError(
|
|
12281
|
+
if (!hasPaidSchedulingAccess(user)) {
|
|
12282
|
+
throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
|
|
12162
12283
|
}
|
|
12163
|
-
if (
|
|
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 = '
|
|
12293
|
+
sql: "UPDATE billing_authorizations SET status = 'starting' WHERE id = ? AND status = 'authorized'",
|
|
12166
12294
|
args: [authorization.id]
|
|
12167
12295
|
});
|
|
12168
|
-
|
|
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 (
|
|
12175
|
-
throw new UnifiedBillingError("
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
12309
|
+
startKey,
|
|
12190
12310
|
SCHEDULED_RUN_BILLING_CLASS,
|
|
12191
12311
|
SCHEDULED_RUN_SOURCE_SURFACE,
|
|
12192
|
-
|
|
12193
|
-
rawCostNanos,
|
|
12312
|
+
SCHEDULED_RUN_BASE_MC,
|
|
12194
12313
|
SCHEDULED_AGENT_LLM_MARKUP_BPS,
|
|
12195
|
-
|
|
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
|
|
12318
|
+
const debit = await debitMcIdempotent(
|
|
12249
12319
|
user.id,
|
|
12250
|
-
|
|
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 = '
|
|
12327
|
+
sql: "UPDATE billing_authorizations SET status = 'authorized' WHERE id = ? AND status = 'starting'",
|
|
12262
12328
|
args: [authorization.id]
|
|
12263
12329
|
});
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
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
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
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: [
|
|
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
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
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
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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) {
|