mtok-relay 0.1.2 → 0.1.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 +578 -275
  2. package/package.json +3 -3
@@ -1,32 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // GENERATED from mtok-relay.mjs — do not edit by hand.
3
3
 
4
- // mtok-relay.mjs
5
- import http from "node:http";
6
-
7
- // lib.mjs
8
- function enforceModelEcho(upstreamModel, offerModel) {
9
- if (String(upstreamModel) !== String(offerModel))
10
- throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
11
- }
12
- var buildFundReport = ({ offerId: offerId2, buyerId, bookingId, n, priceUsd, sellerTxHash, feeTxHash }) => ({
13
- offerId: offerId2,
14
- buyerId,
15
- ...bookingId ? { bookingId } : {},
16
- n,
17
- priceUsd,
18
- sellerTxHash,
19
- feeTxHash
20
- });
21
- var buildDrawReport = ({ offerId: offerId2, buyerId, bookingId, n, usage }) => ({
22
- offerId: offerId2,
23
- buyerId,
24
- bookingId,
25
- n,
26
- inputTokens: usage?.prompt_tokens ?? 0,
27
- outputTokens: usage?.completion_tokens ?? 0
28
- });
29
-
30
4
  // node_modules/@noble/hashes/esm/cryptoNode.js
31
5
  import * as nc from "node:crypto";
32
6
  var crypto = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
@@ -346,11 +320,11 @@ function validateObject(object, validators, optValidators = {}) {
346
320
  }
