mtok-relay 0.2.1 → 0.2.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/dist/mtok-relay.mjs +133 -36
- package/package.json +1 -1
package/dist/mtok-relay.mjs
CHANGED
|
@@ -2016,14 +2016,14 @@ function weierstrass(curveDef) {
|
|
|
2016
2016
|
const sg = signature;
|
|
2017
2017
|
msgHash = ensureBytes("msgHash", msgHash);
|
|
2018
2018
|
publicKey = ensureBytes("publicKey", publicKey);
|
|
2019
|
-
const { lowS, prehash, format } = opts;
|
|
2019
|
+
const { lowS, prehash, format: format2 } = opts;
|
|
2020
2020
|
validateSigVerOpts(opts);
|
|
2021
2021
|
if ("strict" in opts)
|
|
2022
2022
|
throw new Error("options.strict was renamed to lowS");
|
|
2023
|
-
if (
|
|
2023
|
+
if (format2 !== void 0 && format2 !== "compact" && format2 !== "der")
|
|
2024
2024
|
throw new Error("format must be compact or der");
|
|
2025
2025
|
const isHex2 = typeof sg === "string" || isBytes2(sg);
|
|
2026
|
-
const isObj = !isHex2 && !
|
|
2026
|
+
const isObj = !isHex2 && !format2 && typeof sg === "object" && sg !== null && typeof sg.r === "bigint" && typeof sg.s === "bigint";
|
|
2027
2027
|
if (!isHex2 && !isObj)
|
|
2028
2028
|
throw new Error("invalid signature, expected Uint8Array, hex string or Signature instance");
|
|
2029
2029
|
let _sig = void 0;
|
|
@@ -2033,13 +2033,13 @@ function weierstrass(curveDef) {
|
|
|
2033
2033
|
_sig = new Signature(sg.r, sg.s);
|
|
2034
2034
|
if (isHex2) {
|
|
2035
2035
|
try {
|
|
2036
|
-
if (
|
|
2036
|
+
if (format2 !== "compact")
|
|
2037
2037
|
_sig = Signature.fromDER(sg);
|
|
2038
2038
|
} catch (derError) {
|
|
2039
2039
|
if (!(derError instanceof DER.Err))
|
|
2040
2040
|
throw derError;
|
|
2041
2041
|
}
|
|
2042
|
-
if (!_sig &&
|
|
2042
|
+
if (!_sig && format2 !== "der")
|
|
2043
2043
|
_sig = Signature.fromCompact(sg);
|
|
2044
2044
|
}
|
|
2045
2045
|
P = Point.fromHex(publicKey);
|
|
@@ -2157,7 +2157,7 @@ var secp256k1 = createCurve({
|
|
|
2157
2157
|
}, sha256);
|
|
2158
2158
|
|
|
2159
2159
|
// node_modules/viem/_esm/errors/version.js
|
|
2160
|
-
var version = "2.
|
|
2160
|
+
var version = "2.56.0";
|
|
2161
2161
|
|
|
2162
2162
|
// node_modules/viem/_esm/errors/base.js
|
|
2163
2163
|
var errorConfig = {
|
|
@@ -3215,14 +3215,17 @@ async function signMessage({ message, privateKey }) {
|
|
|
3215
3215
|
return await sign({ hash: hashMessage(message), privateKey, to: "hex" });
|
|
3216
3216
|
}
|
|
3217
3217
|
|
|
3218
|
-
// node_modules/viem/_esm/
|
|
3219
|
-
var
|
|
3220
|
-
|
|
3221
|
-
|
|
3218
|
+
// node_modules/viem/_esm/utils/unit/Value.js
|
|
3219
|
+
var exponents = {
|
|
3220
|
+
wei: 0,
|
|
3221
|
+
gwei: 9,
|
|
3222
|
+
szabo: 12,
|
|
3223
|
+
finney: 15,
|
|
3224
|
+
ether: 18
|
|
3222
3225
|
};
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
+
function format(value, decimals = 0) {
|
|
3227
|
+
if (!Number.isInteger(decimals) || decimals < 0)
|
|
3228
|
+
throw new InvalidDecimalsError({ decimals });
|
|
3226
3229
|
let display = value.toString();
|
|
3227
3230
|
const negative = display.startsWith("-");
|
|
3228
3231
|
if (negative)
|
|
@@ -3235,10 +3238,24 @@ function formatUnits(value, decimals) {
|
|
|
3235
3238
|
fraction = fraction.replace(/(0+)$/, "");
|
|
3236
3239
|
return `${negative ? "-" : ""}${integer || "0"}${fraction ? `.${fraction}` : ""}`;
|
|
3237
3240
|
}
|
|
3241
|
+
function formatGwei(wei, unit = "wei") {
|
|
3242
|
+
return format(wei, exponents.gwei - exponents[unit]);
|
|
3243
|
+
}
|
|
3244
|
+
var InvalidDecimalsError = class extends Error {
|
|
3245
|
+
constructor({ decimals }) {
|
|
3246
|
+
super(`\`decimals\` must be a non-negative integer. Got \`${decimals}\`.`);
|
|
3247
|
+
Object.defineProperty(this, "name", {
|
|
3248
|
+
enumerable: true,
|
|
3249
|
+
configurable: true,
|
|
3250
|
+
writable: true,
|
|
3251
|
+
value: "Value.InvalidDecimalsError"
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3238
3255
|
|
|
3239
3256
|
// node_modules/viem/_esm/utils/unit/formatGwei.js
|
|
3240
|
-
function
|
|
3241
|
-
return
|
|
3257
|
+
function formatGwei2(wei, unit = "wei") {
|
|
3258
|
+
return formatGwei(wei, unit);
|
|
3242
3259
|
}
|
|
3243
3260
|
|
|
3244
3261
|
// node_modules/viem/_esm/errors/transaction.js
|
|
@@ -3591,7 +3608,7 @@ Object.defineProperty(ExecutionRevertedError, "nodeMessage", {
|
|
|
3591
3608
|
});
|
|
3592
3609
|
var FeeCapTooHighError = class extends BaseError {
|
|
3593
3610
|
constructor({ cause, maxFeePerGas } = {}) {
|
|
3594
|
-
super(`The fee cap (\`maxFeePerGas\`${maxFeePerGas ? ` = ${
|
|
3611
|
+
super(`The fee cap (\`maxFeePerGas\`${maxFeePerGas ? ` = ${formatGwei2(maxFeePerGas)} gwei` : ""}) cannot be higher than the maximum allowed value (2^256-1).`, {
|
|
3595
3612
|
cause,
|
|
3596
3613
|
name: "FeeCapTooHighError"
|
|
3597
3614
|
});
|
|
@@ -3605,7 +3622,7 @@ Object.defineProperty(FeeCapTooHighError, "nodeMessage", {
|
|
|
3605
3622
|
});
|
|
3606
3623
|
var FeeCapTooLowError = class extends BaseError {
|
|
3607
3624
|
constructor({ cause, maxFeePerGas } = {}) {
|
|
3608
|
-
super(`The fee cap (\`maxFeePerGas\`${maxFeePerGas ? ` = ${
|
|
3625
|
+
super(`The fee cap (\`maxFeePerGas\`${maxFeePerGas ? ` = ${formatGwei2(maxFeePerGas)}` : ""} gwei) cannot be lower than the block base fee.`, {
|
|
3609
3626
|
cause,
|
|
3610
3627
|
name: "FeeCapTooLowError"
|
|
3611
3628
|
});
|
|
@@ -3724,7 +3741,7 @@ Object.defineProperty(TransactionTypeNotSupportedError, "nodeMessage", {
|
|
|
3724
3741
|
var TipAboveFeeCapError = class extends BaseError {
|
|
3725
3742
|
constructor({ cause, maxPriorityFeePerGas, maxFeePerGas } = {}) {
|
|
3726
3743
|
super([
|
|
3727
|
-
`The provided tip (\`maxPriorityFeePerGas\`${maxPriorityFeePerGas ? ` = ${
|
|
3744
|
+
`The provided tip (\`maxPriorityFeePerGas\`${maxPriorityFeePerGas ? ` = ${formatGwei2(maxPriorityFeePerGas)} gwei` : ""}) cannot be higher than the fee cap (\`maxFeePerGas\`${maxFeePerGas ? ` = ${formatGwei2(maxFeePerGas)} gwei` : ""}).`
|
|
3728
3745
|
].join("\n"), {
|
|
3729
3746
|
cause,
|
|
3730
3747
|
name: "TipAboveFeeCapError"
|
|
@@ -4415,6 +4432,15 @@ var InvalidStructTypeError = class extends BaseError {
|
|
|
4415
4432
|
});
|
|
4416
4433
|
}
|
|
4417
4434
|
};
|
|
4435
|
+
var InvalidTypedDataTypeError = class extends BaseError {
|
|
4436
|
+
constructor({ type }) {
|
|
4437
|
+
const canonicalType = type.replace(/^(u?int)/, "$&256");
|
|
4438
|
+
super(`Type "${type}" is not a valid EIP-712 type.`, {
|
|
4439
|
+
metaMessages: [`Use "${canonicalType}" instead.`],
|
|
4440
|
+
name: "InvalidTypedDataTypeError"
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
};
|
|
4418
4444
|
|
|
4419
4445
|
// node_modules/viem/_esm/utils/typedData.js
|
|
4420
4446
|
function validateTypedData(parameters) {
|
|
@@ -4423,6 +4449,9 @@ function validateTypedData(parameters) {
|
|
|
4423
4449
|
for (const param of struct) {
|
|
4424
4450
|
const { name, type } = param;
|
|
4425
4451
|
const value = data[name];
|
|
4452
|
+
const baseType = type.replace(/(\[[0-9]*\])+$/, "");
|
|
4453
|
+
if (baseType === "int" || baseType === "uint")
|
|
4454
|
+
throw new InvalidTypedDataTypeError({ type });
|
|
4426
4455
|
const integerMatch = type.match(integerRegex);
|
|
4427
4456
|
if (integerMatch && (typeof value === "number" || typeof value === "bigint")) {
|
|
4428
4457
|
const [_type, base, size_] = integerMatch;
|
|
@@ -4653,6 +4682,11 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
|
|
|
4653
4682
|
const redemptionFile = redemptionFlag === void 0 ? "./.mtok-redemption.jsonl" : redemptionFlag;
|
|
4654
4683
|
const denylistRaw = flag(argv, "--payer-denylist") ?? env.RELAY_PAYER_DENYLIST ?? "";
|
|
4655
4684
|
const payerDenylist = String(denylistRaw).split(",").map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
4685
|
+
const maxOutputRaw = flag(argv, "--max-output-tokens") ?? env.RELAY_MAX_OUTPUT_TOKENS;
|
|
4686
|
+
const maxOutputTokens = maxOutputRaw != null ? Number(maxOutputRaw) : void 0;
|
|
4687
|
+
if (maxOutputTokens != null && (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 1)) {
|
|
4688
|
+
throw new Error("--max-output-tokens must be a positive integer");
|
|
4689
|
+
}
|
|
4656
4690
|
if (!offerId) throw new Error("--offer <id> is required");
|
|
4657
4691
|
if (!model) throw new Error("--model <id> is required (the offer model you serve)");
|
|
4658
4692
|
if (!upstream) throw new Error("--upstream <url> is required");
|
|
@@ -4691,7 +4725,8 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
|
|
|
4691
4725
|
mtokApiKey,
|
|
4692
4726
|
upstreamKey,
|
|
4693
4727
|
settlementAddr,
|
|
4694
|
-
payerDenylist
|
|
4728
|
+
payerDenylist,
|
|
4729
|
+
maxOutputTokens
|
|
4695
4730
|
};
|
|
4696
4731
|
}
|
|
4697
4732
|
|
|
@@ -4912,7 +4947,17 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
|
|
|
4912
4947
|
if (map.has(key)) return false;
|
|
4913
4948
|
if (!markClaimed(markerKey)) return false;
|
|
4914
4949
|
const entry = { state: "pending", at: now() };
|
|
4915
|
-
|
|
4950
|
+
try {
|
|
4951
|
+
append(key, entry);
|
|
4952
|
+
} catch (e) {
|
|
4953
|
+
if (durable) {
|
|
4954
|
+
try {
|
|
4955
|
+
fs.unlinkSync(markerFor(markerKey));
|
|
4956
|
+
} catch {
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
throw e;
|
|
4960
|
+
}
|
|
4916
4961
|
map.set(key, entry);
|
|
4917
4962
|
return true;
|
|
4918
4963
|
},
|
|
@@ -5267,8 +5312,8 @@ function validateRequest(request, model, { legacy = false } = {}) {
|
|
|
5267
5312
|
}
|
|
5268
5313
|
let validResponseFormat = false;
|
|
5269
5314
|
if (request.response_format != null) {
|
|
5270
|
-
const
|
|
5271
|
-
validResponseFormat = !!
|
|
5315
|
+
const format2 = request.response_format;
|
|
5316
|
+
validResponseFormat = !!format2 && typeof format2 === "object" && !Array.isArray(format2) && Object.keys(format2).length === 1 && ["json_object", "text"].includes(format2.type);
|
|
5272
5317
|
if (!legacy && !validResponseFormat) {
|
|
5273
5318
|
return { error: 'response_format must be exactly { type: "json_object" } or { type: "text" }' };
|
|
5274
5319
|
}
|
|
@@ -5283,8 +5328,21 @@ function validateRequest(request, model, { legacy = false } = {}) {
|
|
|
5283
5328
|
}
|
|
5284
5329
|
};
|
|
5285
5330
|
}
|
|
5331
|
+
function normalizeModelId(m) {
|
|
5332
|
+
return String(m ?? "").toLowerCase().split("/").pop().replace(/^@/, "");
|
|
5333
|
+
}
|
|
5334
|
+
function modelsCompatible(upstreamModel, offerModel) {
|
|
5335
|
+
const a = normalizeModelId(upstreamModel);
|
|
5336
|
+
const b = normalizeModelId(offerModel);
|
|
5337
|
+
if (!a || !b) return false;
|
|
5338
|
+
if (a === b) return true;
|
|
5339
|
+
const [longer, shorter] = a.length >= b.length ? [a, b] : [b, a];
|
|
5340
|
+
if (!longer.startsWith(shorter)) return false;
|
|
5341
|
+
const rest = longer.slice(shorter.length);
|
|
5342
|
+
return /^([-._]\d+)+$/.test(rest);
|
|
5343
|
+
}
|
|
5286
5344
|
function enforceModelEcho(upstreamModel, offerModel) {
|
|
5287
|
-
if (
|
|
5345
|
+
if (!modelsCompatible(upstreamModel, offerModel))
|
|
5288
5346
|
throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
|
|
5289
5347
|
}
|
|
5290
5348
|
function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
|
|
@@ -5293,17 +5351,20 @@ function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
|
|
|
5293
5351
|
return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
|
|
5294
5352
|
}
|
|
5295
5353
|
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
5354
|
+
var BYTES_PER_TOKEN_EST = 3.2;
|
|
5296
5355
|
function estimateInputTokens(messages) {
|
|
5297
5356
|
const utf8 = new TextEncoder();
|
|
5298
|
-
let
|
|
5357
|
+
let bytes = 0;
|
|
5358
|
+
let envelope = 3;
|
|
5299
5359
|
for (const m of messages ?? []) {
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5360
|
+
envelope += MESSAGE_OVERHEAD_TOKENS;
|
|
5361
|
+
bytes += utf8.encode(String(m?.role ?? "")).length;
|
|
5362
|
+
bytes += utf8.encode(typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? null)).length;
|
|
5303
5363
|
}
|
|
5304
|
-
return
|
|
5364
|
+
return envelope + Math.ceil(bytes / BYTES_PER_TOKEN_EST);
|
|
5305
5365
|
}
|
|
5306
|
-
|
|
5366
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 32768;
|
|
5367
|
+
function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = DEFAULT_MAX_OUTPUT_TOKENS }) {
|
|
5307
5368
|
const estIn = estimateInputTokens(messages);
|
|
5308
5369
|
const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
|
|
5309
5370
|
if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
|
|
@@ -5331,9 +5392,17 @@ function createServeCore({
|
|
|
5331
5392
|
dripContractAddress,
|
|
5332
5393
|
feeRecipient,
|
|
5333
5394
|
feeBps,
|
|
5334
|
-
screenPayer
|
|
5395
|
+
screenPayer,
|
|
5396
|
+
// #654: the output-token sanity ceiling for boundServe. The PAID budget already
|
|
5397
|
+
// bounds output (and metering is on real usage), so this is a defensive cap on a
|
|
5398
|
+
// single generation, not a money guard. It is a per-relay knob: the reference host
|
|
5399
|
+
// passes MTOK_MAX_OUTPUT_TOKENS / a config value; operators serving large-context
|
|
5400
|
+
// models raise it. Falls back to boundServe's own generous default when unset.
|
|
5401
|
+
maxOutputTokens
|
|
5335
5402
|
}) {
|
|
5336
5403
|
const serve = async (body) => {
|
|
5404
|
+
const currentFeeBps = typeof feeBps === "function" ? feeBps() : feeBps;
|
|
5405
|
+
const currentFeeRecipient = typeof feeRecipient === "function" ? feeRecipient() : feeRecipient;
|
|
5337
5406
|
const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
|
|
5338
5407
|
const hasRequestNonce = Object.hasOwn(body, "requestNonce");
|
|
5339
5408
|
if (!bookingId) return { status: 400, body: { error: "bad_request", detail: "DRAW needs bookingId" } };
|
|
@@ -5367,7 +5436,7 @@ function createServeCore({
|
|
|
5367
5436
|
n,
|
|
5368
5437
|
requestHash,
|
|
5369
5438
|
sellerWallet,
|
|
5370
|
-
feeRecipient,
|
|
5439
|
+
feeRecipient: currentFeeRecipient,
|
|
5371
5440
|
// #580: refuse a payment older than the redemption window. The JSONL
|
|
5372
5441
|
// payload cache AND the claim markers are both aged out at boot (#600),
|
|
5373
5442
|
// so past retention this age bound is the sole replay defense (its
|
|
@@ -5381,8 +5450,8 @@ function createServeCore({
|
|
|
5381
5450
|
if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
|
|
5382
5451
|
const expectedFee = configuredFeeAtomic({
|
|
5383
5452
|
sellerUsdAtomic: paid.event.sellerUsdAtomic,
|
|
5384
|
-
feeAddress:
|
|
5385
|
-
feeBps
|
|
5453
|
+
feeAddress: currentFeeRecipient,
|
|
5454
|
+
feeBps: currentFeeBps
|
|
5386
5455
|
});
|
|
5387
5456
|
if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
|
|
5388
5457
|
return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
|
|
@@ -5420,7 +5489,7 @@ function createServeCore({
|
|
|
5420
5489
|
const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
|
|
5421
5490
|
const boundInPrice = Math.max(Number(inPrice) || 0, eventInPriceUsd);
|
|
5422
5491
|
const boundOutPrice = Math.max(Number(outPrice) || 0, eventOutPriceUsd);
|
|
5423
|
-
const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice: boundInPrice, outPrice: boundOutPrice, reqMax: checked.safeRequest.max_tokens });
|
|
5492
|
+
const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice: boundInPrice, outPrice: boundOutPrice, reqMax: checked.safeRequest.max_tokens, ...Number(maxOutputTokens) > 0 ? { contextCeil: Math.floor(Number(maxOutputTokens)) } : {} });
|
|
5424
5493
|
if (bound.refuse) {
|
|
5425
5494
|
const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
|
|
5426
5495
|
return { status: 402, body: { error, detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) leaves no safely funded output in the paid amount ($${remainingUsd})`, _bookingId: bookingId, remainingUsd } };
|
|
@@ -5444,6 +5513,7 @@ function createServeCore({
|
|
|
5444
5513
|
} catch (e) {
|
|
5445
5514
|
return { status: 502, body: { error: "model_mismatch", detail: e.message } };
|
|
5446
5515
|
}
|
|
5516
|
+
if (completion && typeof completion === "object") completion.model = model;
|
|
5447
5517
|
const usage = completion.usage ?? {};
|
|
5448
5518
|
const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
|
|
5449
5519
|
const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
|
|
@@ -5501,6 +5571,24 @@ async function createRelayRuntime(config) {
|
|
|
5501
5571
|
const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
|
|
5502
5572
|
const drawLocks = /* @__PURE__ */ new Map();
|
|
5503
5573
|
const platform = await fetchPlatformConfig(config);
|
|
5574
|
+
const bootFeeBps = platform.feeBps;
|
|
5575
|
+
const FEE_REFRESH_MS = 6e4;
|
|
5576
|
+
let lastConfigFetch = Date.now();
|
|
5577
|
+
const refreshPlatformFee = async () => {
|
|
5578
|
+
lastConfigFetch = Date.now();
|
|
5579
|
+
try {
|
|
5580
|
+
const fresh = await fetchPlatformConfig(config);
|
|
5581
|
+
platform.feeBps = fresh.feeBps;
|
|
5582
|
+
return true;
|
|
5583
|
+
} catch (e) {
|
|
5584
|
+
(config.log ?? console).error?.(`mtok relay: platform config refresh failed (${e.message}); keeping last-known fee rate`);
|
|
5585
|
+
return false;
|
|
5586
|
+
}
|
|
5587
|
+
};
|
|
5588
|
+
const refreshPlatformFeeIfStale = async () => {
|
|
5589
|
+
if (Date.now() - lastConfigFetch < FEE_REFRESH_MS) return;
|
|
5590
|
+
await refreshPlatformFee();
|
|
5591
|
+
};
|
|
5504
5592
|
const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
|
|
5505
5593
|
if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
|
|
5506
5594
|
const payerDenylist = new Set((config.payerDenylist ?? []).map((a) => String(a).trim().toLowerCase()).filter(Boolean));
|
|
@@ -5524,8 +5612,13 @@ async function createRelayRuntime(config) {
|
|
|
5524
5612
|
sellerAgentId: config.sellerAgentId,
|
|
5525
5613
|
sellerWallet: config.settlementAddr,
|
|
5526
5614
|
dripContractAddress: platform.dripContractAddress,
|
|
5615
|
+
// #654: the fee floor is min(boot, current) so neither a fee increase nor a
|
|
5616
|
+
// decrease can over-demand and strand an honest already-paid draw; the recipient
|
|
5617
|
+
// stays pinned to the boot address (exact-match verify, see the note above).
|
|
5527
5618
|
feeRecipient: platform.feeAddress,
|
|
5528
|
-
feeBps: platform.feeBps,
|
|
5619
|
+
feeBps: () => Math.min(bootFeeBps, platform.feeBps),
|
|
5620
|
+
// #654: per-relay output sanity ceiling (unset => the shared generous default).
|
|
5621
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
5529
5622
|
screenPayer: payerDenied
|
|
5530
5623
|
});
|
|
5531
5624
|
const withBookingLock = async (bookingId, fn) => {
|
|
@@ -5547,7 +5640,11 @@ async function createRelayRuntime(config) {
|
|
|
5547
5640
|
}
|
|
5548
5641
|
};
|
|
5549
5642
|
const handleDraw = (body, res) => withBookingLock(String(body?.bookingId ?? ""), async () => {
|
|
5550
|
-
|
|
5643
|
+
await refreshPlatformFeeIfStale();
|
|
5644
|
+
let out = await core.serve(body);
|
|
5645
|
+
if (out.status === 402 && out.body?.detail === "fee_amount_too_low" && await refreshPlatformFee()) {
|
|
5646
|
+
out = await core.serve(body);
|
|
5647
|
+
}
|
|
5551
5648
|
return send(res, out.status, out.body);
|
|
5552
5649
|
});
|
|
5553
5650
|
return { handleDraw };
|
package/package.json
CHANGED