mtok-relay 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mtok-relay.mjs +294 -224
  2. package/package.json +1 -1
@@ -3,7 +3,7 @@
3
3
 
4
4
  // node_modules/@noble/hashes/esm/cryptoNode.js
5
5
  import * as nc from "node:crypto";
6
- var crypto = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
6
+ var crypto2 = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
7
7
 
8
8
  // node_modules/@noble/hashes/esm/utils.js
9
9
  function isBytes(a) {
@@ -100,11 +100,11 @@ function createHasher(hashCons) {
100
100
  return hashC;
101
101
  }
102
102
  function randomBytes(bytesLength = 32) {
103
- if (crypto && typeof crypto.getRandomValues === "function") {
104
- return crypto.getRandomValues(new Uint8Array(bytesLength));
103
+ if (crypto2 && typeof crypto2.getRandomValues === "function") {
104
+ return crypto2.getRandomValues(new Uint8Array(bytesLength));
105
105
  }
106
- if (crypto && typeof crypto.randomBytes === "function") {
107
- return Uint8Array.from(crypto.randomBytes(bytesLength));
106
+ if (crypto2 && typeof crypto2.randomBytes === "function") {
107
+ return Uint8Array.from(crypto2.randomBytes(bytesLength));
108
108
  }
109
109
  throw new Error("crypto.getRandomValues must be defined");
110
110
  }
@@ -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.0";
2160
+ var version = "2.55.10";
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"
@@ -4778,45 +4795,9 @@ function startRelayServer({ config, handleDraw }) {
4778
4795
  return server;
4779
4796
  }
4780
4797
 
4781
- // lib.mjs
4782
- function enforceModelEcho(upstreamModel, offerModel) {
4783
- if (String(upstreamModel) !== String(offerModel))
4784
- throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
4785
- }
4786
- function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
4787
- const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
4788
- if (!feeAddress || bps === 0n) return 0n;
4789
- return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
4790
- }
4791
- var MESSAGE_OVERHEAD_TOKENS = 4;
4792
- function estimateInputTokens(messages) {
4793
- const utf8 = new TextEncoder();
4794
- let tokens = 3;
4795
- for (const m of messages ?? []) {
4796
- tokens += MESSAGE_OVERHEAD_TOKENS;
4797
- tokens += utf8.encode(String(m?.role ?? "")).length;
4798
- tokens += utf8.encode(typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? null)).length;
4799
- }
4800
- return tokens;
4801
- }
4802
- function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
4803
- const estIn = estimateInputTokens(messages);
4804
- const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
4805
- if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
4806
- const outBudgetUsd = budgetUsd - estInCostUsd;
4807
- let maxTok = contextCeil;
4808
- if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
4809
- if (!Number.isFinite(Number(outPrice)) || Number(outPrice) <= 0) {
4810
- return { refuse: true, reason: "output_price", estIn, estInCostUsd };
4811
- }
4812
- maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
4813
- if (maxTok < 1) return { refuse: true, reason: "output", estIn, estInCostUsd };
4814
- return { refuse: false, maxTok, estIn, estInCostUsd };
4815
- }
4816
-
4817
4798
  // src/redemption.mjs
4818
4799
  import fs from "node:fs";
4819
- import crypto2 from "node:crypto";
4800
+ import crypto3 from "node:crypto";
4820
4801
  var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
4821
4802
  function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS, now = () => Date.now(), log = console } = {}) {
4822
4803
  const map = /* @__PURE__ */ new Map();
@@ -4881,6 +4862,14 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4881
4862
  fs.rmSync(compact, { force: true });
4882
4863
  }
4883
4864
  fs.mkdirSync(claimsDir, { recursive: true });
4865
+ const markerCutoff = Date.now() - retentionMs;
4866
+ for (const name of fs.readdirSync(claimsDir)) {
4867
+ try {
4868
+ const marker = `${claimsDir}/${name}`;
4869
+ if (fs.statSync(marker).mtimeMs < markerCutoff) fs.unlinkSync(marker);
4870
+ } catch {
4871
+ }
4872
+ }
4884
4873
  durable = true;
4885
4874
  seenVersion = fileVersion();
4886
4875
  log.log?.(`mtok-relay: durable redemption at ${file} (${map.size} entries loaded)`);
