mtok-relay 0.2.5 → 0.2.6

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 +64 -36
  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");
@@ -5536,9 +5561,9 @@ function createServeCore({
5536
5561
  requestHash,
5537
5562
  sellerWallet,
5538
5563
  feeRecipient: currentFeeRecipient,
5539
- // A known completion spends no new inference. New claims need a verified
5564
+ // A known claim spends no new inference. New claims need a verified
5540
5565
  // age because both payload and claim markers expire after retention.
5541
- maxPaidAgeMs: redemptionState === "complete" ? void 0 : redemption.retentionMs
5566
+ maxPaidAgeMs: redemptionState === "complete" || redemptionState === "pending" ? void 0 : redemption.retentionMs
5542
5567
  });
5543
5568
  } catch (e) {
5544
5569
  if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
@@ -5546,14 +5571,6 @@ function createServeCore({
5546
5571
  }
5547
5572
  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
5573
  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
5574
  if (screenPayer) {
5558
5575
  try {
5559
5576
  if (await screenPayer(String(paid.from || "").toLowerCase())) {
@@ -5567,6 +5584,17 @@ function createServeCore({
5567
5584
  if (redemptionState === "pending") {
5568
5585
  return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
5569
5586
  }
5587
+ let currentFeeBps;
5588
+ try {
5589
+ currentFeeBps = typeof feeBps === "function" ? await feeBps(paid) : feeBps ?? 0;
5590
+ if (!Number.isSafeInteger(currentFeeBps) || currentFeeBps < 0 || currentFeeBps > 1e4) throw new TypeError("invalid fee rate");
5591
+ } catch {
5592
+ return { status: 503, body: { error: "fee_policy_unavailable", detail: "fee policy could not be verified; retry this same paid draw", _bookingId: bookingId } };
5593
+ }
5594
+ const expectedFee = configuredFeeAtomic({ sellerUsdAtomic: paid.event.sellerUsdAtomic, feeAddress: currentFeeRecipient, feeBps: currentFeeBps });
5595
+ if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5596
+ return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
5597
+ }
5570
5598
  const paidEvent = paid.event;
5571
5599
  const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
5572
5600
  if (remainingUsd < BALANCE_EPSILON) {
@@ -5660,23 +5688,24 @@ async function createRelayRuntime(config) {
5660
5688
  const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
5661
5689
  const drawLocks = /* @__PURE__ */ new Map();
5662
5690
  const platform = await fetchPlatformConfig(config);
5663
- const bootFeeBps = platform.feeBps;
5664
5691
  const FEE_REFRESH_MS = 6e4;
5665
5692
  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();
5693
+ let refreshing;
5694
+ const refreshPlatformFee = () => {
5695
+ if (!refreshing) refreshing = (async () => {
5696
+ try {
5697
+ const fresh = await fetchPlatformConfig(config);
5698
+ platform.feeSchedule = fresh.feeSchedule;
5699
+ lastConfigFetch = Date.now();
5700
+ return true;
5701
+ } catch (e) {
5702
+ (config.log ?? console).error?.(`mtok relay: platform config refresh failed (${e.message}); new claims require current policy`);
5703
+ return false;
5704
+ }
5705
+ })().finally(() => {
5706
+ refreshing = null;
5707
+ });
5708
+ return refreshing;
5680
5709
  };
5681
5710
  const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
5682
5711
  if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
@@ -5701,11 +5730,11 @@ async function createRelayRuntime(config) {
5701
5730
  sellerAgentId: config.sellerAgentId,
5702
5731
  sellerWallet: config.settlementAddr,
5703
5732
  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
5733
  feeRecipient: platform.feeAddress,
5708
- feeBps: () => Math.min(bootFeeBps, platform.feeBps),
5734
+ feeBps: async ({ paidAtMs }) => {
5735
+ if (Date.now() - lastConfigFetch >= FEE_REFRESH_MS && !await refreshPlatformFee()) throw new Error("fee policy refresh failed");
5736
+ return paymentFeeBpsAt(platform.feeSchedule, paidAtMs);
5737
+ },
5709
5738
  // #654: per-relay output sanity ceiling (unset => the shared generous default).
5710
5739
  maxOutputTokens: config.maxOutputTokens,
5711
5740
  screenPayer: payerDenied
@@ -5734,10 +5763,9 @@ async function createRelayRuntime(config) {
5734
5763
  active++;
5735
5764
  try {
5736
5765
  return await withBookingLock(String(body?.bookingId ?? ""), async () => {
5737
- await refreshPlatformFeeIfStale();
5738
5766
  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);
5767
+ if (out.status === 402 && out.body?.detail === "fee_amount_too_low") {
5768
+ out = await refreshPlatformFee() ? await core.serve(body) : { status: 503, body: { error: "fee_policy_unavailable", _bookingId: body.bookingId } };
5741
5769
  }
5742
5770
  return send(res, out.status, out.body);
5743
5771
  });
@@ -5753,7 +5781,7 @@ async function fetchPlatformConfig(config) {
5753
5781
  const body = await r.json();
5754
5782
  return {
5755
5783
  feeAddress: body.feeAddress,
5756
- feeBps: body.feeBps,
5784
+ feeSchedule: normalizeFeeSchedule(body.feeSchedule ?? [{ effectiveAtMs: 0, feeBps: body.feeBps ?? (body.feeAddress ? void 0 : 0) }]),
5757
5785
  dustThresholdUsd: Number(body.dustThresholdUsd) || 1e-3,
5758
5786
  chainId: Number(body.chainId ?? 8453),
5759
5787
  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.6",
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": {