mtok-relay 0.2.5 → 0.2.7

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 +74 -41
  2. package/package.json +1 -1
@@ -5295,9 +5295,9 @@ function createOnchainVerifier({
5295
5295
  if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: "request_hash_mismatch" };
5296
5296
  if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
5297
5297
  const maxAge = Number(maxPaidAgeMs);
5298
+ let paidAtMs = null;
5298
5299
  if (Number.isFinite(maxAge) && maxAge > 0) {
5299
5300
  const bn = matchedLog?.blockNumber ?? got.receipt.blockNumber;
5300
- let paidAtMs = null;
5301
5301
  if (bn != null) {
5302
5302
  for (let i = 0; i <= receiptRetries; i++) {
5303
5303
  try {
@@ -5330,11 +5330,37 @@ function createOnchainVerifier({
5330
5330
  feeTransfer = findUsdcTransfer(got.receipt, { to: feeRecipient, minAtomic: BigInt(event.feeUsdAtomic), from: event.buyer, consumed });
5331
5331
  if (!feeTransfer.ok) return { ok: false, reason: "fee_transfer_" + feeTransfer.reason };
5332
5332
  }
5333
- return { ok: true, event, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
5333
+ return { ok: true, event, paidAtMs, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
5334
5334
  }
5335
5335
  };
5336
5336
  }
5337
5337
 
5338
+ // core/fee-policy.js
5339
+ function normalizeFeeSchedule(schedule) {
5340
+ if (!Array.isArray(schedule) || !schedule.length || schedule.length > 64) throw new TypeError("fee schedule must contain 1..64 activation records");
5341
+ let previous = -1;
5342
+ return schedule.map(({ effectiveAtMs, feeBps }, index) => {
5343
+ if (!Number.isSafeInteger(effectiveAtMs) || effectiveAtMs <= previous || index === 0 && effectiveAtMs !== 0) {
5344
+ throw new TypeError("fee activations must start at 0 and increase in integer milliseconds");
5345
+ }
5346
+ if (!Number.isSafeInteger(feeBps) || feeBps < 0 || feeBps > 1e4) throw new TypeError("feeBps must be an integer from 0 to 10000");
5347
+ previous = effectiveAtMs;
5348
+ return { effectiveAtMs, feeBps };
5349
+ });
5350
+ }
5351
+ function feeBpsAt(schedule, timeMs) {
5352
+ if (!Number.isSafeInteger(timeMs) || timeMs < 0) throw new TypeError("verified payment time is required for fee policy");
5353
+ return schedule.findLast((entry) => entry.effectiveAtMs <= timeMs).feeBps;
5354
+ }
5355
+ function paymentFeeBpsAt(schedule, paidAtMs) {
5356
+ const start = Math.max(0, paidAtMs - 6e4);
5357
+ let minimum = Math.min(feeBpsAt(schedule, paidAtMs), feeBpsAt(schedule, start));
5358
+ for (const entry of schedule) {
5359
+ if (entry.effectiveAtMs > start && entry.effectiveAtMs <= paidAtMs) minimum = Math.min(minimum, entry.feeBps);
5360
+ }
5361
+ return minimum;
5362
+ }
5363
+
5338
5364
  // bridge/serve-core.mjs
5339
5365
  var BALANCE_EPSILON = 1e-6;
5340
5366
  var REQUEST_NONCE_RE = /^0x[0-9a-fA-F]{32}$/;
@@ -5489,7 +5515,6 @@ function createServeCore({
5489
5515
  maxOutputTokens
5490
5516
  }) {
5491
5517
  const serve = async (body) => {
5492
- const currentFeeBps = typeof feeBps === "function" ? feeBps() : feeBps;
5493
5518
  const currentFeeRecipient = typeof feeRecipient === "function" ? feeRecipient() : feeRecipient;
5494
5519
  const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
5495
5520
  const hasRequestNonce = Object.hasOwn(body, "requestNonce");
@@ -5507,19 +5532,20 @@ function createServeCore({
5507
5532
  const requestHash = hasRequestNonce ? await hash32({ request, requestNonce }) : await hash32(request);
5508
5533
  const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
5509
5534
  const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
5535
+ const redemptionContext = { claimKey: cacheKey };
5510
5536
  if (!dripContractAddress) {
5511
5537
  return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
5512
5538
  }
5513
5539
  if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
5514
5540
  let storedKey = cacheKey;
5515
- let redemptionState = await redemption.state(storedKey);
5541
+ let redemptionState = await redemption.state(storedKey, redemptionContext);
5516
5542
  if (!redemptionState && oldLegacyKey) {
5517
- const oldLegacyState = await redemption.state(oldLegacyKey);
5543
+ const oldLegacyState = await redemption.state(oldLegacyKey, redemptionContext);
5518
5544
  if (oldLegacyState) {
5519
5545
  storedKey = oldLegacyKey;
5520
5546
  redemptionState = oldLegacyState;
5521
5547
  } else {
5522
- redemptionState = await redemption.state(cacheKey);
5548
+ redemptionState = await redemption.state(cacheKey, redemptionContext);
5523
5549
  }
5524
5550
  }
5525
5551
  let paid;
@@ -5536,9 +5562,9 @@ function createServeCore({
5536
5562
  requestHash,
5537
5563
  sellerWallet,
5538
5564
  feeRecipient: currentFeeRecipient,
5539
- // A known completion spends no new inference. New claims need a verified
5565
+ // A known claim spends no new inference. New claims need a verified
5540
5566
  // age because both payload and claim markers expire after retention.
5541
- maxPaidAgeMs: redemptionState === "complete" ? void 0 : redemption.retentionMs
5567
+ maxPaidAgeMs: redemptionState === "complete" || redemptionState === "pending" ? void 0 : redemption.retentionMs
5542
5568
  });
5543
5569
  } catch (e) {
5544
5570
  if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
@@ -5546,14 +5572,6 @@ function createServeCore({
5546
5572
  }
5547
5573
  if (paid?.reason === "payment_age_unavailable") return { status: 503, body: { error: "payment_age_unavailable", detail: "payment age could not be verified; retry this same paid draw", _bookingId: bookingId } };
5548
5574
  if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
5549
- const expectedFee = configuredFeeAtomic({
5550
- sellerUsdAtomic: paid.event.sellerUsdAtomic,
5551
- feeAddress: currentFeeRecipient,
5552
- feeBps: currentFeeBps
5553
- });
5554
- if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5555
- return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
5556
- }
5557
5575
  if (screenPayer) {
5558
5576
  try {
5559
5577
  if (await screenPayer(String(paid.from || "").toLowerCase())) {
@@ -5563,10 +5581,25 @@ function createServeCore({
5563
5581
  return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
5564
5582
  }
5565
5583
  }
5566
- if (redemptionState === "complete") return { status: 200, body: await redemption.get(storedKey) };
5584
+ if (redemptionState === "complete") {
5585
+ const payload2 = await redemption.get(storedKey, redemptionContext);
5586
+ if (payload2 == null) return { status: 503, body: { error: "redemption_unavailable", detail: "saved completion is no longer readable; retry this same paid draw", _bookingId: bookingId } };
5587
+ return { status: 200, body: payload2 };
5588
+ }
5567
5589
  if (redemptionState === "pending") {
5568
5590
  return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5569
5591
  }
5592
+ let currentFeeBps;
5593
+ try {
5594
+ currentFeeBps = typeof feeBps === "function" ? await feeBps(paid) : feeBps ?? 0;
5595
+ if (!Number.isSafeInteger(currentFeeBps) || currentFeeBps < 0 || currentFeeBps > 1e4) throw new TypeError("invalid fee rate");
5596
+ } catch {
5597
+ return { status: 503, body: { error: "fee_policy_unavailable", detail: "fee policy could not be verified; retry this same paid draw", _bookingId: bookingId } };
5598
+ }
5599
+ const expectedFee = configuredFeeAtomic({ sellerUsdAtomic: paid.event.sellerUsdAtomic, feeAddress: currentFeeRecipient, feeBps: currentFeeBps });
5600
+ if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5601
+ return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
5602
+ }
5570
5603
  const paidEvent = paid.event;
5571
5604
  const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
5572
5605
  if (remainingUsd < BALANCE_EPSILON) {
@@ -5583,7 +5616,7 @@ function createServeCore({
5583
5616
  }
5584
5617
  const safeRequest = { ...checked.safeRequest, model, max_tokens: bound.maxTok };
5585
5618
  try {
5586
- if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
5619
+ if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey, { paidAtMs: paid.paidAtMs })) {
5587
5620
  return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5588
5621
  }
5589
5622
  } catch (e) {
@@ -5660,23 +5693,24 @@ async function createRelayRuntime(config) {
5660
5693
  const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
5661
5694
  const drawLocks = /* @__PURE__ */ new Map();
5662
5695
  const platform = await fetchPlatformConfig(config);
5663
- const bootFeeBps = platform.feeBps;
5664
5696
  const FEE_REFRESH_MS = 6e4;
5665
5697
  let lastConfigFetch = Date.now();
5666
- const refreshPlatformFee = async () => {
5667
- lastConfigFetch = Date.now();
5668
- try {
5669
- const fresh = await fetchPlatformConfig(config);
5670
- platform.feeBps = fresh.feeBps;
5671
- return true;
5672
- } catch (e) {
5673
- (config.log ?? console).error?.(`mtok relay: platform config refresh failed (${e.message}); keeping last-known fee rate`);
5674
- return false;
5675
- }
5676
- };
5677
- const refreshPlatformFeeIfStale = async () => {
5678
- if (Date.now() - lastConfigFetch < FEE_REFRESH_MS) return;
5679
- await refreshPlatformFee();
5698
+ let refreshing;
5699
+ const refreshPlatformFee = () => {
5700
+ if (!refreshing) refreshing = (async () => {
5701
+ try {
5702
+ const fresh = await fetchPlatformConfig(config);
5703
+ platform.feeSchedule = fresh.feeSchedule;
5704
+ lastConfigFetch = Date.now();
5705
+ return true;
5706
+ } catch (e) {
5707
+ (config.log ?? console).error?.(`mtok relay: platform config refresh failed (${e.message}); new claims require current policy`);
5708
+ return false;
5709
+ }
5710
+ })().finally(() => {
5711
+ refreshing = null;
5712
+ });
5713
+ return refreshing;
5680
5714
  };
5681
5715
  const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
5682
5716
  if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
@@ -5701,11 +5735,11 @@ async function createRelayRuntime(config) {
5701
5735
  sellerAgentId: config.sellerAgentId,
5702
5736
  sellerWallet: config.settlementAddr,
5703
5737
  dripContractAddress: platform.dripContractAddress,
5704
- // #654: the fee floor is min(boot, current) so neither a fee increase nor a
5705
- // decrease can over-demand and strand an honest already-paid draw; the recipient
5706
- // stays pinned to the boot address (exact-match verify, see the note above).
5707
5738
  feeRecipient: platform.feeAddress,
5708
- feeBps: () => Math.min(bootFeeBps, platform.feeBps),
5739
+ feeBps: async ({ paidAtMs }) => {
5740
+ if (Date.now() - lastConfigFetch >= FEE_REFRESH_MS && !await refreshPlatformFee()) throw new Error("fee policy refresh failed");
5741
+ return paymentFeeBpsAt(platform.feeSchedule, paidAtMs);
5742
+ },
5709
5743
  // #654: per-relay output sanity ceiling (unset => the shared generous default).
5710
5744
  maxOutputTokens: config.maxOutputTokens,
5711
5745
  screenPayer: payerDenied
@@ -5734,10 +5768,9 @@ async function createRelayRuntime(config) {
5734
5768
  active++;
5735
5769
  try {
5736
5770
  return await withBookingLock(String(body?.bookingId ?? ""), async () => {
5737
- await refreshPlatformFeeIfStale();
5738
5771
  let out = await core.serve(body);
5739
- if (out.status === 402 && out.body?.detail === "fee_amount_too_low" && await refreshPlatformFee()) {
5740
- out = await core.serve(body);
5772
+ if (out.status === 402 && out.body?.detail === "fee_amount_too_low") {
5773
+ out = await refreshPlatformFee() ? await core.serve(body) : { status: 503, body: { error: "fee_policy_unavailable", _bookingId: body.bookingId } };
5741
5774
  }
5742
5775
  return send(res, out.status, out.body);
5743
5776
  });
@@ -5753,7 +5786,7 @@ async function fetchPlatformConfig(config) {
5753
5786
  const body = await r.json();
5754
5787
  return {
5755
5788
  feeAddress: body.feeAddress,
5756
- feeBps: body.feeBps,
5789
+ feeSchedule: normalizeFeeSchedule(body.feeSchedule ?? [{ effectiveAtMs: 0, feeBps: body.feeBps ?? (body.feeAddress ? void 0 : 0) }]),
5757
5790
  dustThresholdUsd: Number(body.dustThresholdUsd) || 1e-3,
5758
5791
  chainId: Number(body.chainId ?? 8453),
5759
5792
  usdcAddress: body.usdcAddress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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": {