347
321
  function memoized(fn) {
348
322
  const map = /* @__PURE__ */ new WeakMap();
349
- return (arg, ...args2) => {
323
+ return (arg, ...args) => {
350
324
  const val = map.get(arg);
351
325
  if (val !== void 0)
352
326
  return val;
353
- const computed = fn(arg, ...args2);
327
+ const computed = fn(arg, ...args);
354
328
  map.set(arg, computed);
355
329
  return computed;
356
330
  };
@@ -2183,7 +2157,7 @@ var secp256k1 = createCurve({
2183
2157
  }, sha256);
2184
2158
 
2185
2159
  // node_modules/viem/_esm/errors/version.js
2186
- var version = "2.53.1";
2160
+ var version = "2.54.3";
2187
2161
 
2188
2162
  // node_modules/viem/_esm/errors/base.js
2189
2163
  var errorConfig = {
@@ -2191,29 +2165,29 @@ var errorConfig = {
2191
2165
  version: `viem@${version}`
2192
2166
  };
2193
2167
  var BaseError = class _BaseError extends Error {
2194
- constructor(shortMessage, args2 = {}) {
2168
+ constructor(shortMessage, args = {}) {
2195
2169
  const details = (() => {
2196
- if (args2.cause instanceof _BaseError)
2197
- return args2.cause.details;
2198
- if (args2.cause?.message)
2199
- return args2.cause.message;
2200
- return args2.details;
2170
+ if (args.cause instanceof _BaseError)
2171
+ return args.cause.details;
2172
+ if (args.cause?.message)
2173
+ return args.cause.message;
2174
+ return args.details;
2201
2175
  })();
2202
2176
  const docsPath = (() => {
2203
- if (args2.cause instanceof _BaseError)
2204
- return args2.cause.docsPath || args2.docsPath;
2205
- return args2.docsPath;
2177
+ if (args.cause instanceof _BaseError)
2178
+ return args.cause.docsPath || args.docsPath;
2179
+ return args.docsPath;
2206
2180
  })();
2207
- const docsUrl = errorConfig.getDocsUrl?.({ ...args2, docsPath });
2181
+ const docsUrl = errorConfig.getDocsUrl?.({ ...args, docsPath });
2208
2182
  const message = [
2209
2183
  shortMessage || "An error occurred.",
2210
2184
  "",
2211
- ...args2.metaMessages ? [...args2.metaMessages, ""] : [],
2185
+ ...args.metaMessages ? [...args.metaMessages, ""] : [],
2212
2186
  ...docsUrl ? [`Docs: ${docsUrl}`] : [],
2213
2187
  ...details ? [`Details: ${details}`] : [],
2214
2188
  ...errorConfig.version ? [`Version: ${errorConfig.version}`] : []
2215
2189
  ].join("\n");
2216
- super(message, args2.cause ? { cause: args2.cause } : void 0);
2190
+ super(message, args.cause ? { cause: args.cause } : void 0);
2217
2191
  Object.defineProperty(this, "details", {
2218
2192
  enumerable: true,
2219
2193
  configurable: true,
@@ -2252,8 +2226,8 @@ var BaseError = class _BaseError extends Error {
2252
2226
  });
2253
2227
  this.details = details;
2254
2228
  this.docsPath = docsPath;
2255
- this.metaMessages = args2.metaMessages;
2256
- this.name = args2.name ?? this.name;
2229
+ this.metaMessages = args.metaMessages;
2230
+ this.name = args.name ?? this.name;
2257
2231
  this.shortMessage = shortMessage;
2258
2232
  this.version = version;
2259
2233
  }
@@ -3268,8 +3242,8 @@ function formatGwei(wei, unit = "wei") {
3268
3242
  }
3269
3243
 
3270
3244
  // node_modules/viem/_esm/errors/transaction.js
3271
- function prettyPrint(args2) {
3272
- const entries = Object.entries(args2).map(([key, value]) => {
3245
+ function prettyPrint(args) {
3246
+ const entries = Object.entries(args).map(([key, value]) => {
3273
3247
  if (value === void 0 || value === false)
3274
3248
  return null;
3275
3249
  return [key, value];
@@ -4193,10 +4167,7 @@ function encodeAbiParameters(params, values) {
4193
4167
  params,
4194
4168
  values
4195
4169
  });
4196
- const data = encodeParams(preparedParams);
4197
- if (data.length === 0)
4198
- return "0x";
4199
- return data;
4170
+ return encodeParams(preparedParams);
4200
4171
  }
4201
4172
  function prepareParams({ params, values }) {
4202
4173
  const preparedParams = [];
@@ -4262,7 +4233,7 @@ function encodeParams(preparedParams) {
4262
4233
  staticParams.push(encoded);
4263
4234
  }
4264
4235
  }
4265
- return concat([...staticParams, ...dynamicParams]);
4236
+ return concatHex([...staticParams, ...dynamicParams]);
4266
4237
  }
4267
4238
  function encodeAddress(value) {
4268
4239
  if (!isAddress(value))
@@ -4279,7 +4250,7 @@ function encodeArray(value, { length, param }) {
4279
4250
  givenLength: value.length,
4280
4251
  type: `${param.type}[${length}]`
4281
4252
  });
4282
- let dynamicChild = false;
4253
+ let dynamicChild = value.length === 0 && isDynamicType(param);
4283
4254
  const preparedParams = [];
4284
4255
  for (let i = 0; i < value.length; i++) {
4285
4256
  const preparedParam = prepareParam({ param, value: value[i] });
@@ -4293,7 +4264,7 @@ function encodeArray(value, { length, param }) {
4293
4264
  const length2 = numberToHex(preparedParams.length, { size: 32 });
4294
4265
  return {
4295
4266
  dynamic: true,
4296
- encoded: preparedParams.length > 0 ? concat([length2, data]) : length2
4267
+ encoded: concatHex([length2, data])
4297
4268
  };
4298
4269
  }
4299
4270
  if (dynamicChild)
@@ -4301,7 +4272,7 @@ function encodeArray(value, { length, param }) {
4301
4272
  }
4302
4273
  return {
4303
4274
  dynamic: false,
4304
- encoded: concat(preparedParams.map(({ encoded }) => encoded))
4275
+ encoded: concatHex(preparedParams.map(({ encoded }) => encoded))
4305
4276
  };
4306
4277
  }
4307
4278
  function encodeBytes(value, { param }) {
@@ -4316,7 +4287,10 @@ function encodeBytes(value, { param }) {
4316
4287
  });
4317
4288
  return {
4318
4289
  dynamic: true,
4319
- encoded: concat([padHex(numberToHex(bytesSize, { size: 32 })), value_])
4290
+ encoded: concatHex([
4291
+ padHex(numberToHex(bytesSize, { size: 32 })),
4292
+ value_
4293
+ ])
4320
4294
  };
4321
4295
  }
4322
4296
  if (bytesSize !== Number.parseInt(paramSize, 10))
@@ -4363,7 +4337,7 @@ function encodeString(value) {
4363
4337
  }
4364
4338
  return {
4365
4339
  dynamic: true,
4366
- encoded: concat([
4340
+ encoded: concatHex([
4367
4341
  padHex(numberToHex(size(hexValue), { size: 32 })),
4368
4342
  ...parts
4369
4343
  ])
@@ -4385,7 +4359,7 @@ function encodeTuple(value, { param }) {
4385
4359
  }
4386
4360
  return {
4387
4361
  dynamic,
4388
- encoded: dynamic ? encodeParams(preparedParams) : concat(preparedParams.map(({ encoded }) => encoded))
4362
+ encoded: dynamic ? encodeParams(preparedParams) : concatHex(preparedParams.map(({ encoded }) => encoded))
4389
4363
  };
4390
4364
  }
4391
4365
  function getArrayComponents(type) {
@@ -4395,6 +4369,21 @@ function getArrayComponents(type) {
4395
4369
  [matches[2] ? Number(matches[2]) : null, matches[1]]
4396
4370
  ) : void 0;
4397
4371
  }
4372
+ function isDynamicType(param) {
4373
+ const { type } = param;
4374
+ if (type === "string")
4375
+ return true;
4376
+ if (type === "bytes")
4377
+ return true;
4378
+ if (type.endsWith("[]"))
4379
+ return true;
4380
+ if (type === "tuple")
4381
+ return param.components.some(isDynamicType);
4382
+ const arrayComponents = getArrayComponents(type);
4383
+ if (arrayComponents)
4384
+ return isDynamicType({ ...param, type: arrayComponents[1] });
4385
+ return false;
4386
+ }
4398
4387
 
4399
4388
  // node_modules/viem/_esm/utils/stringify.js
4400
4389
  var stringify = (value, replacer, space) => JSON.stringify(value, (key, value_) => {
@@ -4643,6 +4632,223 @@ function privateKeyToAccount(privateKey, options = {}) {
4643
4632
  };
4644
4633
  }
4645
4634
 
4635
+ // src/config.mjs
4636
+ var flag = (args, name) => {
4637
+ const i = args.indexOf(name);
4638
+ return i !== -1 ? args[i + 1] : null;
4639
+ };
4640
+ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {}) {
4641
+ const offerId = flag(argv, "--offer");
4642
+ const model = flag(argv, "--model");
4643
+ const upstream = (flag(argv, "--upstream") ?? "").replace(/\/$/, "");
4644
+ const apiBase = (flag(argv, "--api") ?? "https://mtok.market").replace(/\/$/, "");
4645
+ const port = Number(flag(argv, "--port") ?? 8788);
4646
+ const rpcFlag = flag(argv, "--rpc");
4647
+ const settlementPubkeyFlag = flag(argv, "--settlement-pubkey");
4648
+ const outPrice = Number(flag(argv, "--out-price") ?? 0);
4649
+ const inPrice = Number(flag(argv, "--in-price") ?? outPrice);
4650
+ const redemptionFile = flag(argv, "--redemption-file") ?? env.RELAY_REDEMPTION_FILE ?? null;
4651
+ const denylistRaw = flag(argv, "--payer-denylist") ?? env.RELAY_PAYER_DENYLIST ?? "";
4652
+ const payerDenylist = String(denylistRaw).split(",").map((a) => a.trim().toLowerCase()).filter(Boolean);
4653
+ if (!offerId) throw new Error("--offer <id> is required");
4654
+ if (!model) throw new Error("--model <id> is required (the offer model you serve)");
4655
+ if (!upstream) throw new Error("--upstream <url> is required");
4656
+ const mtokApiKey = env.MTOK_API_KEY;
4657
+ const upstreamKey = env.UPSTREAM_KEY;
4658
+ const relayWalletKey = env.RELAY_WALLET_KEY;
4659
+ if (!upstreamKey) throw new Error("UPSTREAM_KEY env var is required");
4660
+ if (!relayWalletKey && !settlementPubkeyFlag) {
4661
+ throw new Error("RELAY_WALLET_KEY env var (or --settlement-pubkey) is required");
4662
+ }
4663
+ let settlementAddr = settlementPubkeyFlag;
4664
+ if (!settlementAddr) {
4665
+ try {
4666
+ settlementAddr = privateKeyToAccount(relayWalletKey.startsWith("0x") ? relayWalletKey : "0x" + relayWalletKey).address;
4667
+ } catch (e) {
4668
+ throw new Error(`invalid RELAY_WALLET_KEY: ${e.message}`);
4669
+ }
4670
+ }
4671
+ return {
4672
+ offerId,
4673
+ model,
4674
+ upstream,
4675
+ apiBase,
4676
+ port,
4677
+ rpcFlag,
4678
+ outPrice,
4679
+ inPrice,
4680
+ redemptionFile,
4681
+ mtokApiKey,
4682
+ upstreamKey,
4683
+ settlementAddr,
4684
+ payerDenylist
4685
+ };
4686
+ }
4687
+
4688
+ // src/http.mjs
4689
+ import http from "node:http";
4690
+ var MAX_BODY_BYTES = 256e3;
4691
+ function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
4692
+ return new Promise((resolve, reject) => {
4693
+ const chunks = [];
4694
+ let bytes = 0;
4695
+ let settled = false;
4696
+ req.on("data", (d) => {
4697
+ bytes += d.length;
4698
+ if (bytes > maxBytes && !settled) {
4699
+ settled = true;
4700
+ reject(Object.assign(new Error("body_too_large"), { code: "body_too_large" }));
4701
+ req.resume();
4702
+ return;
4703
+ }
4704
+ if (settled) return;
4705
+ chunks.push(d);
4706
+ });
4707
+ req.on("end", () => {
4708
+ if (settled) return;
4709
+ settled = true;
4710
+ const raw = Buffer.concat(chunks).toString("utf8");
4711
+ try {
4712
+ resolve(JSON.parse(raw || "{}"));
4713
+ } catch {
4714
+ resolve({});
4715
+ }
4716
+ });
4717
+ req.on("error", (e) => {
4718
+ if (settled) return;
4719
+ settled = true;
4720
+ reject(e);
4721
+ });
4722
+ });
4723
+ }
4724
+ function send(res, status, body) {
4725
+ const payload = JSON.stringify(body);
4726
+ res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
4727
+ res.end(payload);
4728
+ }
4729
+ function startRelayServer({ config, handleDraw }) {
4730
+ const windows = /* @__PURE__ */ new Map();
4731
+ const windowMs = 6e4;
4732
+ const maxPerMinute = Number(config.maxRequestsPerMinute ?? 120);
4733
+ const checkRate = (req) => {
4734
+ if (!Number.isFinite(maxPerMinute)) return true;
4735
+ const key = req.socket.remoteAddress || "unknown";
4736
+ const now = Date.now();
4737
+ let w = windows.get(key);
4738
+ if (!w || now - w.start >= windowMs) {
4739
+ w = { start: now, count: 0 };
4740
+ windows.set(key, w);
4741
+ if (windows.size > 1e4) {
4742
+ for (const [k, v] of windows) if (now - v.start >= windowMs) windows.delete(k);
4743
+ }
4744
+ }
4745
+ w.count += 1;
4746
+ return w.count <= maxPerMinute;
4747
+ };
4748
+ const server = http.createServer(async (req, res) => {
4749
+ if (req.method !== "POST" || req.url !== "/chunk") {
4750
+ return send(res, 404, { error: "not found" });
4751
+ }
4752
+ if (!checkRate(req)) return send(res, 429, { error: "rate_limited" });
4753
+ let body;
4754
+ try {
4755
+ body = await readBody(req);
4756
+ } catch (e) {
4757
+ if (e?.code === "body_too_large") return send(res, 413, { error: "body_too_large" });
4758
+ return send(res, 400, { error: "bad body" });
4759
+ }
4760
+ if (body.request == null) {
4761
+ return send(res, 400, { error: "bad_request", detail: "need a DRAW (request); the legacy FUND lane is retired, pay per draw on-chain" });
4762
+ }
4763
+ return handleDraw(body, res);
4764
+ });
4765
+ server.listen(config.port, () => {
4766
+ console.log(`mtok-relay: listening on port ${config.port} offer=${config.offerId} model=${config.model} upstream=${config.upstream} api=${config.apiBase} settlement=${config.settlementAddr}`);
4767
+ });
4768
+ return server;
4769
+ }
4770
+
4771
+ // lib.mjs
4772
+ function enforceModelEcho(upstreamModel, offerModel) {
4773
+ if (String(upstreamModel) !== String(offerModel))
4774
+ throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
4775
+ }
4776
+ function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
4777
+ const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
4778
+ if (!feeAddress || bps === 0n) return 0n;
4779
+ return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
4780
+ }
4781
+ var CHARS_PER_TOKEN_EST = 3.2;
4782
+ function estimateInputTokens(messages) {
4783
+ let chars = 0;
4784
+ for (const m of messages ?? []) {
4785
+ const c = m?.content;
4786
+ if (typeof c === "string") chars += c.length;
4787
+ else if (Array.isArray(c)) for (const part of c) chars += String(part?.text ?? "").length;
4788
+ }
4789
+ return Math.ceil(chars / CHARS_PER_TOKEN_EST);
4790
+ }
4791
+ function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
4792
+ const estIn = estimateInputTokens(messages);
4793
+ const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
4794
+ if (estInCostUsd >= budgetUsd) return { refuse: true, estIn, estInCostUsd };
4795
+ const outBudgetUsd = budgetUsd - estInCostUsd;
4796
+ let maxTok = contextCeil;
4797
+ if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
4798
+ if (Number(outPrice) > 0) maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
4799
+ return { refuse: false, maxTok: Math.max(1, maxTok), estIn, estInCostUsd };
4800
+ }
4801
+
4802
+ // src/redemption.mjs
4803
+ import fs from "node:fs";
4804
+ var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
4805
+ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS, now = () => Date.now(), log = console } = {}) {
4806
+ const map = /* @__PURE__ */ new Map();
4807
+ let durable = false;
4808
+ if (file) {
4809
+ try {
4810
+ if (fs.existsSync(file)) {
4811
+ const cutoff = now() - retentionMs;
4812
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
4813
+ if (!line.trim()) continue;
4814
+ try {
4815
+ const { k, at, payload } = JSON.parse(line);
4816
+ if (k && Number(at) >= cutoff) map.set(k, { payload, at: Number(at) });
4817
+ } catch {
4818
+ }
4819
+ }
4820
+ }
4821
+ fs.writeFileSync(file, [...map].map(([k, e]) => JSON.stringify({ k, at: e.at, payload: e.payload })).join("\n") + (map.size ? "\n" : ""));
4822
+ durable = true;
4823
+ log.log?.(`mtok-relay: durable redemption at ${file} (${map.size} entries loaded)`);
4824
+ } catch (e) {
4825
+ log.warn?.(`mtok-relay: redemption file ${file} not writable (${e.message}); redemption is IN-MEMORY ONLY, a restart can re-serve a paid draw (#495). Point --redemption-file at a durable path.`);
4826
+ durable = false;
4827
+ }
4828
+ } else {
4829
+ log.warn?.("mtok-relay: no --redemption-file, redemption is IN-MEMORY ONLY, a restart or long idle can re-serve one payment for a fresh inference (#495). Set --redemption-file to a durable path.");
4830
+ }
4831
+ return {
4832
+ durable,
4833
+ has(key) {
4834
+ return map.has(key);
4835
+ },
4836
+ get(key) {
4837
+ return map.get(key)?.payload;
4838
+ },
4839
+ set(key, payload) {
4840
+ const at = now();
4841
+ map.set(key, { payload, at });
4842
+ if (durable) {
4843
+ try {
4844
+ fs.appendFileSync(file, JSON.stringify({ k: key, at, payload }) + "\n");
4845
+ } catch {
4846
+ }
4847
+ }
4848
+ }
4849
+ };
4850
+ }
4851
+
4646
4852
  // core/errors.js
4647
4853
  function apiError(status, code, message, details) {
4648
4854
  const err = new Error(message);
@@ -4654,12 +4860,66 @@ function apiError(status, code, message, details) {
4654
4860
 
4655
4861
  // core/onchain.js
4656
4862
  var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
4863
+ var DRAW_PAID_TOPIC = "0xb0243f80521d0dccd159389597aba96047e60ba5d7a9df12b67e5cb75230ac41";
4864
+ var DRAW_PAID_TOPIC_V2 = "0x94f9e7578a5c019f78ad332dcb2dc5563bcf257d777009ff5cef4118f084a70b";
4865
+ var DRAW_PAID_TOPICS = [DRAW_PAID_TOPIC, DRAW_PAID_TOPIC_V2];
4866
+ var isDrawPaidTopic = (topic) => DRAW_PAID_TOPICS.includes(String(topic || "").toLowerCase());
4657
4867
  var topicToAddress = (topic) => "0x" + String(topic).slice(-40).toLowerCase();
4658
4868
  var lc = (a) => String(a || "").toLowerCase();
4869
+ var strip0x = (v) => String(v || "").replace(/^0x/i, "");
4870
+ var wordAt = (hex, i) => strip0x(hex).slice(i * 64, i * 64 + 64);
4871
+ var uintWord = (hex, i) => BigInt("0x" + (wordAt(hex, i) || "0"));
4872
+ var asciiFromHex = (hex) => {
4873
+ const bytes = strip0x(hex);
4874
+ const out = [];
4875
+ for (let i = 0; i < bytes.length; i += 2) {
4876
+ const b = parseInt(bytes.slice(i, i + 2), 16);
4877
+ if (!Number.isFinite(b)) break;
4878
+ out.push(b);
4879
+ }
4880
+ return new TextDecoder().decode(new Uint8Array(out));
4881
+ };
4882
+ function decodeAbiString(dataHex, offsetBytes) {
4883
+ const body = strip0x(dataHex);
4884
+ const start = Number(offsetBytes) * 2;
4885
+ if (!Number.isFinite(start) || start < 0 || start + 64 > body.length) throw new Error("bad_string_offset");
4886
+ const len = Number(BigInt("0x" + body.slice(start, start + 64)));
4887
+ const textStart = start + 64;
4888
+ const textEnd = textStart + len * 2;
4889
+ if (!Number.isFinite(len) || len < 0 || textEnd > body.length) throw new Error("bad_string_length");
4890
+ return asciiFromHex(body.slice(textStart, textEnd));
4891
+ }
4892
+ function decodeDrawPaidLog(log) {
4893
+ const data = log?.data || "0x";
4894
+ const isV2 = String(log?.topics?.[0] || "").toLowerCase() === DRAW_PAID_TOPIC_V2;
4895
+ if (strip0x(data).length < (isV2 ? 13 : 11) * 64) throw new Error("short_draw_paid_data");
4896
+ const out = {
4897
+ drawId: log?.topics?.[1],
4898
+ sellerAgentKey: log?.topics?.[2],
4899
+ buyerAgentKey: log?.topics?.[3],
4900
+ sellerAgentId: decodeAbiString(data, uintWord(data, 0)),
4901
+ buyerAgentId: decodeAbiString(data, uintWord(data, 1)),
4902
+ bookingId: decodeAbiString(data, uintWord(data, 2)),
4903
+ offerId: decodeAbiString(data, uintWord(data, 3)),
4904
+ model: decodeAbiString(data, uintWord(data, 4)),
4905
+ n: Number(uintWord(data, 5)),
4906
+ sellerUsdAtomic: uintWord(data, 6).toString(),
4907
+ feeUsdAtomic: uintWord(data, 7).toString(),
4908
+ inputPricePerMTokAtomic: uintWord(data, 8).toString(),
4909
+ outputPricePerMTokAtomic: uintWord(data, 9).toString(),
4910
+ requestHash: "0x" + wordAt(data, 10)
4911
+ };
4912
+ if (isV2) {
4913
+ out.seller = topicToAddress(wordAt(data, 11));
4914
+ out.buyer = topicToAddress(wordAt(data, 12));
4915
+ }
4916
+ return out;
4917
+ }
4659
4918
  function createOnchainVerifier({
4660
4919
  rpcUrl,
4661
4920
  rpcUrls,
4662
4921
  usdcAddress,
4922
+ expectedChainId,
4663
4923
  fetchImpl = globalThis.fetch,
4664
4924
  // A just-submitted payment can be mined on the payer's RPC yet not YET indexed by
4665
4925
  // ours (cross-RPC propagation lag), so a single receipt lookup returns null and the
@@ -4672,251 +4932,294 @@ function createOnchainVerifier({
4672
4932
  const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || "").split(",")).map((s) => String(s).trim()).filter(Boolean);
4673
4933
  const configured = Boolean(urls.length && usdcAddress);
4674
4934
  const usdc = lc(usdcAddress);
4935
+ async function rpcOn(url, method, params) {
4936
+ const res = await fetchImpl(url, {
4937
+ method: "POST",
4938
+ headers: { "content-type": "application/json" },
4939
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
4940
+ });
4941
+ if (!res.ok) throw apiError(502, "rpc_error", `chain RPC returned ${res.status}`);
4942
+ const body = await res.json();
4943
+ if (body.error) throw apiError(502, "rpc_error", `chain RPC: ${body.error.message ?? "unknown"}`);
4944
+ return body.result;
4945
+ }
4675
4946
  async function rpc(method, params) {
4676
4947
  let lastErr;
4677
- for (const url of urls) {
4948
+ const pool = chainPinned && chainOkUrls.size ? urls.filter((u) => chainOkUrls.has(u)) : urls;
4949
+ for (const url of pool) {
4678
4950
  try {
4679
- const res = await fetchImpl(url, {
4680
- method: "POST",
4681
- headers: { "content-type": "application/json" },
4682
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
4683
- });
4684
- if (!res.ok) {
4685
- lastErr = apiError(502, "rpc_error", `chain RPC returned ${res.status}`);
4686
- continue;
4687
- }
4688
- const body = await res.json();
4689
- if (body.error) {
4690
- lastErr = apiError(502, "rpc_error", `chain RPC: ${body.error.message ?? "unknown"}`);
4691
- continue;
4692
- }
4693
- return body.result;
4951
+ return await rpcOn(url, method, params);
4694
4952
  } catch (e) {
4695
4953
  lastErr = e;
4696
4954
  }
4697
4955
  }
4698
4956
  throw lastErr ?? apiError(502, "rpc_error", "no chain RPC configured");
4699
4957
  }
4958
+ const chainOkUrls = /* @__PURE__ */ new Set();
4959
+ const expChain = Number(expectedChainId);
4960
+ const chainPinned = Number.isFinite(expChain) && expChain > 0;
4961
+ async function assertChain() {
4962
+ if (!chainPinned) return true;
4963
+ if (chainOkUrls.size) return true;
4964
+ for (const url of urls) {
4965
+ try {
4966
+ const hex = await rpcOn(url, "eth_chainId", []);
4967
+ if (typeof hex === "string" && parseInt(hex, 16) === expChain) chainOkUrls.add(url);
4968
+ } catch {
4969
+ }
4970
+ }
4971
+ return chainOkUrls.size > 0;
4972
+ }
4973
+ async function fetchReceipt(txHash) {
4974
+ let receipt = await rpc("eth_getTransactionReceipt", [txHash]);
4975
+ for (let i = 0; !receipt && i < receiptRetries; i++) {
4976
+ await sleepImpl(receiptRetryMs);
4977
+ receipt = await rpc("eth_getTransactionReceipt", [txHash]);
4978
+ }
4979
+ if (!receipt) return { error: "tx_not_found_or_pending" };
4980
+ if (lc(receipt.transactionHash) !== lc(txHash)) return { error: "receipt_tx_mismatch" };
4981
+ if (receipt.status !== "0x1") return { error: "tx_failed" };
4982
+ return { receipt };
4983
+ }
4984
+ function findUsdcTransfer(receipt, { to, minAtomic, from, consumed } = {}) {
4985
+ const want = lc(to);
4986
+ const wantFrom = from ? lc(from) : null;
4987
+ const logs = receipt.logs || [];
4988
+ let sawRecipient = false;
4989
+ for (let i = 0; i < logs.length; i++) {
4990
+ if (consumed && consumed.has(i)) continue;
4991
+ const l = logs[i];
4992
+ if (lc(l.address) !== usdc || l.topics?.[0] !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
4993
+ if (topicToAddress(l.topics[2]) !== want) continue;
4994
+ if (wantFrom && topicToAddress(l.topics[1]) !== wantFrom) continue;
4995
+ sawRecipient = true;
4996
+ let amount;
4997
+ try {
4998
+ amount = BigInt(l.data);
4999
+ } catch {
5000
+ continue;
5001
+ }
5002
+ if (amount < BigInt(minAtomic)) continue;
5003
+ return { ok: true, from: topicToAddress(l.topics[1]), amount: amount.toString(), index: i };
5004
+ }
5005
+ return { ok: false, reason: sawRecipient ? "amount_too_low" : "no_matching_usdc_transfer" };
5006
+ }
4700
5007
  return {
4701
5008
  configured,
4702
5009
  // Confirm txHash is a successful USDC transfer of >= minAtomic to `to`.
4703
5010
  // Returns { ok, reason?, from?, amount? }; never throws on a bad payment
4704
5011
  // (only on an RPC transport failure).
4705
5012
  async verifyTransfer(txHash, { to, minAtomic }) {
4706
- let receipt = await rpc("eth_getTransactionReceipt", [txHash]);
4707
- for (let i = 0; !receipt && i < receiptRetries; i++) {
4708
- await sleepImpl(receiptRetryMs);
4709
- receipt = await rpc("eth_getTransactionReceipt", [txHash]);
4710
- }
4711
- if (!receipt) return { ok: false, reason: "tx_not_found_or_pending" };
4712
- if (receipt.status !== "0x1") return { ok: false, reason: "tx_failed" };
4713
- const want = lc(to);
4714
- const log = (receipt.logs || []).find(
4715
- (l) => lc(l.address) === usdc && l.topics?.[0] === TRANSFER_TOPIC && topicToAddress(l.topics[2]) === want
5013
+ if (!await assertChain()) return { ok: false, reason: "wrong_chain" };
5014
+ const got = await fetchReceipt(txHash);
5015
+ if (got.error) return { ok: false, reason: got.error };
5016
+ return findUsdcTransfer(got.receipt, { to, minAtomic });
5017
+ },
5018
+ async verifyDrawPaid(txHash, {
5019
+ contractAddress,
5020
+ buyerAgentId,
5021
+ sellerAgentId,
5022
+ bookingId,
5023
+ offerId,
5024
+ model,
5025
+ n,
5026
+ requestHash,
5027
+ sellerWallet,
5028
+ feeRecipient,
5029
+ minSellerAtomic = 0n
5030
+ } = {}) {
5031
+ if (!await assertChain()) return { ok: false, reason: "wrong_chain" };
5032
+ const contract = lc(contractAddress);
5033
+ if (!contract) return { ok: false, reason: "contract_not_configured" };
5034
+ const got = await fetchReceipt(txHash);
5035
+ if (got.error) return { ok: false, reason: got.error };
5036
+ const log = (got.receipt.logs || []).find(
5037
+ (l) => lc(l.address) === contract && isDrawPaidTopic(l.topics?.[0])
4716
5038
  );
4717
- if (!log) return { ok: false, reason: "no_matching_usdc_transfer" };
4718
- const amount = BigInt(log.data);
4719
- if (amount < BigInt(minAtomic)) return { ok: false, reason: "amount_too_low" };
4720
- return { ok: true, from: topicToAddress(log.topics[1]), amount: amount.toString() };
5039
+ if (!log) return { ok: false, reason: "no_draw_paid_event" };
5040
+ let event;
5041
+ try {
5042
+ event = decodeDrawPaidLog(log);
5043
+ } catch {
5044
+ return { ok: false, reason: "malformed_draw_paid_event" };
5045
+ }
5046
+ if (buyerAgentId != null && event.buyerAgentId !== String(buyerAgentId)) return { ok: false, reason: "buyer_agent_mismatch" };
5047
+ if (sellerAgentId != null && event.sellerAgentId !== String(sellerAgentId)) return { ok: false, reason: "seller_agent_mismatch" };
5048
+ if (bookingId != null && event.bookingId !== String(bookingId)) return { ok: false, reason: "booking_mismatch" };
5049
+ if (offerId != null && event.offerId !== String(offerId)) return { ok: false, reason: "offer_mismatch" };
5050
+ if (model != null && event.model !== String(model)) return { ok: false, reason: "model_mismatch" };
5051
+ if (n != null && event.n !== Number(n)) return { ok: false, reason: "draw_n_mismatch" };
5052
+ if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: "request_hash_mismatch" };
5053
+ if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
5054
+ const consumed = /* @__PURE__ */ new Set();
5055
+ let sellerTransfer = null;
5056
+ if (sellerWallet) {
5057
+ sellerTransfer = findUsdcTransfer(got.receipt, { to: sellerWallet, minAtomic: BigInt(event.sellerUsdAtomic), from: event.buyer });
5058
+ if (!sellerTransfer.ok) return { ok: false, reason: "seller_transfer_" + sellerTransfer.reason };
5059
+ if (sellerTransfer.index != null) consumed.add(sellerTransfer.index);
5060
+ }
5061
+ let feeTransfer = null;
5062
+ if (feeRecipient && BigInt(event.feeUsdAtomic) > 0n) {
5063
+ feeTransfer = findUsdcTransfer(got.receipt, { to: feeRecipient, minAtomic: BigInt(event.feeUsdAtomic), from: event.buyer, consumed });
5064
+ if (!feeTransfer.ok) return { ok: false, reason: "fee_transfer_" + feeTransfer.reason };
5065
+ }
5066
+ return { ok: true, event, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
4721
5067
  }
4722
5068
  };
4723
5069
  }
4724
- function usdToAtomic(usd) {
4725
- return BigInt(Math.round(Number(usd) * 1e6));
4726
- }
4727
5070
 
4728
- // mtok-relay.mjs
4729
- var CONTEXT_CEIL = 4096;
5071
+ // src/rpc.mjs
5072
+ var BASE_MAINNET_RPCS = [
5073
+ "https://mainnet.base.org",
5074
+ "https://base.llamarpc.com",
5075
+ "https://base-rpc.publicnode.com",
5076
+ "https://base.drpc.org"
5077
+ ];
5078
+ var BASE_SEPOLIA_RPCS = [
5079
+ "https://sepolia.base.org",
5080
+ "https://base-sepolia-rpc.publicnode.com"
5081
+ ];
5082
+ var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
5083
+
5084
+ // src/runtime.mjs
5085
+ import crypto2 from "node:crypto";
4730
5086
  var BALANCE_EPSILON = 1e-6;
4731
- var args = process.argv.slice(2);
4732
- var flag = (name) => {
4733
- const i = args.indexOf(name);
4734
- return i !== -1 ? args[i + 1] : null;
4735
- };
4736
- var offerId = flag("--offer");
4737
- var MODEL = flag("--model");
4738
- var upstream = (flag("--upstream") ?? "").replace(/\/$/, "");
4739
- var apiBase = (flag("--api") ?? "https://mtok.market").replace(/\/$/, "");
4740
- var port = Number(flag("--port") ?? 8788);
4741
- var rpcFlag = flag("--rpc");
4742
- var settlementPubkeyFlag = flag("--settlement-pubkey");
4743
- var outPrice = Number(flag("--out-price") ?? 0);
4744
- if (!offerId) {
4745
- console.error("mtok-relay: --offer <id> is required");
4746
- process.exit(1);
4747
- }
4748
- if (!MODEL) {
4749
- console.error("mtok-relay: --model <id> is required (the offer model you serve)");
4750
- process.exit(1);
4751
- }
4752
- if (!upstream) {
4753
- console.error("mtok-relay: --upstream <url> is required");
4754
- process.exit(1);
4755
- }
4756
- var MTOK_API_KEY = process.env.MTOK_API_KEY;
4757
- var UPSTREAM_KEY = process.env.UPSTREAM_KEY;
4758
- var RELAY_WALLET_KEY = process.env.RELAY_WALLET_KEY;
4759
- if (!MTOK_API_KEY) {
4760
- console.error("mtok-relay: MTOK_API_KEY env var is required");
4761
- process.exit(1);
4762
- }
4763
- if (!UPSTREAM_KEY) {
4764
- console.error("mtok-relay: UPSTREAM_KEY env var is required");
4765
- process.exit(1);
4766
- }
4767
- if (!RELAY_WALLET_KEY && !settlementPubkeyFlag) {
4768
- console.error("mtok-relay: RELAY_WALLET_KEY env var (or --settlement-pubkey) is required");
4769
- process.exit(1);
4770
- }
4771
- var SETTLEMENT_ADDR;
4772
- if (settlementPubkeyFlag) {
4773
- SETTLEMENT_ADDR = settlementPubkeyFlag;
4774
- } else {
4775
- try {
4776
- SETTLEMENT_ADDR = privateKeyToAccount(RELAY_WALLET_KEY.startsWith("0x") ? RELAY_WALLET_KEY : "0x" + RELAY_WALLET_KEY).address;
4777
- } catch (e) {
4778
- console.error("mtok-relay: invalid RELAY_WALLET_KEY \u2014", e.message);
4779
- process.exit(1);
4780
- }
4781
- }
4782
- function readBody(req) {
4783
- return new Promise((resolve, reject) => {
4784
- let raw = "";
4785
- req.on("data", (d) => {
4786
- raw += d;
5087
+ var hash32 = (v) => "0x" + crypto2.createHash("sha256").update(typeof v === "string" ? v : JSON.stringify(v ?? null)).digest("hex");
5088
+ async function createRelayRuntime(config) {
5089
+ const served = createRedemptionStore({ file: config.redemptionFile });
5090
+ const drawLocks = /* @__PURE__ */ new Map();
5091
+ const platform = await fetchPlatformConfig(config);
5092
+ const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress });
5093
+ if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
5094
+ const payerDenylist = new Set((config.payerDenylist ?? []).map((a) => String(a).trim().toLowerCase()).filter(Boolean));
5095
+ const screenPayer = typeof config.screenPayer === "function" ? config.screenPayer : null;
5096
+ const payerDenied = async (payer) => {
5097
+ if (!payer) return false;
5098
+ if (payerDenylist.has(payer)) return true;
5099
+ if (screenPayer && await screenPayer(payer)) return true;
5100
+ return false;
5101
+ };
5102
+ const withBookingLock = async (bookingId, fn) => {
5103
+ const previous = drawLocks.get(bookingId) || Promise.resolve();
5104
+ let release;
5105
+ const gate = new Promise((resolve) => {
5106
+ release = resolve;
4787
5107
  });
4788
- req.on("end", () => {
5108
+ const tail = previous.catch(() => {
5109
+ }).then(() => gate);
5110
+ drawLocks.set(bookingId, tail);
5111
+ await previous.catch(() => {
5112
+ });
5113
+ try {
5114
+ return await fn();
5115
+ } finally {
5116
+ release();
5117
+ if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
5118
+ }
5119
+ };
5120
+ const handleDraw = async (body, res) => {
5121
+ const { bookingId, n, buyerId, request, drawPaidTxHash } = body;
5122
+ if (!bookingId) return send(res, 400, { error: "bad_request", detail: "DRAW needs bookingId" });
5123
+ if (n == null) return send(res, 400, { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" });
5124
+ return withBookingLock(bookingId, async () => {
5125
+ const requestHash = hash32(request);
5126
+ const cacheKey = `${bookingId}:${n}:${requestHash}`;
5127
+ if (served.has(cacheKey)) return send(res, 200, served.get(cacheKey));
5128
+ if (!platform.dripContractAddress) {
5129
+ return send(res, 402, { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" });
5130
+ }
5131
+ if (!drawPaidTxHash) return send(res, 402, { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" });
5132
+ let paid;
4789
5133
  try {
4790
- resolve(JSON.parse(raw || "{}"));
4791
- } catch {
4792
- resolve({});
5134
+ paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
5135
+ contractAddress: platform.dripContractAddress,
5136
+ buyerAgentId: buyerId,
5137
+ bookingId,
5138
+ offerId: config.offerId,
5139
+ model: config.model,
5140
+ n,
5141
+ requestHash,
5142
+ sellerWallet: config.settlementAddr,
5143
+ feeRecipient: platform.feeAddress
5144
+ });
5145
+ } catch (e) {
5146
+ return send(res, 402, { error: "payment_unverified", detail: e.message });
5147
+ }
5148
+ if (!paid?.ok) return send(res, 402, { error: "payment_unverified", detail: paid?.reason || "unknown" });
5149
+ const expectedFee = configuredFeeAtomic({
5150
+ sellerUsdAtomic: paid.event.sellerUsdAtomic,
5151
+ feeAddress: platform.feeAddress,
5152
+ feeBps: platform.feeBps
5153
+ });
5154
+ if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5155
+ return send(res, 402, { error: "payment_unverified", detail: "fee_amount_too_low" });
5156
+ }
5157
+ try {
5158
+ if (await payerDenied(String(paid.from || "").toLowerCase())) {
5159
+ return send(res, 403, { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" });
5160
+ }
5161
+ } catch (e) {
5162
+ return send(res, 403, { error: "payer_denied", detail: "payer screening failed: " + e.message });
5163
+ }
5164
+ const paidEvent = paid.event;
5165
+ const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
5166
+ if (remainingUsd <= BALANCE_EPSILON) {
5167
+ return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
5168
+ }
5169
+ const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
5170
+ const inPrice = Math.max(Number(config.inPrice) || 0, eventInPriceUsd);
5171
+ const bound = boundServe({ messages: request?.messages, budgetUsd: remainingUsd, inPrice, outPrice: config.outPrice, reqMax: Number(request?.max_tokens) });
5172
+ if (bound.refuse) {
5173
+ return send(res, 402, { error: "input_too_large", detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) meets or exceeds the paid amount ($${remainingUsd}); send a shorter prompt or pay for more`, _bookingId: bookingId, remainingUsd });
5174
+ }
5175
+ const safeRequest = { ...request, max_tokens: bound.maxTok };
5176
+ let completion;
5177
+ try {
5178
+ const upstreamRes = await fetch(config.upstream + "/v1/chat/completions", {
5179
+ method: "POST",
5180
+ headers: { "content-type": "application/json", authorization: "Bearer " + config.upstreamKey },
5181
+ body: JSON.stringify(safeRequest)
5182
+ });
5183
+ completion = await upstreamRes.json();
5184
+ } catch (e) {
5185
+ return send(res, 502, { error: "upstream_error", detail: e.message });
5186
+ }
5187
+ try {
5188
+ enforceModelEcho(completion.model, config.model);
5189
+ } catch (e) {
5190
+ return send(res, 502, { error: "model_mismatch", detail: e.message });
4793
5191
  }
5192
+ const usage = completion.usage ?? {};
5193
+ const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
5194
+ const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
5195
+ const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
5196
+ const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
5197
+ served.set(cacheKey, payload);
5198
+ return send(res, 200, payload);
4794
5199
  });
4795
- req.on("error", reject);
4796
- });
4797
- }
4798
- function send(res, status, body) {
4799
- const payload = JSON.stringify(body);
4800
- res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
4801
- res.end(payload);
5200
+ };
5201
+ return { handleDraw };
4802
5202
  }
4803
- var feeAddress;
4804
- var feeBps;
4805
- var verifier;
4806
- async function boot() {
4807
- const r = await fetch(apiBase + "/api/config");
5203
+ async function fetchPlatformConfig(config) {
5204
+ const r = await fetch(config.apiBase + "/api/config");
4808
5205
  if (!r.ok) throw new Error("config fetch failed: " + r.status);
4809
- const config = await r.json();
4810
- feeAddress = config.feeAddress;
4811
- feeBps = config.feeBps;
4812
- const chainId = Number(config.chainId ?? 8453);
4813
- const rpcUrls = rpcFlag ? [rpcFlag] : chainId === 8453 ? ["https://mainnet.base.org", "https://base.llamarpc.com", "https://base-rpc.publicnode.com", "https://base.drpc.org"] : ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
4814
- verifier = createOnchainVerifier({ rpcUrls, usdcAddress: config.usdcAddress });
4815
- if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
5206
+ const body = await r.json();
5207
+ return {
5208
+ feeAddress: body.feeAddress,
5209
+ feeBps: body.feeBps,
5210
+ dustThresholdUsd: Number(body.dustThresholdUsd) || 1e-3,
5211
+ chainId: Number(body.chainId ?? 8453),
5212
+ usdcAddress: body.usdcAddress,
5213
+ dripContractAddress: body.dripContractAddress
5214
+ };
4816
5215
  }
4817
- async function reportToPlatform(reportBody) {
4818
- const rr = await fetch(apiBase + "/api/chunks/report", {
4819
- method: "POST",
4820
- headers: { "content-type": "application/json", "x-api-key": MTOK_API_KEY },
4821
- body: JSON.stringify(reportBody)
4822
- });
4823
- const body = await rr.json().catch(() => ({}));
4824
- return { ok: rr.status === 201, status: rr.status, body, booking: body?.booking ?? null };
4825
- }
4826
- async function handleFund(body, res) {
4827
- const { bookingId, n, sellerTxHash, feeTxHash, priceUsd, buyerId } = body;
4828
- try {
4829
- const sellerOk = (await verifier.verifyTransfer(sellerTxHash, { to: SETTLEMENT_ADDR, minAtomic: usdToAtomic(priceUsd) })).ok;
4830
- const feeAtomic = usdToAtomic((Number(priceUsd) || 0) * (Number(feeBps) || 0) / 1e4);
4831
- const feeOk = (await verifier.verifyTransfer(feeTxHash, { to: feeAddress, minAtomic: feeAtomic })).ok;
4832
- if (!sellerOk || !feeOk) return send(res, 402, { error: "payment_unverified", detail: `seller=${sellerOk} fee=${feeOk}` });
4833
- } catch (e) {
4834
- return send(res, 402, { error: "payment_unverified", detail: e.message });
4835
- }
4836
- let rep;
4837
- try {
4838
- rep = await reportToPlatform(buildFundReport({ offerId, buyerId, bookingId, n, priceUsd, sellerTxHash, feeTxHash }));
4839
- } catch (e) {
4840
- return send(res, 502, { error: "report_failed", detail: e.message });
4841
- }
4842
- if (!rep.ok || !rep.booking) {
4843
- console.error("mtok-relay: FUND report rejected (%d) for n=%d offerId=%s: %s", rep.status, n, offerId, JSON.stringify(rep.body));
4844
- return send(res, 502, { error: "report_failed", detail: rep.body?.error || rep.status });
4845
- }
4846
- return send(res, 200, { ...rep.booking, _bookingId: rep.booking.id, remainingUsd: rep.booking.remainingUsd });
4847
- }
4848
- async function handleDraw(body, res) {
4849
- const { bookingId, n, buyerId, request } = body;
4850
- if (!bookingId) return send(res, 400, { error: "bad_request", detail: "DRAW needs bookingId" });
4851
- let booking;
4852
- try {
4853
- const r = await fetch(apiBase + `/api/bookings/${encodeURIComponent(bookingId)}`, { headers: { "x-api-key": MTOK_API_KEY } });
4854
- const rb = await r.json().catch(() => ({}));
4855
- if (r.status !== 200 || !rb?.booking) return send(res, 502, { error: "booking_read_failed", detail: rb?.error || r.status });
4856
- booking = rb.booking;
4857
- } catch (e) {
4858
- return send(res, 502, { error: "booking_read_failed", detail: e.message });
4859
- }
4860
- const remainingUsd = Number(booking.remainingUsd) || 0;
4861
- if (remainingUsd <= BALANCE_EPSILON) {
4862
- return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
4863
- }
4864
- let maxTok = CONTEXT_CEIL;
4865
- const reqMax = Number(request?.max_tokens);
4866
- if (reqMax > 0) maxTok = Math.min(maxTok, Math.floor(reqMax));
4867
- if (outPrice > 0) maxTok = Math.min(maxTok, Math.floor(remainingUsd / outPrice * 1e6));
4868
- const safeRequest = { ...request, max_tokens: Math.max(1, maxTok) };
4869
- let completion;
4870
- try {
4871
- const upstreamRes = await fetch(upstream + "/v1/chat/completions", {
4872
- method: "POST",
4873
- headers: { "content-type": "application/json", authorization: "Bearer " + UPSTREAM_KEY },
4874
- body: JSON.stringify(safeRequest)
4875
- });
4876
- completion = await upstreamRes.json();
4877
- } catch (e) {
4878
- return send(res, 502, { error: "upstream_error", detail: e.message });
4879
- }
4880
- try {
4881
- enforceModelEcho(completion.model, MODEL);
4882
- } catch (e) {
4883
- return send(res, 502, { error: "model_mismatch", detail: e.message });
4884
- }
4885
- let rep;
4886
- try {
4887
- rep = await reportToPlatform(buildDrawReport({ offerId, buyerId, bookingId, n, usage: completion.usage }));
4888
- } catch (e) {
4889
- console.error("mtok-relay: DRAW report failed (network) for n=%d offerId=%s: %s", n, offerId, e.message);
4890
- return send(res, 502, { error: "report_failed", detail: e.message, completion });
4891
- }
4892
- if (!rep.ok || !rep.booking) {
4893
- console.error("mtok-relay: DRAW report rejected (%d) for n=%d offerId=%s: %s", rep.status, n, offerId, JSON.stringify(rep.body));
4894
- return send(res, rep.status === 402 ? 402 : 502, { error: rep.body?.error?.code || rep.body?.error || "report_failed", detail: rep.body?.error?.message || rep.status, completion, _bookingId: bookingId });
4895
- }
4896
- return send(res, 200, { ...completion, _bookingId: rep.booking.id, remainingUsd: rep.booking.remainingUsd });
4897
- }
4898
- var server = http.createServer(async (req, res) => {
4899
- if (req.method !== "POST" || req.url !== "/chunk") {
4900
- return send(res, 404, { error: "not found" });
4901
- }
4902
- let body;
4903
- try {
4904
- body = await readBody(req);
4905
- } catch {
4906
- return send(res, 400, { error: "bad body" });
4907
- }
4908
- const hasFund = body.sellerTxHash != null && body.sellerTxHash !== "";
4909
- const hasDraw = body.request != null;
4910
- if (hasFund && hasDraw) return send(res, 400, { error: "bad_request", detail: "send a FUND (sellerTxHash) or a DRAW (request), not both" });
4911
- if (hasFund) return handleFund(body, res);
4912
- if (hasDraw) return handleDraw(body, res);
4913
- return send(res, 400, { error: "bad_request", detail: "need a FUND (sellerTxHash) or a DRAW (request)" });
4914
- });
4915
- boot().then(() => {
4916
- server.listen(port, () => {
4917
- console.log(`mtok-relay: listening on port ${port} offer=${offerId} model=${MODEL} upstream=${upstream} api=${apiBase} settlement=${SETTLEMENT_ADDR}`);
4918
- });
4919
- }).catch((e) => {
4920
- console.error("mtok-relay: boot failed \u2014", e.message);
5216
+
5217
+ // mtok-relay.mjs
5218
+ try {
5219
+ const config = readRelayConfig();
5220
+ const runtime = await createRelayRuntime(config);
5221
+ startRelayServer({ config, ...runtime });
5222
+ } catch (e) {
5223
+ console.error("mtok-relay: boot failed -", e.message);
4921
5224
  process.exit(1);
4922
- });
5225
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control. Run with: npx mtok-relay --offer <id> --model <id> --upstream <url>.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,8 +33,8 @@
33
33
  "prepublishOnly": "node build-relay-bundle.mjs"
34
34
  },
35
35
  "devDependencies": {
36
- "viem": "^2.0.0",
37
- "esbuild": "^0.28.1"
36
+ "esbuild": "^0.28.1",
37
+ "viem": "^2.0.0"
38
38
  },
39
39
  "overrides": {
40
40
  "ws": "^8.21.0"