mtok-relay 0.2.0 → 0.2.1

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 +256 -205
  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
  }
@@ -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.1";
2161
2161
 
2162
2162
  // node_modules/viem/_esm/errors/base.js
2163
2163
  var errorConfig = {
@@ -4778,45 +4778,9 @@ function startRelayServer({ config, handleDraw }) {
4778
4778
  return server;
4779
4779
  }
4780
4780
 
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
4781
  // src/redemption.mjs
4818
4782
  import fs from "node:fs";
4819
- import crypto2 from "node:crypto";
4783
+ import crypto3 from "node:crypto";
4820
4784
  var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
4821
4785
  function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS, now = () => Date.now(), log = console } = {}) {
4822
4786
  const map = /* @__PURE__ */ new Map();
@@ -4881,6 +4845,14 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4881
4845
  fs.rmSync(compact, { force: true });
4882
4846
  }
4883
4847
  fs.mkdirSync(claimsDir, { recursive: true });
4848
+ const markerCutoff = Date.now() - retentionMs;
4849
+ for (const name of fs.readdirSync(claimsDir)) {
4850
+ try {
4851
+ const marker = `${claimsDir}/${name}`;
4852
+ if (fs.statSync(marker).mtimeMs < markerCutoff) fs.unlinkSync(marker);
4853
+ } catch {
4854
+ }
4855
+ }
4884
4856
  durable = true;
4885
4857
  seenVersion = fileVersion();
4886
4858
  log.log?.(`mtok-relay: durable redemption at ${file} (${map.size} entries loaded)`);
@@ -4897,7 +4869,7 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4897
4869
  syncWrite(file, "a", JSON.stringify(recordFor(key, entry)) + "\n");
4898
4870
  seenVersion = fileVersion();
4899
4871
  };
