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.
Files changed (2) hide show
  1. package/dist/mtok-relay.mjs +133 -36
  2. package/package.json +1 -1
@@ -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 (format !== void 0 && format !== "compact" && format !== "der")
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 && !format && typeof sg === "object" && sg !== null && typeof sg.r === "bigint" && typeof sg.s === "bigint";
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 (format !== "compact")
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 && format !== "der")
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.55.1";
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/constants/unit.js
3219
- var gweiUnits = {
3220
- ether: -9,
3221
- wei: 9
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
- // node_modules/viem/_esm/utils/unit/formatUnits.js
3225
- function formatUnits(value, decimals) {
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 formatGwei(wei, unit = "wei") {
3241
- return formatUnits(wei, gweiUnits[unit]);
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 ? ` = ${formatGwei(maxFeePerGas)} gwei` : ""}) cannot be higher than the maximum allowed value (2^256-1).`, {
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 ? ` = ${formatGwei(maxFeePerGas)}` : ""} gwei) cannot be lower than the block base fee.`, {
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 ? ` = ${formatGwei(maxPriorityFeePerGas)} gwei` : ""}) cannot be higher than the fee cap (\`maxFeePerGas\`${maxFeePerGas ? ` = ${formatGwei(maxFeePerGas)} gwei` : ""}).`
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
- append(key, entry);
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 format = request.response_format;
5271
- validResponseFormat = !!format && typeof format === "object" && !Array.isArray(format) && Object.keys(format).length === 1 && ["json_object", "text"].includes(format.type);
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 (String(upstreamModel) !== String(offerModel))
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 tokens = 3;
5357
+ let bytes = 0;
5358
+ let envelope = 3;
5299
5359
  for (const m of messages ?? []) {
5300
- tokens += MESSAGE_OVERHEAD_TOKENS;
5301
- tokens += utf8.encode(String(m?.role ?? "")).length;
5302
- tokens += utf8.encode(typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? null)).length;
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 tokens;
5364
+ return envelope + Math.ceil(bytes / BYTES_PER_TOKEN_EST);
5305
5365
  }
5306
- function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
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: feeRecipient,
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
- const out = await core.serve(body);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control.",
5
5
  "type": "module",
6
6
  "bin": {