@@ -4897,7 +4886,7 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4897
4886
  syncWrite(file, "a", JSON.stringify(recordFor(key, entry)) + "\n");
4898
4887
  seenVersion = fileVersion();
4899
4888
  };
4900
- const markerFor = (key) => `${claimsDir}/${crypto2.createHash("sha256").update(String(key)).digest("hex")}`;
4889
+ const markerFor = (key) => `${claimsDir}/${crypto3.createHash("sha256").update(String(key)).digest("hex")}`;
4901
4890
  const markClaimed = (key) => {
4902
4891
  if (!durable) throw new Error("durable redemption unavailable");
4903
4892
  const marker = markerFor(key);
@@ -5230,50 +5219,16 @@ function createOnchainVerifier({
5230
5219
  };
5231
5220
  }
5232
5221
 
5233
- // bridge/bridge.mjs
5234
- function httpUpstream({ baseUrl, key }) {
5235
- const url = String(baseUrl || "").replace(/\/$/, "") + "/chat/completions";
5236
- return async (payload) => {
5237
- const res = await fetch(url, {
5238
- method: "POST",
5239
- headers: {
5240
- "content-type": "application/json",
5241
- ...key ? { authorization: `Bearer ${key}` } : {}
5242
- },
5243
- body: JSON.stringify(payload)
5244
- });
5245
- const text = await res.text();
5246
- let json;
5247
- try {
5248
- json = JSON.parse(text);
5249
- } catch {
5250
- throw new Error(`non-JSON upstream response (${res.status})`);
5251
- }
5252
- if (!res.ok) throw new Error(json?.error?.message || `upstream ${res.status}`);
5253
- return json;
5254
- };
5255
- }
5256
-
5257
- // src/rpc.mjs
5258
- var BASE_MAINNET_RPCS = [
5259
- "https://mainnet.base.org",
5260
- "https://base.llamarpc.com",
5261
- "https://base-rpc.publicnode.com",
5262
- "https://base.drpc.org"
5263
- ];
5264
- var BASE_SEPOLIA_RPCS = [
5265
- "https://sepolia.base.org",
5266
- "https://base-sepolia-rpc.publicnode.com"
5267
- ];
5268
- var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
5269
-
5270
- // src/runtime.mjs
5271
- import crypto3 from "node:crypto";
5222
+ // bridge/serve-core.mjs
5272
5223
  var BALANCE_EPSILON = 1e-6;
5273
- var hash32 = (v) => "0x" + crypto3.createHash("sha256").update(typeof v === "string" ? v : JSON.stringify(v ?? null)).digest("hex");
5274
5224
  var REQUEST_NONCE_RE = /^0x[0-9a-fA-F]{32}$/;
5275
5225
  var CHAT_ROLES = /* @__PURE__ */ new Set(["developer", "system", "user", "assistant"]);
5276
5226
  var REQUEST_KEYS = /* @__PURE__ */ new Set(["model", "messages", "max_tokens", "temperature", "response_format", "stream", "n"]);
5227
+ async function hash32(v) {
5228
+ const bytes = new TextEncoder().encode(typeof v === "string" ? v : JSON.stringify(v ?? null));
5229
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
5230
+ return "0x" + [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
5231
+ }
5277
5232
  function legacyContentText(content) {
5278
5233
  const render = (part) => {
5279
5234
  if (typeof part === "string") return part;
@@ -5329,8 +5284,8 @@ function validateRequest(request, model, { legacy = false } = {}) {
5329
5284
  }
5330
5285
  let validResponseFormat = false;
5331
5286
  if (request.response_format != null) {
5332
- const format = request.response_format;
5333
- validResponseFormat = !!format && typeof format === "object" && !Array.isArray(format) && Object.keys(format).length === 1 && ["json_object", "text"].includes(format.type);
5287
+ const format2 = request.response_format;
5288
+ validResponseFormat = !!format2 && typeof format2 === "object" && !Array.isArray(format2) && Object.keys(format2).length === 1 && ["json_object", "text"].includes(format2.type);
5334
5289
  if (!legacy && !validResponseFormat) {
5335
5290
  return { error: 'response_format must be exactly { type: "json_object" } or { type: "text" }' };
5336
5291
  }
@@ -5345,6 +5300,222 @@ function validateRequest(request, model, { legacy = false } = {}) {
5345
5300
  }
5346
5301
  };
5347
5302
  }
5303
+ function enforceModelEcho(upstreamModel, offerModel) {
5304
+ if (String(upstreamModel) !== String(offerModel))
5305
+ throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
5306
+ }
5307
+ function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
5308
+ const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
5309
+ if (!feeAddress || bps === 0n) return 0n;
5310
+ return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
5311
+ }
5312
+ var MESSAGE_OVERHEAD_TOKENS = 4;
5313
+ var BYTES_PER_TOKEN_EST = 3.2;
5314
+ function estimateInputTokens(messages) {
5315
+ const utf8 = new TextEncoder();
5316
+ let bytes = 0;
5317
+ let envelope = 3;
5318
+ for (const m of messages ?? []) {
5319
+ envelope += MESSAGE_OVERHEAD_TOKENS;
5320
+ bytes += utf8.encode(String(m?.role ?? "")).length;
5321
+ bytes += utf8.encode(typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? null)).length;
5322
+ }
5323
+ return envelope + Math.ceil(bytes / BYTES_PER_TOKEN_EST);
5324
+ }
5325
+ function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
5326
+ const estIn = estimateInputTokens(messages);
5327
+ const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
5328
+ if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
5329
+ const outBudgetUsd = budgetUsd - estInCostUsd;
5330
+ let maxTok = contextCeil;
5331
+ if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
5332
+ if (!Number.isFinite(Number(outPrice)) || Number(outPrice) <= 0) {
5333
+ return { refuse: true, reason: "output_price", estIn, estInCostUsd };
5334
+ }
5335
+ maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
5336
+ if (maxTok < 1) return { refuse: true, reason: "output", estIn, estInCostUsd };
5337
+ return { refuse: false, maxTok, estIn, estInCostUsd };
5338
+ }
5339
+ function createServeCore({
5340
+ model,
5341
+ inPrice,
5342
+ outPrice,
5343
+ verifier,
5344
+ redemption,
5345
+ upstream,
5346
+ log,
5347
+ offerId,
5348
+ sellerAgentId,
5349
+ sellerWallet,
5350
+ dripContractAddress,
5351
+ feeRecipient,
5352
+ feeBps,
5353
+ screenPayer
5354
+ }) {
5355
+ const serve = async (body) => {
5356
+ const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
5357
+ const hasRequestNonce = Object.hasOwn(body, "requestNonce");
5358
+ if (!bookingId) return { status: 400, body: { error: "bad_request", detail: "DRAW needs bookingId" } };
5359
+ if (n == null) return { status: 400, body: { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" } };
5360
+ if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295) {
5361
+ return { status: 400, body: { error: "bad_request", detail: "DRAW delivery index n must be a nonnegative uint32 integer" } };
5362
+ }
5363
+ if (hasRequestNonce && !REQUEST_NONCE_RE.test(requestNonce)) {
5364
+ return { status: 400, body: { error: "bad_request", detail: "DRAW needs requestNonce as 16 random bytes encoded as 0x-prefixed hex" } };
5365
+ }
5366
+ const checked = validateRequest(request, model, { legacy: !hasRequestNonce });
5367
+ if (checked.error) return { status: 400, body: { error: "bad_request", detail: checked.error } };
5368
+ const requestHashScheme = hasRequestNonce ? "nonce-v1" : "legacy-v0";
5369
+ const requestHash = hasRequestNonce ? await hash32({ request, requestNonce }) : await hash32(request);
5370
+ const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
5371
+ const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
5372
+ if (!dripContractAddress) {
5373
+ return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
5374
+ }
5375
+ if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
5376
+ let paid;
5377
+ try {
5378
+ paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
5379
+ contractAddress: dripContractAddress,
5380
+ buyerAgentId: buyerId,
5381
+ sellerAgentId,
5382
+ // when set, enforces the offer-owner match (#codex review)
5383
+ bookingId,
5384
+ offerId,
5385
+ model,
5386
+ n,
5387
+ requestHash,
5388
+ sellerWallet,
5389
+ feeRecipient,
5390
+ // #580: refuse a payment older than the redemption window. The JSONL
5391
+ // payload cache AND the claim markers are both aged out at boot (#600),
5392
+ // so past retention this age bound is the sole replay defense (its
5393
+ // skip-on-unreadable-block residual is named in redemption.mjs). An
5394
+ // honest retry is seconds-to-minutes old, never days.
5395
+ maxPaidAgeMs: redemption.retentionMs
5396
+ });
5397
+ } catch (e) {
5398
+ return { status: 402, body: { error: "payment_unverified", detail: e.message } };
5399
+ }
5400
+ if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
5401
+ const expectedFee = configuredFeeAtomic({
5402
+ sellerUsdAtomic: paid.event.sellerUsdAtomic,
5403
+ feeAddress: feeRecipient,
5404
+ feeBps
5405
+ });
5406
+ if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5407
+ return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
5408
+ }
5409
+ if (screenPayer) {
5410
+ try {
5411
+ if (await screenPayer(String(paid.from || "").toLowerCase())) {
5412
+ return { status: 403, body: { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" } };
5413
+ }
5414
+ } catch (e) {
5415
+ return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
5416
+ }
5417
+ }
5418
+ let storedKey = cacheKey;
5419
+ let redemptionState = await redemption.state(storedKey);
5420
+ if (!redemptionState && oldLegacyKey) {
5421
+ const oldLegacyState = await redemption.state(oldLegacyKey);
5422
+ if (oldLegacyState) {
5423
+ storedKey = oldLegacyKey;
5424
+ redemptionState = oldLegacyState;
5425
+ } else {
5426
+ redemptionState = await redemption.state(cacheKey);
5427
+ }
5428
+ }
5429
+ if (redemptionState === "complete") return { status: 200, body: await redemption.get(storedKey) };
5430
+ if (redemptionState === "pending") {
5431
+ return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5432
+ }
5433
+ const paidEvent = paid.event;
5434
+ const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
5435
+ if (remainingUsd < BALANCE_EPSILON) {
5436
+ return { status: 402, body: { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd } };
5437
+ }
5438
+ const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
5439
+ const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
5440
+ const boundInPrice = Math.max(Number(inPrice) || 0, eventInPriceUsd);
5441
+ const boundOutPrice = Math.max(Number(outPrice) || 0, eventOutPriceUsd);
5442
+ const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice: boundInPrice, outPrice: boundOutPrice, reqMax: checked.safeRequest.max_tokens });
5443
+ if (bound.refuse) {
5444
+ const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
5445
+ 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 } };
5446
+ }
5447
+ const safeRequest = { ...checked.safeRequest, model, max_tokens: bound.maxTok };
5448
+ try {
5449
+ if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
5450
+ return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5451
+ }
5452
+ } catch (e) {
5453
+ return { status: 503, body: { error: "redemption_unavailable", detail: `could not durably claim the paid draw: ${e.message}`, _bookingId: bookingId } };
5454
+ }
5455
+ let completion;
5456
+ try {
5457
+ completion = await upstream(safeRequest);
5458
+ } catch (e) {
5459
+ return { status: 502, body: { error: "upstream_error", detail: e.message } };
5460
+ }
5461
+ try {
5462
+ enforceModelEcho(completion.model, model);
5463
+ } catch (e) {
5464
+ return { status: 502, body: { error: "model_mismatch", detail: e.message } };
5465
+ }
5466
+ const usage = completion.usage ?? {};
5467
+ const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
5468
+ const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
5469
+ const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
5470
+ const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
5471
+ try {
5472
+ await redemption.complete(cacheKey, payload);
5473
+ } catch (e) {
5474
+ (log ?? console).error?.(`mtok serve core: completion for ${cacheKey} could not be persisted (${e.message}); retries will remain pending`);
5475
+ }
5476
+ return { status: 200, body: payload };
5477
+ };
5478
+ return { serve };
5479
+ }
5480
+
5481
+ // bridge/bridge.mjs
5482
+ function httpUpstream({ baseUrl, key }) {
5483
+ const url = String(baseUrl || "").replace(/\/$/, "") + "/chat/completions";
5484
+ return async (payload) => {
5485
+ const res = await fetch(url, {
5486
+ method: "POST",
5487
+ headers: {
5488
+ "content-type": "application/json",
5489
+ ...key ? { authorization: `Bearer ${key}` } : {}
5490
+ },
5491
+ body: JSON.stringify(payload)
5492
+ });
5493
+ const text = await res.text();
5494
+ let json;
5495
+ try {
5496
+ json = JSON.parse(text);
5497
+ } catch {
5498
+ throw new Error(`non-JSON upstream response (${res.status})`);
5499
+ }
5500
+ if (!res.ok) throw new Error(json?.error?.message || `upstream ${res.status}`);
5501
+ return json;
5502
+ };
5503
+ }
5504
+
5505
+ // src/rpc.mjs
5506
+ var BASE_MAINNET_RPCS = [
5507
+ "https://mainnet.base.org",
5508
+ "https://base.llamarpc.com",
5509
+ "https://base-rpc.publicnode.com",
5510
+ "https://base.drpc.org"
5511
+ ];
5512
+ var BASE_SEPOLIA_RPCS = [
5513
+ "https://sepolia.base.org",
5514
+ "https://base-sepolia-rpc.publicnode.com"
5515
+ ];
5516
+ var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
5517
+
5518
+ // src/runtime.mjs
5348
5519
  async function createRelayRuntime(config) {
5349
5520
  const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
5350
5521
  const drawLocks = /* @__PURE__ */ new Map();
@@ -5360,6 +5531,22 @@ async function createRelayRuntime(config) {
5360
5531
  if (screenPayer && await screenPayer(payer)) return true;
5361
5532
  return false;
5362
5533
  };
5534
+ const core = createServeCore({
5535
+ model: config.model,
5536
+ inPrice: config.inPrice,
5537
+ outPrice: config.outPrice,
5538
+ verifier,
5539
+ redemption: served,
5540
+ upstream,
5541
+ log: config.log,
5542
+ offerId: config.offerId,
5543
+ sellerAgentId: config.sellerAgentId,
5544
+ sellerWallet: config.settlementAddr,
5545
+ dripContractAddress: platform.dripContractAddress,
5546
+ feeRecipient: platform.feeAddress,
5547
+ feeBps: platform.feeBps,
5548
+ screenPayer: payerDenied
5549
+ });
5363
5550
  const withBookingLock = async (bookingId, fn) => {
5364
5551
  const previous = drawLocks.get(bookingId) || Promise.resolve();
5365
5552
  let release;
@@ -5378,127 +5565,10 @@ async function createRelayRuntime(config) {
5378
5565
  if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
5379
5566
  }
5380
5567
  };
5381
- const handleDraw = async (body, res) => {
5382
- const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
5383
- const hasRequestNonce = Object.hasOwn(body, "requestNonce");
5384
- if (!bookingId) return send(res, 400, { error: "bad_request", detail: "DRAW needs bookingId" });
5385
- if (n == null) return send(res, 400, { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" });
5386
- if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295) {
5387
- return send(res, 400, { error: "bad_request", detail: "DRAW delivery index n must be a nonnegative uint32 integer" });
5388
- }
5389
- if (hasRequestNonce && !REQUEST_NONCE_RE.test(requestNonce)) {
5390
- return send(res, 400, { error: "bad_request", detail: "DRAW needs requestNonce as 16 random bytes encoded as 0x-prefixed hex" });
5391
- }
5392
- const checked = validateRequest(request, config.model, { legacy: !hasRequestNonce });
5393
- if (checked.error) return send(res, 400, { error: "bad_request", detail: checked.error });
5394
- return withBookingLock(bookingId, async () => {
5395
- const requestHashScheme = hasRequestNonce ? "nonce-v1" : "legacy-v0";
5396
- const requestHash = hasRequestNonce ? hash32({ request, requestNonce }) : hash32(request);
5397
- const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
5398
- const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
5399
- if (!platform.dripContractAddress) {
5400
- return send(res, 402, { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" });
5401
- }
5402
- if (!drawPaidTxHash) return send(res, 402, { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" });
5403
- let paid;
5404
- try {
5405
- paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
5406
- contractAddress: platform.dripContractAddress,
5407
- buyerAgentId: buyerId,
5408
- sellerAgentId: config.sellerAgentId,
5409
- // when set, enforces the offer-owner match (#codex review)
5410
- bookingId,
5411
- offerId: config.offerId,
5412
- model: config.model,
5413
- n,
5414
- requestHash,
5415
- sellerWallet: config.settlementAddr,
5416
- feeRecipient: platform.feeAddress,
5417
- // #580: refuse a payment older than the redemption window. The JSONL
5418
- // payload cache is compacted by age (exclusive claim markers remain
5419
- // fail-closed), and an honest retry is seconds-to-minutes old, never days.
5420
- maxPaidAgeMs: served.retentionMs
5421
- });
5422
- } catch (e) {
5423
- return send(res, 402, { error: "payment_unverified", detail: e.message });
5424
- }
5425
- if (!paid?.ok) return send(res, 402, { error: "payment_unverified", detail: paid?.reason || "unknown" });
5426
- const expectedFee = configuredFeeAtomic({
5427
- sellerUsdAtomic: paid.event.sellerUsdAtomic,
5428
- feeAddress: platform.feeAddress,
5429
- feeBps: platform.feeBps
5430
- });
5431
- if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5432
- return send(res, 402, { error: "payment_unverified", detail: "fee_amount_too_low" });
5433
- }
5434
- try {
5435
- if (await payerDenied(String(paid.from || "").toLowerCase())) {
5436
- return send(res, 403, { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" });
5437
- }
5438
- } catch (e) {
5439
- return send(res, 403, { error: "payer_denied", detail: "payer screening failed: " + e.message });
5440
- }
5441
- let storedKey = cacheKey;
5442
- let redemptionState = served.state(storedKey);
5443
- if (!redemptionState && oldLegacyKey) {
5444
- const oldLegacyState = served.state(oldLegacyKey);
5445
- if (oldLegacyState) {
5446
- storedKey = oldLegacyKey;
5447
- redemptionState = oldLegacyState;
5448
- } else {
5449
- redemptionState = served.state(cacheKey);
5450
- }
5451
- }
5452
- if (redemptionState === "complete") return send(res, 200, served.get(storedKey));
5453
- if (redemptionState === "pending") {
5454
- return send(res, 409, { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId });
5455
- }
5456
- const paidEvent = paid.event;
5457
- const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
5458
- if (remainingUsd < BALANCE_EPSILON) {
5459
- return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
5460
- }
5461
- const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
5462
- const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
5463
- const inPrice = Math.max(Number(config.inPrice) || 0, eventInPriceUsd);
5464
- const outPrice = Math.max(Number(config.outPrice) || 0, eventOutPriceUsd);
5465
- const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice, outPrice, reqMax: checked.safeRequest.max_tokens });
5466
- if (bound.refuse) {
5467
- const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
5468
- return send(res, 402, { error, detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) leaves no safely funded output in the paid amount ($${remainingUsd})`, _bookingId: bookingId, remainingUsd });
5469
- }
5470
- const safeRequest = { ...checked.safeRequest, model: config.model, max_tokens: bound.maxTok };
5471
- try {
5472
- if (!served.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
5473
- return send(res, 409, { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId });
5474
- }
5475
- } catch (e) {
5476
- return send(res, 503, { error: "redemption_unavailable", detail: `could not durably claim the paid draw: ${e.message}`, _bookingId: bookingId });
5477
- }
5478
- let completion;
5479
- try {
5480
- completion = await upstream(safeRequest);
5481
- } catch (e) {
5482
- return send(res, 502, { error: "upstream_error", detail: e.message });
5483
- }
5484
- try {
5485
- enforceModelEcho(completion.model, config.model);
5486
- } catch (e) {
5487
- return send(res, 502, { error: "model_mismatch", detail: e.message });
5488
- }
5489
- const usage = completion.usage ?? {};
5490
- const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
5491
- const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
5492
- const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
5493
- const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
5494
- try {
5495
- served.complete(cacheKey, payload);
5496
- } catch (e) {
5497
- (config.log ?? console).error?.(`mtok-relay: completion for ${cacheKey} could not be persisted (${e.message}); retries will remain pending`);
5498
- }
5499
- return send(res, 200, payload);
5500
- });
5501
- };
5568
+ const handleDraw = (body, res) => withBookingLock(String(body?.bookingId ?? ""), async () => {
5569
+ const out = await core.serve(body);
5570
+ return send(res, out.status, out.body);
5571
+ });
5502
5572
  return { handleDraw };
5503
5573
  }
5504
5574
  async function fetchPlatformConfig(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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": {