4900
- const markerFor = (key) => `${claimsDir}/${crypto2.createHash("sha256").update(String(key)).digest("hex")}`;
4872
+ const markerFor = (key) => `${claimsDir}/${crypto3.createHash("sha256").update(String(key)).digest("hex")}`;
4901
4873
  const markClaimed = (key) => {
4902
4874
  if (!durable) throw new Error("durable redemption unavailable");
4903
4875
  const marker = markerFor(key);
@@ -5230,50 +5202,16 @@ function createOnchainVerifier({
5230
5202
  };
5231
5203
  }
5232
5204
 
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";
5205
+ // bridge/serve-core.mjs
5272
5206
  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
5207
  var REQUEST_NONCE_RE = /^0x[0-9a-fA-F]{32}$/;
5275
5208
  var CHAT_ROLES = /* @__PURE__ */ new Set(["developer", "system", "user", "assistant"]);
5276
5209
  var REQUEST_KEYS = /* @__PURE__ */ new Set(["model", "messages", "max_tokens", "temperature", "response_format", "stream", "n"]);
5210
+ async function hash32(v) {
5211
+ const bytes = new TextEncoder().encode(typeof v === "string" ? v : JSON.stringify(v ?? null));
5212
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
5213
+ return "0x" + [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
5214
+ }
5277
5215
  function legacyContentText(content) {
5278
5216
  const render = (part) => {
5279
5217
  if (typeof part === "string") return part;
@@ -5345,6 +5283,220 @@ function validateRequest(request, model, { legacy = false } = {}) {
5345
5283
  }
5346
5284
  };
5347
5285
  }
5286
+ function enforceModelEcho(upstreamModel, offerModel) {
5287
+ if (String(upstreamModel) !== String(offerModel))
5288
+ throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
5289
+ }
5290
+ function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
5291
+ const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
5292
+ if (!feeAddress || bps === 0n) return 0n;
5293
+ return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
5294
+ }
5295
+ var MESSAGE_OVERHEAD_TOKENS = 4;
5296
+ function estimateInputTokens(messages) {
5297
+ const utf8 = new TextEncoder();
5298
+ let tokens = 3;
5299
+ 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;
5303
+ }
5304
+ return tokens;
5305
+ }
5306
+ function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
5307
+ const estIn = estimateInputTokens(messages);
5308
+ const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
5309
+ if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
5310
+ const outBudgetUsd = budgetUsd - estInCostUsd;
5311
+ let maxTok = contextCeil;
5312
+ if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
5313
+ if (!Number.isFinite(Number(outPrice)) || Number(outPrice) <= 0) {
5314
+ return { refuse: true, reason: "output_price", estIn, estInCostUsd };
5315
+ }
5316
+ maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
5317
+ if (maxTok < 1) return { refuse: true, reason: "output", estIn, estInCostUsd };
5318
+ return { refuse: false, maxTok, estIn, estInCostUsd };
5319
+ }
5320
+ function createServeCore({
5321
+ model,
5322
+ inPrice,
5323
+ outPrice,
5324
+ verifier,
5325
+ redemption,
5326
+ upstream,
5327
+ log,
5328
+ offerId,
5329
+ sellerAgentId,
5330
+ sellerWallet,
5331
+ dripContractAddress,
5332
+ feeRecipient,
5333
+ feeBps,
5334
+ screenPayer
5335
+ }) {
5336
+ const serve = async (body) => {
5337
+ const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
5338
+ const hasRequestNonce = Object.hasOwn(body, "requestNonce");
5339
+ if (!bookingId) return { status: 400, body: { error: "bad_request", detail: "DRAW needs bookingId" } };
5340
+ if (n == null) return { status: 400, body: { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" } };
5341
+ if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295) {
5342
+ return { status: 400, body: { error: "bad_request", detail: "DRAW delivery index n must be a nonnegative uint32 integer" } };
5343
+ }
5344
+ if (hasRequestNonce && !REQUEST_NONCE_RE.test(requestNonce)) {
5345
+ return { status: 400, body: { error: "bad_request", detail: "DRAW needs requestNonce as 16 random bytes encoded as 0x-prefixed hex" } };
5346
+ }
5347
+ const checked = validateRequest(request, model, { legacy: !hasRequestNonce });
5348
+ if (checked.error) return { status: 400, body: { error: "bad_request", detail: checked.error } };
5349
+ const requestHashScheme = hasRequestNonce ? "nonce-v1" : "legacy-v0";
5350
+ const requestHash = hasRequestNonce ? await hash32({ request, requestNonce }) : await hash32(request);
5351
+ const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
5352
+ const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
5353
+ if (!dripContractAddress) {
5354
+ return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
5355
+ }
5356
+ if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
5357
+ let paid;
5358
+ try {
5359
+ paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
5360
+ contractAddress: dripContractAddress,
5361
+ buyerAgentId: buyerId,
5362
+ sellerAgentId,
5363
+ // when set, enforces the offer-owner match (#codex review)
5364
+ bookingId,
5365
+ offerId,
5366
+ model,
5367
+ n,
5368
+ requestHash,
5369
+ sellerWallet,
5370
+ feeRecipient,
5371
+ // #580: refuse a payment older than the redemption window. The JSONL
5372
+ // payload cache AND the claim markers are both aged out at boot (#600),
5373
+ // so past retention this age bound is the sole replay defense (its
5374
+ // skip-on-unreadable-block residual is named in redemption.mjs). An
5375
+ // honest retry is seconds-to-minutes old, never days.
5376
+ maxPaidAgeMs: redemption.retentionMs
5377
+ });
5378
+ } catch (e) {
5379
+ return { status: 402, body: { error: "payment_unverified", detail: e.message } };
5380
+ }
5381
+ if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
5382
+ const expectedFee = configuredFeeAtomic({
5383
+ sellerUsdAtomic: paid.event.sellerUsdAtomic,
5384
+ feeAddress: feeRecipient,
5385
+ feeBps
5386
+ });
5387
+ if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5388
+ return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
5389
+ }
5390
+ if (screenPayer) {
5391
+ try {
5392
+ if (await screenPayer(String(paid.from || "").toLowerCase())) {
5393
+ return { status: 403, body: { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" } };
5394
+ }
5395
+ } catch (e) {
5396
+ return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
5397
+ }
5398
+ }
5399
+ let storedKey = cacheKey;
5400
+ let redemptionState = await redemption.state(storedKey);
5401
+ if (!redemptionState && oldLegacyKey) {
5402
+ const oldLegacyState = await redemption.state(oldLegacyKey);
5403
+ if (oldLegacyState) {
5404
+ storedKey = oldLegacyKey;
5405
+ redemptionState = oldLegacyState;
5406
+ } else {
5407
+ redemptionState = await redemption.state(cacheKey);
5408
+ }
5409
+ }
5410
+ if (redemptionState === "complete") return { status: 200, body: await redemption.get(storedKey) };
5411
+ if (redemptionState === "pending") {
5412
+ return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5413
+ }
5414
+ const paidEvent = paid.event;
5415
+ const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
5416
+ if (remainingUsd < BALANCE_EPSILON) {
5417
+ return { status: 402, body: { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd } };
5418
+ }
5419
+ const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
5420
+ const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
5421
+ const boundInPrice = Math.max(Number(inPrice) || 0, eventInPriceUsd);
5422
+ 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 });
5424
+ if (bound.refuse) {
5425
+ const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
5426
+ 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 } };
5427
+ }
5428
+ const safeRequest = { ...checked.safeRequest, model, max_tokens: bound.maxTok };
5429
+ try {
5430
+ if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
5431
+ return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5432
+ }
5433
+ } catch (e) {
5434
+ return { status: 503, body: { error: "redemption_unavailable", detail: `could not durably claim the paid draw: ${e.message}`, _bookingId: bookingId } };
5435
+ }
5436
+ let completion;
5437
+ try {
5438
+ completion = await upstream(safeRequest);
5439
+ } catch (e) {
5440
+ return { status: 502, body: { error: "upstream_error", detail: e.message } };
5441
+ }
5442
+ try {
5443
+ enforceModelEcho(completion.model, model);
5444
+ } catch (e) {
5445
+ return { status: 502, body: { error: "model_mismatch", detail: e.message } };
5446
+ }
5447
+ const usage = completion.usage ?? {};
5448
+ const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
5449
+ const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
5450
+ const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
5451
+ const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
5452
+ try {
5453
+ await redemption.complete(cacheKey, payload);
5454
+ } catch (e) {
5455
+ (log ?? console).error?.(`mtok serve core: completion for ${cacheKey} could not be persisted (${e.message}); retries will remain pending`);
5456
+ }
5457
+ return { status: 200, body: payload };
5458
+ };
5459
+ return { serve };
5460
+ }
5461
+
5462
+ // bridge/bridge.mjs
5463
+ function httpUpstream({ baseUrl, key }) {
5464
+ const url = String(baseUrl || "").replace(/\/$/, "") + "/chat/completions";
5465
+ return async (payload) => {
5466
+ const res = await fetch(url, {
5467
+ method: "POST",
5468
+ headers: {
5469
+ "content-type": "application/json",
5470
+ ...key ? { authorization: `Bearer ${key}` } : {}
5471
+ },
5472
+ body: JSON.stringify(payload)
5473
+ });
5474
+ const text = await res.text();
5475
+ let json;
5476
+ try {
5477
+ json = JSON.parse(text);
5478
+ } catch {
5479
+ throw new Error(`non-JSON upstream response (${res.status})`);
5480
+ }
5481
+ if (!res.ok) throw new Error(json?.error?.message || `upstream ${res.status}`);
5482
+ return json;
5483
+ };
5484
+ }
5485
+
5486
+ // src/rpc.mjs
5487
+ var BASE_MAINNET_RPCS = [
5488
+ "https://mainnet.base.org",
5489
+ "https://base.llamarpc.com",
5490
+ "https://base-rpc.publicnode.com",
5491
+ "https://base.drpc.org"
5492
+ ];
5493
+ var BASE_SEPOLIA_RPCS = [
5494
+ "https://sepolia.base.org",
5495
+ "https://base-sepolia-rpc.publicnode.com"
5496
+ ];
5497
+ var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
5498
+
5499
+ // src/runtime.mjs
5348
5500
  async function createRelayRuntime(config) {
5349
5501
  const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
5350
5502
  const drawLocks = /* @__PURE__ */ new Map();
@@ -5360,6 +5512,22 @@ async function createRelayRuntime(config) {
5360
5512
  if (screenPayer && await screenPayer(payer)) return true;
5361
5513
  return false;
5362
5514
  };
5515
+ const core = createServeCore({
5516
+ model: config.model,
5517
+ inPrice: config.inPrice,
5518
+ outPrice: config.outPrice,
5519
+ verifier,
5520
+ redemption: served,
5521
+ upstream,
5522
+ log: config.log,
5523
+ offerId: config.offerId,
5524
+ sellerAgentId: config.sellerAgentId,
5525
+ sellerWallet: config.settlementAddr,
5526
+ dripContractAddress: platform.dripContractAddress,
5527
+ feeRecipient: platform.feeAddress,
5528
+ feeBps: platform.feeBps,
5529
+ screenPayer: payerDenied
5530
+ });
5363
5531
  const withBookingLock = async (bookingId, fn) => {
5364
5532
  const previous = drawLocks.get(bookingId) || Promise.resolve();
5365
5533
  let release;
@@ -5378,127 +5546,10 @@ async function createRelayRuntime(config) {
5378
5546
  if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
5379
5547
  }
5380
5548
  };
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
- };
5549
+ const handleDraw = (body, res) => withBookingLock(String(body?.bookingId ?? ""), async () => {
5550
+ const out = await core.serve(body);
5551
+ return send(res, out.status, out.body);
5552
+ });
5502
5553
  return { handleDraw };
5503
5554
  }
5504
5555
  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.1",
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": {