@antseed/node 0.2.105 → 0.2.107

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 (220) hide show
  1. package/dist/billing/index.d.ts +2 -0
  2. package/dist/billing/index.d.ts.map +1 -0
  3. package/dist/billing/index.js +2 -0
  4. package/dist/billing/index.js.map +1 -0
  5. package/dist/billing/unit.d.ts +3 -0
  6. package/dist/billing/unit.d.ts.map +1 -0
  7. package/dist/billing/unit.js +3 -0
  8. package/dist/billing/unit.js.map +1 -0
  9. package/dist/buyer-request-handler.d.ts +1 -54
  10. package/dist/buyer-request-handler.d.ts.map +1 -1
  11. package/dist/buyer-request-handler.js +2 -379
  12. package/dist/buyer-request-handler.js.map +1 -1
  13. package/dist/discovery/announcer.d.ts +7 -1
  14. package/dist/discovery/announcer.d.ts.map +1 -1
  15. package/dist/discovery/announcer.js +84 -33
  16. package/dist/discovery/announcer.js.map +1 -1
  17. package/dist/discovery/http-metadata-resolver.d.ts +4 -0
  18. package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
  19. package/dist/discovery/http-metadata-resolver.js +42 -2
  20. package/dist/discovery/http-metadata-resolver.js.map +1 -1
  21. package/dist/discovery/index.d.ts +1 -1
  22. package/dist/discovery/index.d.ts.map +1 -1
  23. package/dist/discovery/index.js +1 -1
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/metadata-codec.d.ts +4 -4
  26. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  27. package/dist/discovery/metadata-codec.js +339 -23
  28. package/dist/discovery/metadata-codec.js.map +1 -1
  29. package/dist/discovery/metadata-validator.d.ts +7 -3
  30. package/dist/discovery/metadata-validator.d.ts.map +1 -1
  31. package/dist/discovery/metadata-validator.js +139 -8
  32. package/dist/discovery/metadata-validator.js.map +1 -1
  33. package/dist/discovery/peer-lookup.d.ts +1 -0
  34. package/dist/discovery/peer-lookup.d.ts.map +1 -1
  35. package/dist/discovery/peer-lookup.js +23 -5
  36. package/dist/discovery/peer-lookup.js.map +1 -1
  37. package/dist/discovery/peer-metadata.d.ts +2 -90
  38. package/dist/discovery/peer-metadata.d.ts.map +1 -1
  39. package/dist/discovery/peer-metadata.js +3 -11
  40. package/dist/discovery/peer-metadata.js.map +1 -1
  41. package/dist/discovery/seller-address-resolver.d.ts +1 -49
  42. package/dist/discovery/seller-address-resolver.d.ts.map +1 -1
  43. package/dist/discovery/seller-address-resolver.js +2 -114
  44. package/dist/discovery/seller-address-resolver.js.map +1 -1
  45. package/dist/errors.d.ts +43 -0
  46. package/dist/errors.d.ts.map +1 -0
  47. package/dist/errors.js +73 -0
  48. package/dist/errors.js.map +1 -0
  49. package/dist/health/model-health-checker.d.ts +6 -5
  50. package/dist/health/model-health-checker.d.ts.map +1 -1
  51. package/dist/health/model-health-checker.js +22 -10
  52. package/dist/health/model-health-checker.js.map +1 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/interfaces/seller-provider.d.ts +6 -0
  58. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  59. package/dist/node.d.ts.map +1 -1
  60. package/dist/node.js +28 -0
  61. package/dist/node.js.map +1 -1
  62. package/dist/p2p/connection-auth.d.ts +3 -8
  63. package/dist/p2p/connection-auth.d.ts.map +1 -1
  64. package/dist/p2p/connection-auth.js +4 -6
  65. package/dist/p2p/connection-auth.js.map +1 -1
  66. package/dist/p2p/connection-manager.d.ts +3 -1
  67. package/dist/p2p/connection-manager.d.ts.map +1 -1
  68. package/dist/p2p/connection-manager.js.map +1 -1
  69. package/dist/p2p/identity.d.ts +1 -21
  70. package/dist/p2p/identity.d.ts.map +1 -1
  71. package/dist/p2p/identity.js +3 -62
  72. package/dist/p2p/identity.js.map +1 -1
  73. package/dist/p2p/message-protocol.d.ts +1 -49
  74. package/dist/p2p/message-protocol.d.ts.map +1 -1
  75. package/dist/p2p/message-protocol.js +2 -125
  76. package/dist/p2p/message-protocol.js.map +1 -1
  77. package/dist/p2p/payment-codec.d.ts +1 -21
  78. package/dist/p2p/payment-codec.d.ts.map +1 -1
  79. package/dist/p2p/payment-codec.js +2 -220
  80. package/dist/p2p/payment-codec.js.map +1 -1
  81. package/dist/p2p/payment-mux.d.ts +1 -51
  82. package/dist/p2p/payment-mux.d.ts.map +1 -1
  83. package/dist/p2p/payment-mux.js +2 -159
  84. package/dist/p2p/payment-mux.js.map +1 -1
  85. package/dist/payments/buyer-free-usage-manager.d.ts +1 -43
  86. package/dist/payments/buyer-free-usage-manager.d.ts.map +1 -1
  87. package/dist/payments/buyer-free-usage-manager.js +2 -298
  88. package/dist/payments/buyer-free-usage-manager.js.map +1 -1
  89. package/dist/payments/buyer-payment-manager.d.ts +1 -277
  90. package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
  91. package/dist/payments/buyer-payment-manager.js +2 -1039
  92. package/dist/payments/buyer-payment-manager.js.map +1 -1
  93. package/dist/payments/buyer-payment-negotiator.d.ts +1 -130
  94. package/dist/payments/buyer-payment-negotiator.d.ts.map +1 -1
  95. package/dist/payments/buyer-payment-negotiator.js +2 -950
  96. package/dist/payments/buyer-payment-negotiator.js.map +1 -1
  97. package/dist/payments/chain-config.d.ts.map +1 -1
  98. package/dist/payments/chain-config.js +13 -2
  99. package/dist/payments/chain-config.js.map +1 -1
  100. package/dist/payments/channel-session-state.d.ts +1 -13
  101. package/dist/payments/channel-session-state.d.ts.map +1 -1
  102. package/dist/payments/channel-session-state.js +2 -25
  103. package/dist/payments/channel-session-state.js.map +1 -1
  104. package/dist/payments/channel-store.d.ts +2 -51
  105. package/dist/payments/channel-store.d.ts.map +1 -1
  106. package/dist/payments/channel-store.js +3 -14
  107. package/dist/payments/channel-store.js.map +1 -1
  108. package/dist/payments/channel-usage-accounting.d.ts +1 -17
  109. package/dist/payments/channel-usage-accounting.d.ts.map +1 -1
  110. package/dist/payments/channel-usage-accounting.js +2 -65
  111. package/dist/payments/channel-usage-accounting.js.map +1 -1
  112. package/dist/payments/evm/base-evm-client.d.ts +1 -26
  113. package/dist/payments/evm/base-evm-client.d.ts.map +1 -1
  114. package/dist/payments/evm/base-evm-client.js +2 -115
  115. package/dist/payments/evm/base-evm-client.js.map +1 -1
  116. package/dist/payments/evm/channels-client.d.ts +1 -85
  117. package/dist/payments/evm/channels-client.d.ts.map +1 -1
  118. package/dist/payments/evm/channels-client.js +2 -193
  119. package/dist/payments/evm/channels-client.js.map +1 -1
  120. package/dist/payments/evm/deposits-client.d.ts +1 -29
  121. package/dist/payments/evm/deposits-client.d.ts.map +1 -1
  122. package/dist/payments/evm/deposits-client.js +2 -67
  123. package/dist/payments/evm/deposits-client.js.map +1 -1
  124. package/dist/payments/evm/signatures.d.ts +1 -203
  125. package/dist/payments/evm/signatures.d.ts.map +1 -1
  126. package/dist/payments/evm/signatures.js +2 -236
  127. package/dist/payments/evm/signatures.js.map +1 -1
  128. package/dist/payments/evm/subpool-client.d.ts +32 -0
  129. package/dist/payments/evm/subpool-client.d.ts.map +1 -0
  130. package/dist/payments/evm/subpool-client.js +158 -0
  131. package/dist/payments/evm/subpool-client.js.map +1 -0
  132. package/dist/payments/index.d.ts +1 -1
  133. package/dist/payments/index.d.ts.map +1 -1
  134. package/dist/payments/index.js +1 -1
  135. package/dist/payments/index.js.map +1 -1
  136. package/dist/payments/pricing.d.ts +1 -26
  137. package/dist/payments/pricing.d.ts.map +1 -1
  138. package/dist/payments/pricing.js +2 -36
  139. package/dist/payments/pricing.js.map +1 -1
  140. package/dist/payments/seller-payment-manager.d.ts +7 -0
  141. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  142. package/dist/payments/seller-payment-manager.js +27 -2
  143. package/dist/payments/seller-payment-manager.js.map +1 -1
  144. package/dist/payments/usdc-utils.d.ts +1 -8
  145. package/dist/payments/usdc-utils.d.ts.map +1 -1
  146. package/dist/payments/usdc-utils.js +2 -16
  147. package/dist/payments/usdc-utils.js.map +1 -1
  148. package/dist/proxy/proxy-mux.d.ts +1 -69
  149. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  150. package/dist/proxy/proxy-mux.js +2 -346
  151. package/dist/proxy/proxy-mux.js.map +1 -1
  152. package/dist/proxy/request-codec.d.ts +1 -41
  153. package/dist/proxy/request-codec.d.ts.map +1 -1
  154. package/dist/proxy/request-codec.js +2 -280
  155. package/dist/proxy/request-codec.js.map +1 -1
  156. package/dist/seller-request-handler.d.ts +10 -3
  157. package/dist/seller-request-handler.d.ts.map +1 -1
  158. package/dist/seller-request-handler.js +112 -17
  159. package/dist/seller-request-handler.js.map +1 -1
  160. package/dist/types/billing.d.ts +2 -0
  161. package/dist/types/billing.d.ts.map +1 -0
  162. package/dist/types/billing.js +2 -0
  163. package/dist/types/billing.js.map +1 -0
  164. package/dist/types/capability.d.ts +1 -25
  165. package/dist/types/capability.d.ts.map +1 -1
  166. package/dist/types/capability.js +2 -1
  167. package/dist/types/capability.js.map +1 -1
  168. package/dist/types/connection.d.ts +1 -26
  169. package/dist/types/connection.d.ts.map +1 -1
  170. package/dist/types/connection.js +2 -10
  171. package/dist/types/connection.js.map +1 -1
  172. package/dist/types/http.d.ts +1 -39
  173. package/dist/types/http.d.ts.map +1 -1
  174. package/dist/types/http.js +2 -16
  175. package/dist/types/http.js.map +1 -1
  176. package/dist/types/index.d.ts +1 -0
  177. package/dist/types/index.d.ts.map +1 -1
  178. package/dist/types/index.js +1 -0
  179. package/dist/types/index.js.map +1 -1
  180. package/dist/types/peer.d.ts +9 -31
  181. package/dist/types/peer.d.ts.map +1 -1
  182. package/dist/types/peer.js +2 -14
  183. package/dist/types/peer.js.map +1 -1
  184. package/dist/types/protocol.d.ts +1 -306
  185. package/dist/types/protocol.d.ts.map +1 -1
  186. package/dist/types/protocol.js +2 -81
  187. package/dist/types/protocol.js.map +1 -1
  188. package/dist/types/service-api.d.ts +1 -5
  189. package/dist/types/service-api.d.ts.map +1 -1
  190. package/dist/types/service-api.js +2 -12
  191. package/dist/types/service-api.js.map +1 -1
  192. package/dist/utils/debug.d.ts +1 -5
  193. package/dist/utils/debug.d.ts.map +1 -1
  194. package/dist/utils/debug.js +2 -52
  195. package/dist/utils/debug.js.map +1 -1
  196. package/dist/utils/hex.d.ts +1 -2
  197. package/dist/utils/hex.d.ts.map +1 -1
  198. package/dist/utils/hex.js +2 -14
  199. package/dist/utils/hex.js.map +1 -1
  200. package/dist/utils/json-codec.d.ts +1 -9
  201. package/dist/utils/json-codec.d.ts.map +1 -1
  202. package/dist/utils/json-codec.js +2 -51
  203. package/dist/utils/json-codec.js.map +1 -1
  204. package/dist/utils/response-usage.d.ts +1 -9
  205. package/dist/utils/response-usage.d.ts.map +1 -1
  206. package/dist/utils/response-usage.js +2 -39
  207. package/dist/utils/response-usage.js.map +1 -1
  208. package/dist/verification/codec.d.ts +1 -3
  209. package/dist/verification/codec.d.ts.map +1 -1
  210. package/dist/verification/codec.js +2 -37
  211. package/dist/verification/codec.js.map +1 -1
  212. package/dist/verification/response-auth.d.ts +1 -30
  213. package/dist/verification/response-auth.d.ts.map +1 -1
  214. package/dist/verification/response-auth.js +2 -110
  215. package/dist/verification/response-auth.js.map +1 -1
  216. package/dist/verification/verification-mux.d.ts +1 -19
  217. package/dist/verification/verification-mux.d.ts.map +1 -1
  218. package/dist/verification/verification-mux.js +2 -99
  219. package/dist/verification/verification-mux.js.map +1 -1
  220. package/package.json +4 -2
@@ -1,1040 +1,3 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { DepositsClient } from './evm/deposits-client.js';
3
- import { signSpendingAuth, signReserveAuth, makeChannelsDomain, computeMetadataHash, encodeMetadata, ZERO_METADATA, ZERO_METADATA_HASH, computeChannelId, } from './evm/signatures.js';
4
- import { debugLog, debugWarn } from '../utils/debug.js';
5
- import { peerIdToAddress } from '../types/peer.js';
6
- import { CHANNEL_ROLE, CHANNEL_STATUS } from './channel-store.js';
7
- import { advanceUsageMetadata, CountedRequestTracker, RequestServiceTracker } from './channel-usage-accounting.js';
8
- import { estimateCostFromBytes, computeCostUsdc } from './pricing.js';
9
- /** Default tolerance: accept seller claims up to 1.4x buyer's estimate. */
10
- const DEFAULT_COST_TOLERANCE = 1.4;
11
- /** Fraction of reserve ceiling at which to signal a top-up is needed.
12
- * Trigger well before the contract's TOP_UP_SETTLED_THRESHOLD_BPS (85%)
13
- * so that by the time the seller calls topUp() on-chain, enough has been
14
- * settled to pass the threshold check. */
15
- const DEFAULT_TOPUP_THRESHOLD = 0.65;
16
- /**
17
- * Manages buyer-side payment sessions using EIP-712 SpendingAuth
18
- * with cumulative authorization, bytes/4 cost verification, and overdraft control.
19
- */
20
- export class BuyerPaymentManager {
21
- _identity;
22
- _signer;
23
- _depositsClient;
24
- _config;
25
- _channelStore;
26
- /** In-memory map of active confirmed sessions by seller peerId for fast lookups. */
27
- _confirmedPeers = new Set();
28
- /** Peers that explicitly rejected our spending auth. */
29
- _rejectedPeers = new Set();
30
- _sellerAddressResolver;
31
- /** sellerPeerId -> cumulative USDC amount in the latest SpendingAuth */
32
- _cumulativeAmount = new Map();
33
- /** sellerPeerId -> cumulative metadata for SpendingAuth */
34
- _metadata = new Map();
35
- /** sellerPeerId -> buyer-verified cumulative cost from bytes/4 */
36
- _verifiedCost = new Map();
37
- /** requestId -> service/model the buyer requested (from its own request body).
38
- * Used in handleNeedAuth to validate cost with the correct pricing tier
39
- * without trusting the seller's claim of which service was used. */
40
- _requestService = new RequestServiceTracker();
41
- /** sellerPeerId -> full pricing map (defaults + per-service overrides from peer metadata / 402) */
42
- _sessionPricing = new Map();
43
- /** Cumulative response token totals per seller, tracked independently of signing metadata. */
44
- _responseTokenTotals = new Map();
45
- /** sellerPeerId -> current on-chain reserve ceiling (can grow with top-ups) */
46
- _currentReserveCeiling = new Map();
47
- /** sellerPeerId -> original first-reserve amount for replaying reserve() safely. */
48
- _initialReserveAmount = new Map();
49
- /** sellerPeerId -> salt used in the current reserve */
50
- _reserveSalt = new Map();
51
- /** Cached EIP-712 domain — static for the lifetime of this manager. */
52
- _channelsDomain;
53
- constructor(identity, config, channelStore, sellerAddressResolver) {
54
- this._identity = identity;
55
- this._config = config;
56
- this._sellerAddressResolver = sellerAddressResolver;
57
- this._signer = identity.wallet;
58
- this._depositsClient = new DepositsClient({
59
- rpcUrl: config.rpcUrl,
60
- ...(config.fallbackRpcUrls ? { fallbackRpcUrls: config.fallbackRpcUrls } : {}),
61
- contractAddress: config.depositsContractAddress,
62
- usdcAddress: config.usdcAddress,
63
- evmChainId: config.chainId,
64
- });
65
- this._channelStore = channelStore;
66
- this._channelsDomain = makeChannelsDomain(config.chainId, config.channelsContractAddress);
67
- // Hydrate cumulative maps from persisted active sessions
68
- this._hydrateFromStore();
69
- }
70
- /** Hydrate cumulative tracking maps from persisted active buyer sessions. */
71
- _hydrateFromStore() {
72
- const activeChannels = this._channelStore.getActiveChannelsByBuyer(CHANNEL_ROLE.BUYER, this._identity.wallet.address);
73
- for (const channel of activeChannels) {
74
- const peerId = channel.peerId;
75
- const persistedCumulative = BigInt(channel.authMax);
76
- this._cumulativeAmount.set(peerId, persistedCumulative);
77
- const metadata = this._sanitizeMetadata(this._channelStore.getChannelMetadata(channel));
78
- this._metadata.set(peerId, metadata);
79
- if (!this._disableMetadataV2Services) {
80
- this._persistServiceMetadata(channel.sessionId, metadata);
81
- }
82
- // Hydrate verifiedCost to authMax so _maxSignable can grow beyond maxPerRequestUsdc.
83
- // Without this, maxSignable = 0 + maxPerRequestUsdc after restart, permanently capping
84
- // the cumulative and causing non-monotonic SpendingAuth rejections on the seller.
85
- this._verifiedCost.set(peerId, persistedCumulative);
86
- this._responseTokenTotals.set(peerId, {
87
- input: Number(channel.tokensDelivered),
88
- output: Number(channel.previousConsumption),
89
- requests: channel.requestCount,
90
- });
91
- }
92
- }
93
- get signer() {
94
- return this._signer;
95
- }
96
- setSigner(signer) {
97
- this._signer = signer;
98
- }
99
- get depositsClient() {
100
- return this._depositsClient;
101
- }
102
- get _costTolerance() {
103
- return this._config.costToleranceMultiplier ?? DEFAULT_COST_TOLERANCE;
104
- }
105
- get _disableMetadataV2Services() {
106
- return this._config.disableMetadataV2Services === true;
107
- }
108
- _sanitizeMetadata(metadata) {
109
- const current = metadata ?? ZERO_METADATA;
110
- if (!this._disableMetadataV2Services)
111
- return current;
112
- return {
113
- cumulativeInputTokens: current.cumulativeInputTokens,
114
- cumulativeOutputTokens: current.cumulativeOutputTokens,
115
- cumulativeRequestCount: current.cumulativeRequestCount,
116
- services: [],
117
- };
118
- }
119
- _advanceUsageMetadata(previous, service, delta) {
120
- return advanceUsageMetadata(this._sanitizeMetadata(previous), this._disableMetadataV2Services ? undefined : service, delta);
121
- }
122
- _getCeiling(sellerPeerId) {
123
- return this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
124
- }
125
- /** Clean up all in-memory state for a seller when the session ends. */
126
- cleanupSession(sellerPeerId) {
127
- this._cumulativeAmount.delete(sellerPeerId);
128
- this._metadata.delete(sellerPeerId);
129
- this._verifiedCost.delete(sellerPeerId);
130
- this._sessionPricing.delete(sellerPeerId);
131
- this._currentReserveCeiling.delete(sellerPeerId);
132
- this._initialReserveAmount.delete(sellerPeerId);
133
- this._reserveSalt.delete(sellerPeerId);
134
- this._confirmedPeers.delete(sellerPeerId);
135
- this._rejectedPeers.delete(sellerPeerId);
136
- this._responseTokenTotals.delete(sellerPeerId);
137
- }
138
- getActiveSession(sellerPeerId) {
139
- return this._channelStore.getActiveChannelByPeerAndBuyer(sellerPeerId, CHANNEL_ROLE.BUYER, this._identity.wallet.address);
140
- }
141
- retireSession(sellerPeerId, status, settledAmount) {
142
- const session = this.getActiveSession(sellerPeerId);
143
- if (session) {
144
- this._channelStore.updateChannelStatus(session.sessionId, status, settledAmount !== undefined ? settledAmount.toString() : undefined);
145
- }
146
- this.cleanupSession(sellerPeerId);
147
- }
148
- canReplayReserveAuth(sellerPeerId) {
149
- return this._reserveSalt.has(sellerPeerId);
150
- }
151
- clearLockConfirmation(sellerPeerId) {
152
- this._confirmedPeers.delete(sellerPeerId);
153
- this._rejectedPeers.delete(sellerPeerId);
154
- }
155
- async resendCurrentSpendingAuth(sellerPeerId, paymentMux) {
156
- const session = this.getActiveSession(sellerPeerId);
157
- if (!session) {
158
- throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
159
- }
160
- const cumulativeAmount = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
161
- const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
162
- const metadataHashHex = computeMetadataHash(currentMeta);
163
- const encodedMetadata = encodeMetadata(currentMeta);
164
- const metadataMsg = {
165
- channelId: session.sessionId,
166
- cumulativeAmount,
167
- metadataHash: metadataHashHex,
168
- };
169
- const spendingAuthSig = await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg);
170
- paymentMux.sendSpendingAuth({
171
- channelId: session.sessionId,
172
- cumulativeAmount: cumulativeAmount.toString(),
173
- metadataHash: metadataHashHex,
174
- metadata: encodedMetadata,
175
- spendingAuthSig,
176
- });
177
- return session.sessionId;
178
- }
179
- async extendCurrentSpendingAuth(sellerPeerId, minBudgetPerRequest, paymentMux, targetCumulative) {
180
- const session = this.getActiveSession(sellerPeerId);
181
- if (!session) {
182
- throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
183
- }
184
- const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
185
- let maxSignable = this._maxSignable(sellerPeerId);
186
- maxSignable = this._reopenOverdraftWindowIfCollapsed(sellerPeerId, currentCumulative, maxSignable, 'extendCurrentSpendingAuth');
187
- const ceiling = this._getCeiling(sellerPeerId);
188
- // Prefer the seller-supplied target when present — a raw
189
- // `currentCumulative + minBudgetPerRequest` advance may still fall short
190
- // of what the seller has already spent, producing an infinite 402 loop.
191
- const minAdvance = currentCumulative + minBudgetPerRequest;
192
- const requestedAmount = targetCumulative != null && targetCumulative > minAdvance
193
- ? targetCumulative
194
- : minAdvance;
195
- const nextCumulative = requestedAmount < maxSignable ? requestedAmount : maxSignable;
196
- const extendNeedsTopUp = this._needsCeilingAdvance(requestedAmount, maxSignable, ceiling);
197
- if (nextCumulative <= currentCumulative) {
198
- // Nothing to sign at current ceiling — try topUp anyway for next round
199
- if (extendNeedsTopUp) {
200
- await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'extendCurrentSpendingAuth');
201
- }
202
- debugWarn(`[BuyerPayment] Cannot extend active session for ${sellerPeerId.slice(0, 12)}... ` +
203
- `(current=${currentCumulative} maxSignable=${maxSignable} target=${targetCumulative ?? 'n/a'})`);
204
- return session.sessionId;
205
- }
206
- const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
207
- await this._sendUpdatedSpendingAuth(session, sellerPeerId, nextCumulative, currentMeta, paymentMux);
208
- // Send topUp AFTER the SpendingAuth so the seller processes the higher
209
- // cumulative first, meeting the on-chain settle threshold for topUp.
210
- if (extendNeedsTopUp) {
211
- await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'extendCurrentSpendingAuth');
212
- }
213
- return session.sessionId;
214
- }
215
- /**
216
- * Build a CloseChannelRequest for an active session.
217
- *
218
- * With `includeAuth` (the default) the buyer signs its current cumulative so
219
- * a seller that lost the last SpendingAuth can still close at the full
220
- * amount owed. Signing costs nothing extra: the cumulative is unchanged, so
221
- * this authorizes no more than the seller could already claim. Pass
222
- * `includeAuth: false` to send a bare request and let the seller close using
223
- * whatever auth it already holds.
224
- */
225
- async buildCloseChannelRequest(sellerPeerId, { includeAuth = true } = {}) {
226
- const session = this.getActiveSession(sellerPeerId);
227
- if (!session) {
228
- throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
229
- }
230
- const cumulativeAmount = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
231
- if (!includeAuth || cumulativeAmount <= 0n) {
232
- return { version: 1, channelId: session.sessionId };
233
- }
234
- const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
235
- const metadataHash = computeMetadataHash(currentMeta);
236
- const metadataMsg = {
237
- channelId: session.sessionId,
238
- cumulativeAmount,
239
- metadataHash,
240
- };
241
- return {
242
- version: 1,
243
- channelId: session.sessionId,
244
- cumulativeAmount: cumulativeAmount.toString(),
245
- metadataHash,
246
- metadata: encodeMetadata(currentMeta),
247
- spendingAuthSig: await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg),
248
- };
249
- }
250
- async resendReserveAuth(sellerPeerId, paymentMux) {
251
- const session = this.getActiveSession(sellerPeerId);
252
- const salt = this._reserveSalt.get(sellerPeerId);
253
- if (!session || !salt) {
254
- throw new Error(`[BuyerPayment] No replayable reserve for seller ${sellerPeerId.slice(0, 12)}...`);
255
- }
256
- // Force a fresh AuthAck after replaying the reserve path.
257
- this._confirmedPeers.delete(sellerPeerId);
258
- const replayAmount = this._initialReserveAmount.get(sellerPeerId)
259
- ?? this._currentReserveCeiling.get(sellerPeerId)
260
- ?? this._config.maxReserveAmountUsdc;
261
- const maxAmount = replayAmount > this._config.maxReserveAmountUsdc
262
- ? this._config.maxReserveAmountUsdc
263
- : replayAmount;
264
- const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
265
- const reserveMsg = {
266
- channelId: session.sessionId,
267
- maxAmount,
268
- deadline: BigInt(deadline),
269
- };
270
- const reserveAuthSig = await signReserveAuth(this._signer, this._channelsDomain, reserveMsg);
271
- paymentMux.sendSpendingAuth({
272
- channelId: session.sessionId,
273
- cumulativeAmount: session.authMax,
274
- metadataHash: ZERO_METADATA_HASH,
275
- metadata: encodeMetadata(this._sanitizeMetadata(this._metadata.get(sellerPeerId))),
276
- spendingAuthSig: reserveAuthSig,
277
- reserveSalt: salt,
278
- reserveMaxAmount: maxAmount.toString(),
279
- reserveDeadline: deadline,
280
- });
281
- return session.sessionId;
282
- }
283
- _reopenOverdraftWindowIfCollapsed(sellerPeerId, currentCumulative, maxSignable, context) {
284
- // Overdraft-window unblock: when the buyer has already signed up to
285
- // `verified + maxPerRequest` and the seller returned 402 because `spent`
286
- // caught up, advance `verifiedCost` to the current signed cumulative so
287
- // the window reopens. The buyer has already committed to pay
288
- // `currentCumulative` via the signed SpendingAuth, so the seller can
289
- // already claim that much on-chain — advancing verified to match doesn't
290
- // expose new funds, it just reclaims overdraft headroom.
291
- //
292
- // SECURITY: the trust anchor here is `currentCumulative`, NOT any
293
- // seller-supplied target. A malicious seller could overstate required
294
- // spend in a 402 / NeedAuth path in an attempt to drain the reserve. We
295
- // defend by only advancing verifiedCost to the amount the buyer has
296
- // already signed. Seller-provided values remain destination hints bounded
297
- // by `verifiedCost + maxPerRequestUsdc` and are never used to mint new
298
- // trust directly.
299
- if (maxSignable > currentCumulative)
300
- return maxSignable;
301
- const previousVerified = this._verifiedCost.get(sellerPeerId) ?? 0n;
302
- if (currentCumulative <= previousVerified)
303
- return maxSignable;
304
- this._verifiedCost.set(sellerPeerId, currentCumulative);
305
- const reopened = this._maxSignable(sellerPeerId);
306
- debugLog(`[BuyerPayment] ${context}: advanced verifiedCost ${previousVerified} → ${currentCumulative} ` +
307
- `to unblock overdraft window for ${sellerPeerId.slice(0, 12)}...`);
308
- return reopened;
309
- }
310
- _needsCeilingAdvance(requestedAmount, maxSignable, ceiling) {
311
- return requestedAmount > maxSignable && maxSignable >= ceiling;
312
- }
313
- /**
314
- * Both signPerRequestAuth (buyer-initiated) and handleNeedAuth (seller-initiated)
315
- * can fire for the same request when the proactive auth doesn't fully cover the
316
- * seller's required cumulative. Whichever path counts a request's tokens first
317
- * records its requestId here so the other path attributes only the amount delta.
318
- */
319
- _serviceTokensCounted = new CountedRequestTracker();
320
- _spendListener = null;
321
- /**
322
- * Observe per-request spend as it is signed. Used to attribute cost to the
323
- * caller's own unit of work (a tool conversation, say), which only the layer
324
- * that issued the request can know.
325
- */
326
- setSpendListener(listener) {
327
- this._spendListener = listener;
328
- }
329
- _reportSpend(event) {
330
- if (!this._spendListener)
331
- return;
332
- try {
333
- this._spendListener(event);
334
- }
335
- catch (err) {
336
- // Accounting is a bystander here — never let it break the payment path.
337
- debugWarn(`[BuyerPayment] spend listener threw: ${err instanceof Error ? err.message : err}`);
338
- }
339
- }
340
- _persistServiceMetadata(sessionId, metadata) {
341
- this._channelStore.replaceMetadataServiceTotals(sessionId, this._sanitizeMetadata(metadata).services);
342
- }
343
- async _sendUpdatedSpendingAuth(session, sellerPeerId, cumulativeAmount, metadata, paymentMux) {
344
- const sanitizedMetadata = this._sanitizeMetadata(metadata);
345
- const metadataHashHex = computeMetadataHash(sanitizedMetadata);
346
- const encodedMetadata = encodeMetadata(sanitizedMetadata);
347
- const metadataMsg = {
348
- channelId: session.sessionId,
349
- cumulativeAmount,
350
- metadataHash: metadataHashHex,
351
- };
352
- const spendingAuthSig = await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg);
353
- this._cumulativeAmount.set(sellerPeerId, cumulativeAmount);
354
- this._persistServiceMetadata(session.sessionId, sanitizedMetadata);
355
- this._channelStore.upsertChannel({
356
- ...session,
357
- authMax: cumulativeAmount.toString(),
358
- updatedAt: Date.now(),
359
- });
360
- paymentMux.sendSpendingAuth({
361
- channelId: session.sessionId,
362
- cumulativeAmount: cumulativeAmount.toString(),
363
- metadataHash: metadataHashHex,
364
- metadata: encodedMetadata,
365
- spendingAuthSig,
366
- });
367
- }
368
- async _topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, context) {
369
- try {
370
- await this.topUpReserve(sellerPeerId, paymentMux);
371
- }
372
- catch (err) {
373
- debugWarn(`[BuyerPayment] ${context}: topUpReserve failed: ${err instanceof Error ? err.message : err}`);
374
- }
375
- }
376
- // ── Spending Authorization ────────────────────────────────────
377
- /**
378
- * Sign and send an initial EIP-712 SpendingAuth to a seller.
379
- * The initial cumulativeAmount is set to the seller's minBudgetPerRequest.
380
- *
381
- * @param pricing Token pricing from the seller's 402 / peer metadata.
382
- * @param pricingMap Full pricing map (defaults + per-service) from peer metadata. If provided,
383
- * merges with existing session pricing. The `pricing` arg is used as the service-specific
384
- * override for the service that triggered the 402.
385
- */
386
- async authorizeSpending(sellerPeerId, paymentMux, minBudgetPerRequest, reserveAmountOrPricing, pricingArg, pricingMap, metadataArg) {
387
- const sellerEvmAddr = this._sellerAddressResolver
388
- ? await this._sellerAddressResolver.resolveSellerAddress(sellerPeerId, metadataArg)
389
- : peerIdToAddress(sellerPeerId);
390
- const reserveAmount = typeof reserveAmountOrPricing === 'bigint'
391
- ? reserveAmountOrPricing
392
- : this._config.maxReserveAmountUsdc;
393
- const pricing = typeof reserveAmountOrPricing === 'bigint'
394
- ? pricingArg
395
- : reserveAmountOrPricing;
396
- // Budget validation: reject if seller demands more than buyer's overdraft limit
397
- if (minBudgetPerRequest > this._config.maxPerRequestUsdc) {
398
- debugWarn(`[BuyerPayment] Seller ${sellerPeerId.slice(0, 12)}... minBudgetPerRequest=${minBudgetPerRequest} exceeds maxPerRequestUsdc=${this._config.maxPerRequestUsdc} — not authorizing`);
399
- return '';
400
- }
401
- // Clear confirmation state so we wait for a fresh AuthAck on the new session
402
- this._confirmedPeers.delete(sellerPeerId);
403
- // Store full pricing map (defaults + all per-service overrides).
404
- // Merge 402-negotiated pricing on top of peer-metadata defaults so
405
- // the seller's live rate takes precedence over cached peer metadata.
406
- if (pricingMap) {
407
- const mergedDefaults = pricing
408
- ? { ...pricingMap.defaults, ...pricing }
409
- : pricingMap.defaults;
410
- this._sessionPricing.set(sellerPeerId, { defaults: mergedDefaults, services: pricingMap.services });
411
- }
412
- else if (pricing) {
413
- // Legacy: single pricing, store as defaults
414
- const existing = this._sessionPricing.get(sellerPeerId);
415
- this._sessionPricing.set(sellerPeerId, {
416
- defaults: pricing,
417
- services: existing?.services ?? {},
418
- });
419
- }
420
- // Generate random salt and compute deterministic channelId
421
- const salt = '0x' + randomBytes(32).toString('hex');
422
- const buyerEvmAddr = this._identity.wallet.address;
423
- const channelId = computeChannelId(buyerEvmAddr, sellerEvmAddr, salt);
424
- const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
425
- debugLog(`[BuyerPayment] authorizeSpending: channel=${channelId.slice(0, 18)}... seller=${sellerPeerId.slice(0, 12)}... amount=${minBudgetPerRequest}`);
426
- // Sign ReserveAuth — binds channelId, maxAmount, deadline on-chain
427
- const channelsDomain = this._channelsDomain;
428
- const maxAmount = reserveAmount;
429
- const reserveMsg = {
430
- channelId,
431
- maxAmount,
432
- deadline: BigInt(deadline),
433
- };
434
- const reserveAuthSig = await signReserveAuth(this._signer, channelsDomain, reserveMsg);
435
- // Initialize state for this session.
436
- // Start cumulative at 0 — the initial message is a ReserveAuth (not a SpendingAuth).
437
- // The first real SpendingAuth will be sent by handleNeedAuth after the seller serves
438
- // the first request and reports its cost.
439
- this._cumulativeAmount.set(sellerPeerId, 0n);
440
- this._metadata.set(sellerPeerId, this._sanitizeMetadata({ ...ZERO_METADATA }));
441
- this._verifiedCost.set(sellerPeerId, 0n);
442
- this._currentReserveCeiling.set(sellerPeerId, maxAmount);
443
- this._initialReserveAmount.set(sellerPeerId, maxAmount);
444
- this._reserveSalt.set(sellerPeerId, salt);
445
- // Store session
446
- const now = Date.now();
447
- const session = {
448
- sessionId: channelId,
449
- peerId: sellerPeerId,
450
- role: CHANNEL_ROLE.BUYER,
451
- sellerEvmAddr,
452
- buyerEvmAddr: this._identity.wallet.address,
453
- nonce: 0,
454
- authMax: '0',
455
- deadline,
456
- previousSessionId: '0x' + '0'.repeat(64),
457
- previousConsumption: '0',
458
- tokensDelivered: '0',
459
- requestCount: 0,
460
- reservedAt: now,
461
- settledAt: null,
462
- settledAmount: null,
463
- status: CHANNEL_STATUS.ACTIVE,
464
- latestBuyerSig: null,
465
- latestSpendingAuthSig: null,
466
- latestMetadata: null,
467
- createdAt: now,
468
- updatedAt: now,
469
- };
470
- this._channelStore.upsertChannel(session);
471
- // Send SpendingAuth via PaymentMux — reserve carries ReserveAuth sig
472
- paymentMux.sendSpendingAuth({
473
- channelId,
474
- cumulativeAmount: '0',
475
- metadataHash: ZERO_METADATA_HASH,
476
- metadata: encodeMetadata(ZERO_METADATA),
477
- spendingAuthSig: reserveAuthSig,
478
- reserveSalt: salt,
479
- reserveMaxAmount: maxAmount.toString(),
480
- reserveDeadline: deadline,
481
- });
482
- return channelId;
483
- }
484
- // ── AuthAck handler ───────────────────────────────────────────
485
- handleAuthAck(sellerPeerId, payload) {
486
- const session = this.getActiveSession(sellerPeerId);
487
- if (!session) {
488
- debugWarn(`[BuyerPayment] AuthAck for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
489
- return;
490
- }
491
- if (session.sessionId !== payload.channelId) {
492
- debugWarn(`[BuyerPayment] AuthAck channel mismatch: expected=${session.sessionId.slice(0, 18)}... got=${payload.channelId.slice(0, 18)}...`);
493
- return;
494
- }
495
- this._confirmedPeers.add(sellerPeerId);
496
- debugLog(`[BuyerPayment] AuthAck confirmed: channel=${session.sessionId.slice(0, 18)}...`);
497
- }
498
- // ── Buyer-side cost verification ──────────────────────────────
499
- /**
500
- * Estimate tokens and cost from response content without updating state.
501
- */
502
- _estimateResponseCost(sellerPeerId, inputBytes, outputBytes, service) {
503
- const pricing = this.getSessionPricing(sellerPeerId, service);
504
- if (!pricing)
505
- return null;
506
- return estimateCostFromBytes(inputBytes, outputBytes, pricing);
507
- }
508
- /**
509
- * Accumulate a cost estimate into verifiedCost.
510
- */
511
- _accumulateVerifiedCost(sellerPeerId, estimate) {
512
- const prev = this._verifiedCost.get(sellerPeerId) ?? 0n;
513
- const newVerified = prev + estimate.cost;
514
- this._verifiedCost.set(sellerPeerId, newVerified);
515
- return newVerified;
516
- }
517
- /**
518
- * Record response content and update the buyer's verified cost.
519
- * Call this after receiving each response from the seller.
520
- *
521
- * NOTE: Do not call this AND signPerRequestAuth for the same response —
522
- * signPerRequestAuth already updates verifiedCost internally.
523
- *
524
- * @returns The updated verified cost and estimated tokens, or null if no pricing is available.
525
- */
526
- recordResponseBytes(sellerPeerId, inputBytes, outputBytes) {
527
- const estimate = this._estimateResponseCost(sellerPeerId, inputBytes, outputBytes);
528
- if (!estimate)
529
- return null;
530
- const newVerified = this._accumulateVerifiedCost(sellerPeerId, estimate);
531
- const inSize = inputBytes.length;
532
- const outSize = outputBytes.length;
533
- debugLog(`[BuyerPayment] recordResponseBytes: seller=${sellerPeerId.slice(0, 12)}... ` +
534
- `in=${inSize}B→${estimate.inputTokens}tok out=${outSize}B→${estimate.outputTokens}tok ` +
535
- `requestCost=${estimate.cost} verifiedCost=${newVerified}`);
536
- return { verifiedCost: newVerified, inputTokens: estimate.inputTokens, outputTokens: estimate.outputTokens };
537
- }
538
- // ── Per-request authorization (overdraft model) ─────────────
539
- /**
540
- * Compute the max signable cumulative amount based on the overdraft model:
541
- * maxSignable = verifiedCost + maxPerRequestUsdc, capped at reserve ceiling.
542
- */
543
- _maxSignable(sellerPeerId) {
544
- const verified = this._verifiedCost.get(sellerPeerId) ?? 0n;
545
- const ceiling = this._getCeiling(sellerPeerId);
546
- const maxSignable = verified + this._config.maxPerRequestUsdc;
547
- return maxSignable < ceiling ? maxSignable : ceiling;
548
- }
549
- /**
550
- * Check whether the current cumulative amount is approaching the reserve ceiling
551
- * and a top-up should be triggered.
552
- */
553
- _needsTopUp(sellerPeerId) {
554
- const ceiling = this._getCeiling(sellerPeerId);
555
- const current = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
556
- const threshold = BigInt(Math.floor(Number(ceiling) * DEFAULT_TOPUP_THRESHOLD));
557
- return current >= threshold;
558
- }
559
- /**
560
- * Sign an updated SpendingAuth after receiving a response.
561
- *
562
- * The buyer uses the seller's claimed cost to advance the cumulative amount,
563
- * but validates it against the buyer's bytes/4 estimate. If the seller's claim
564
- * exceeds the buyer's estimate by more than the configured tolerance, the buyer
565
- * caps at tolerance * buyerEstimate. The cumulative is also capped at the
566
- * overdraft limit (verifiedCost + maxPerRequestUsdc) and the reserve ceiling.
567
- *
568
- * @param sellerPeerId Seller peer ID.
569
- * @param responseStats Byte counts from the last response and seller's claimed cost.
570
- * @returns The signed payload and whether a reserve top-up is needed.
571
- */
572
- async signPerRequestAuth(sellerPeerId, responseStats) {
573
- const session = this.getActiveSession(sellerPeerId);
574
- if (!session) {
575
- throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}... — call authorizeSpending() first`);
576
- }
577
- // Prefer reported token counts (from seller headers or buyer's parsed response usage)
578
- // over byte-based estimation. Byte estimation inflates tokens due to JSON/SSE overhead.
579
- // Also treat seller-claimed cost of 0 as authoritative — don't fall back to byte estimation.
580
- const hasReportedTokens = (responseStats.reportedInputTokens != null && responseStats.reportedInputTokens > 0n) ||
581
- (responseStats.reportedOutputTokens != null && responseStats.reportedOutputTokens > 0n) ||
582
- (responseStats.sellerClaimedCost != null && responseStats.sellerClaimedCost === 0n);
583
- let estimatedInputTokens;
584
- let estimatedOutputTokens;
585
- let estimatedCachedInputTokens = 0n;
586
- let buyerEstimatedRequestCost;
587
- if (hasReportedTokens) {
588
- estimatedInputTokens = responseStats.reportedInputTokens ?? 0n;
589
- estimatedOutputTokens = responseStats.reportedOutputTokens ?? 0n;
590
- const cachedInputTokens = responseStats.reportedCachedInputTokens ?? 0n;
591
- estimatedCachedInputTokens = cachedInputTokens;
592
- // For cost estimation, reportedInputTokens is normalized to total logical
593
- // input tokens (fresh + cached), so split fresh by subtracting cached.
594
- const freshInputTokens = cachedInputTokens > 0n
595
- ? BigInt(Math.max(0, Number(estimatedInputTokens) - Number(cachedInputTokens)))
596
- : estimatedInputTokens;
597
- // Compute cost from reported tokens using service-specific pricing
598
- const pricing = this.getSessionPricing(sellerPeerId, responseStats.service);
599
- if (pricing) {
600
- const cost = computeCostUsdc(Number(freshInputTokens), Number(estimatedOutputTokens), pricing, Number(cachedInputTokens));
601
- buyerEstimatedRequestCost = cost;
602
- this._accumulateVerifiedCost(sellerPeerId, { cost, inputTokens: Number(estimatedInputTokens), outputTokens: Number(estimatedOutputTokens) });
603
- }
604
- else if (responseStats.sellerClaimedCost != null && responseStats.sellerClaimedCost > 0n) {
605
- // No local pricing available (e.g. after buyer restart before session pricing is restored).
606
- // Use seller's claimed cost for verifiedCost accumulation so _maxSignable can grow.
607
- // The seller's claim is still validated against tolerance below.
608
- buyerEstimatedRequestCost = responseStats.sellerClaimedCost;
609
- this._accumulateVerifiedCost(sellerPeerId, { cost: responseStats.sellerClaimedCost, inputTokens: Number(estimatedInputTokens), outputTokens: Number(estimatedOutputTokens) });
610
- }
611
- else {
612
- buyerEstimatedRequestCost = 0n;
613
- }
614
- debugLog(`[BuyerPayment] Using reported tokens: in=${estimatedInputTokens} cached=${cachedInputTokens} out=${estimatedOutputTokens} cost=${buyerEstimatedRequestCost}`);
615
- }
616
- else {
617
- // Fall back to byte-based estimation
618
- const estimate = this._estimateResponseCost(sellerPeerId, responseStats.inputBytes, responseStats.outputBytes, responseStats.service);
619
- estimatedInputTokens = estimate ? BigInt(estimate.inputTokens) : 0n;
620
- estimatedOutputTokens = estimate ? BigInt(estimate.outputTokens) : 0n;
621
- buyerEstimatedRequestCost = estimate ? estimate.cost : 0n;
622
- if (estimate) {
623
- this._accumulateVerifiedCost(sellerPeerId, estimate);
624
- }
625
- debugLog(`[BuyerPayment] Byte-estimated tokens (no reported): in=${estimatedInputTokens} out=${estimatedOutputTokens} cost=${buyerEstimatedRequestCost}`);
626
- }
627
- // Determine the accepted cost for this request:
628
- // When the seller reports a cost within tolerance, accept the seller's claim so the
629
- // buyer doesn't underpay due to minor parsing differences. Only cap when the seller's
630
- // claim exceeds the tolerance threshold.
631
- let acceptedCost;
632
- if (responseStats.sellerClaimedCost != null && responseStats.sellerClaimedCost > 0n) {
633
- if (buyerEstimatedRequestCost > 0n) {
634
- const maxAcceptable = BigInt(Math.ceil(Number(buyerEstimatedRequestCost) * this._costTolerance));
635
- if (responseStats.sellerClaimedCost > maxAcceptable) {
636
- debugWarn(`[BuyerPayment] Seller claimed ${responseStats.sellerClaimedCost} exceeds ${this._costTolerance}x buyer estimate ${buyerEstimatedRequestCost} — capping at ${maxAcceptable}`);
637
- acceptedCost = maxAcceptable;
638
- }
639
- else {
640
- // Seller's claim is within tolerance — accept it as-is
641
- acceptedCost = responseStats.sellerClaimedCost;
642
- }
643
- }
644
- else {
645
- // No buyer estimate available — accept seller's claim
646
- acceptedCost = responseStats.sellerClaimedCost;
647
- }
648
- }
649
- else {
650
- acceptedCost = buyerEstimatedRequestCost;
651
- }
652
- // If cost is 0, the cumulative amount stays the same — no spending auth needed
653
- // but we still sign one to keep the seller's session alive.
654
- // Advance cumulative amount by the accepted cost, then add overdraft headroom
655
- // for the next request (so the seller has budget to serve it).
656
- // maxSignable already caps at reserve ceiling, so one cap is sufficient
657
- const prevAmount = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
658
- const maxSignable = this._maxSignable(sellerPeerId);
659
- let newAmount = prevAmount + acceptedCost;
660
- if (newAmount > maxSignable)
661
- newAmount = maxSignable;
662
- this._cumulativeAmount.set(sellerPeerId, newAmount);
663
- const signedDelta = newAmount - prevAmount;
664
- // Update cumulative metadata
665
- // If handleNeedAuth already counted this request (seller-initiated NeedAuth
666
- // raced ahead), skip service attribution entirely: service totals are
667
- // documented as lower bounds, so undercounting beats double-counting.
668
- const alreadyCounted = this._serviceTokensCounted.has(responseStats.requestId);
669
- const newMeta = this._advanceUsageMetadata(this._metadata.get(sellerPeerId), alreadyCounted ? undefined : responseStats.service, {
670
- amount: signedDelta,
671
- inputTokens: estimatedInputTokens,
672
- cachedInputTokens: estimatedCachedInputTokens,
673
- outputTokens: estimatedOutputTokens,
674
- requests: 1n,
675
- });
676
- if (!alreadyCounted)
677
- this._serviceTokensCounted.mark(responseStats.requestId);
678
- this._metadata.set(sellerPeerId, newMeta);
679
- this._persistServiceMetadata(session.sessionId, newMeta);
680
- this._reportSpend({
681
- sellerPeerId,
682
- requestId: responseStats.requestId ?? null,
683
- amountUsdc: signedDelta.toString(),
684
- inputTokens: (alreadyCounted ? 0n : estimatedInputTokens).toString(),
685
- cachedInputTokens: (alreadyCounted ? 0n : estimatedCachedInputTokens).toString(),
686
- outputTokens: (alreadyCounted ? 0n : estimatedOutputTokens).toString(),
687
- });
688
- debugLog(`[BuyerPayment] signPerRequestAuth #${newMeta.cumulativeRequestCount}: ` +
689
- `thisReq in=${estimatedInputTokens} out=${estimatedOutputTokens} | ` +
690
- `cumulative in=${newMeta.cumulativeInputTokens} out=${newMeta.cumulativeOutputTokens} | ` +
691
- `acceptedCost=${acceptedCost} cumulativeAmount=${newAmount}`);
692
- // Compute metadata hash and encode metadata
693
- const metadataHashHex = computeMetadataHash(newMeta);
694
- const encodedMetadata = encodeMetadata(newMeta);
695
- // Sign EIP-712 SpendingAuth
696
- const channelsDomain = this._channelsDomain;
697
- const metadataMsg = {
698
- channelId: session.sessionId,
699
- cumulativeAmount: newAmount,
700
- metadataHash: metadataHashHex,
701
- };
702
- const spendingAuthSig = await signSpendingAuth(this._signer, channelsDomain, metadataMsg);
703
- // Persist updated cumulative values to ChannelStore
704
- this._channelStore.upsertChannel({
705
- ...session,
706
- authMax: newAmount.toString(),
707
- requestCount: Number(newMeta.cumulativeRequestCount),
708
- updatedAt: Date.now(),
709
- });
710
- const payload = {
711
- channelId: session.sessionId,
712
- cumulativeAmount: newAmount.toString(),
713
- metadataHash: metadataHashHex,
714
- metadata: encodedMetadata,
715
- spendingAuthSig,
716
- };
717
- const topUpNeeded = this._needsTopUp(sellerPeerId);
718
- return { payload, topUpNeeded };
719
- }
720
- // ── NeedAuth handler ───────────────────────────────────────────
721
- /**
722
- * Handle seller-initiated NeedAuth messages sent after every served request.
723
- * The seller includes the cost of the last request; the buyer validates it
724
- * against its own token count and signs a new SpendingAuth for the required
725
- * cumulative amount, capped at the reserve ceiling.
726
- */
727
- async handleNeedAuth(sellerPeerId, payload, paymentMux) {
728
- const session = this.getActiveSession(sellerPeerId);
729
- if (!session) {
730
- debugWarn(`[BuyerPayment] NeedAuth for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
731
- return;
732
- }
733
- const buyerService = this._requestService.get(payload.requestId);
734
- const requiredCumulativeAmount = BigInt(payload.requiredCumulativeAmount);
735
- const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
736
- // Reject stale/lower NeedAuth (monotonicity guard)
737
- if (requiredCumulativeAmount <= currentCumulative) {
738
- debugLog(`[BuyerPayment] NeedAuth stale: required=${requiredCumulativeAmount} <= current=${currentCumulative} — ignoring`);
739
- return;
740
- }
741
- this._requestService.take(payload.requestId);
742
- let acceptedServiceCost = 0n;
743
- const reportedInputTokens = BigInt(payload.inputTokens ?? '0');
744
- const reportedCachedInputTokens = BigInt(payload.cachedInputTokens ?? '0');
745
- const reportedOutputTokens = BigInt(payload.outputTokens ?? '0');
746
- // Validate the seller's claimed cost if reported
747
- if (payload.lastRequestCost) {
748
- const sellerCost = BigInt(payload.lastRequestCost);
749
- const sellerIn = reportedInputTokens;
750
- const sellerOut = reportedOutputTokens;
751
- const sellerCached = reportedCachedInputTokens;
752
- acceptedServiceCost = sellerCost;
753
- // Prefer seller-supplied freshInputTokens to avoid OpenAI-vs-Anthropic
754
- // cached-semantics ambiguity (OpenAI: prompt_tokens includes cached;
755
- // Anthropic: input_tokens excludes cached). Fall back to OpenAI-style
756
- // subtraction for older sellers that don't emit the field.
757
- const sellerFreshExplicit = payload.freshInputTokens != null
758
- ? BigInt(payload.freshInputTokens)
759
- : null;
760
- // Use the buyer's own knowledge of which service it requested, not the seller's claim.
761
- // A malicious seller could set service to a more expensive model to inflate the ceiling.
762
- const pricing = this.getSessionPricing(sellerPeerId, buyerService);
763
- if (pricing && sellerCost > 0n) {
764
- const freshIn = sellerFreshExplicit ?? (sellerCached > 0n
765
- ? BigInt(Math.max(0, Number(sellerIn) - Number(sellerCached)))
766
- : sellerIn);
767
- const buyerEstimate = computeCostUsdc(Number(freshIn), Number(sellerOut), pricing, Number(sellerCached));
768
- const maxAcceptable = BigInt(Math.ceil(Number(buyerEstimate) * this._costTolerance));
769
- if (buyerEstimate > 0n && sellerCost > maxAcceptable) {
770
- debugWarn(`[BuyerPayment] NeedAuth: seller claimed cost ${sellerCost} exceeds ${this._costTolerance}x buyer estimate ${buyerEstimate} — rejecting`);
771
- return;
772
- }
773
- debugLog(`[BuyerPayment] NeedAuth: seller cost=${sellerCost} buyer estimate=${buyerEstimate} (in=${sellerIn} cached=${sellerCached} out=${sellerOut}) — validated`);
774
- }
775
- else {
776
- debugLog(`[BuyerPayment] NeedAuth: seller cost=${sellerCost} (no local pricing, accepting)`);
777
- }
778
- // Accumulate verified cost from the seller's report
779
- this._accumulateVerifiedCost(sellerPeerId, {
780
- cost: sellerCost,
781
- inputTokens: Number(sellerIn),
782
- outputTokens: Number(sellerOut),
783
- });
784
- }
785
- // Cap at overdraft limit: verifiedCost + maxPerRequestUsdc, then at reserve ceiling.
786
- // This prevents a malicious seller from claiming a small cost but requesting the full reserve.
787
- let maxSignable = this._maxSignable(sellerPeerId);
788
- const ceiling = this._getCeiling(sellerPeerId);
789
- let needsTopUp = this._needsCeilingAdvance(requiredCumulativeAmount, maxSignable, ceiling);
790
- if (maxSignable <= currentCumulative && !needsTopUp) {
791
- maxSignable = this._reopenOverdraftWindowIfCollapsed(sellerPeerId, currentCumulative, maxSignable, 'handleNeedAuth');
792
- needsTopUp = this._needsCeilingAdvance(requiredCumulativeAmount, maxSignable, ceiling);
793
- if (maxSignable <= currentCumulative && !needsTopUp) {
794
- debugWarn(`[BuyerPayment] NeedAuth: maxSignable=${maxSignable} <= currentCumulative=${currentCumulative} — overdraft limit reached`);
795
- return;
796
- }
797
- }
798
- // When a topUp is needed, first sign at the current ceiling so the seller
799
- // has a high-enough settled amount to pass the on-chain TopUpThresholdNotMet
800
- // check (contract requires 85% of deposit to be settleable before topUp).
801
- // We cap at the old ceiling here; the topUp is sent AFTER so the seller
802
- // processes the SpendingAuth first, then the topUp with adequate settle amount.
803
- const effectiveAmount = needsTopUp
804
- ? (maxSignable > currentCumulative ? maxSignable : currentCumulative)
805
- : (requiredCumulativeAmount < maxSignable ? requiredCumulativeAmount : maxSignable);
806
- if (effectiveAmount <= currentCumulative) {
807
- // Nothing to sign — trigger topUp anyway to extend ceiling for next round
808
- if (needsTopUp) {
809
- await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'handleNeedAuth');
810
- }
811
- debugWarn(`[BuyerPayment] NeedAuth: effectiveAmount=${effectiveAmount} <= currentCumulative=${currentCumulative} — cannot advance`);
812
- return;
813
- }
814
- debugLog(`[BuyerPayment] NeedAuth: channel=${session.sessionId.slice(0, 18)}... required=${requiredCumulativeAmount} effective=${effectiveAmount}`);
815
- // Advance cumulative metadata. requestCount always increments so the
816
- // on-chain metadata stays consistent even when older sellers omit token
817
- // fields; absent token fields contribute 0 to the running totals.
818
- const signedDelta = effectiveAmount - currentCumulative;
819
- const serviceAmountDelta = acceptedServiceCost > 0n
820
- ? (acceptedServiceCost < signedDelta ? acceptedServiceCost : signedDelta)
821
- : 0n;
822
- // If signPerRequestAuth already counted this request (buyer-initiated auth
823
- // raced ahead but didn't fully cover the seller's required cumulative), skip
824
- // service attribution entirely: service totals are documented as lower
825
- // bounds, so undercounting beats double-counting.
826
- const alreadyCounted = this._serviceTokensCounted.has(payload.requestId);
827
- const newMeta = this._advanceUsageMetadata(this._metadata.get(sellerPeerId), alreadyCounted ? undefined : buyerService, {
828
- amount: serviceAmountDelta,
829
- inputTokens: reportedInputTokens,
830
- cachedInputTokens: reportedCachedInputTokens,
831
- outputTokens: reportedOutputTokens,
832
- requests: 1n,
833
- });
834
- if (!alreadyCounted)
835
- this._serviceTokensCounted.mark(payload.requestId);
836
- this._metadata.set(sellerPeerId, newMeta);
837
- // Send via PaymentMux
838
- try {
839
- await this._sendUpdatedSpendingAuth(session, sellerPeerId, effectiveAmount, newMeta, paymentMux);
840
- this._reportSpend({
841
- sellerPeerId,
842
- requestId: payload.requestId ?? null,
843
- amountUsdc: signedDelta.toString(),
844
- inputTokens: (alreadyCounted ? 0n : reportedInputTokens).toString(),
845
- cachedInputTokens: (alreadyCounted ? 0n : reportedCachedInputTokens).toString(),
846
- outputTokens: (alreadyCounted ? 0n : reportedOutputTokens).toString(),
847
- });
848
- debugLog(`[BuyerPayment] NeedAuth responded: new cumulativeAmount=${effectiveAmount}`);
849
- }
850
- catch {
851
- debugLog(`[BuyerPayment] NeedAuth: connection closed before SpendingAuth could be sent`);
852
- }
853
- // Send topUp AFTER the SpendingAuth so the seller processes the higher
854
- // cumulative first — this ensures the on-chain settle amount meets the
855
- // contract's TopUpThresholdNotMet requirement (85% of deposit must be
856
- // settleable before topUp is allowed). Also proactively send the top-up
857
- // once the signed cumulative reaches the buyer's 65% threshold; the seller
858
- // may defer the on-chain topUp until the contract's 85% gate is satisfied.
859
- if (needsTopUp || this._needsTopUp(sellerPeerId)) {
860
- await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'handleNeedAuth');
861
- }
862
- }
863
- // ── Reserve top-up ─────────────────────────────────────────────
864
- /**
865
- * Sign a new ReserveAuth with a higher maxAmount to extend the session's reserve ceiling.
866
- * The seller must call reserve() on-chain again with the new signature.
867
- * Note: requires contract support for top-up (increaseDeposit on existing channelId).
868
- */
869
- async topUpReserve(sellerPeerId, paymentMux) {
870
- const session = this.getActiveSession(sellerPeerId);
871
- if (!session) {
872
- debugWarn(`[BuyerPayment] topUpReserve: no active session for ${sellerPeerId.slice(0, 12)}...`);
873
- return;
874
- }
875
- const prevCeiling = this._getCeiling(sellerPeerId);
876
- const newCeiling = prevCeiling + this._config.maxReserveAmountUsdc;
877
- const additionalReserve = newCeiling - prevCeiling;
878
- const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
879
- try {
880
- const balance = await this.getBalance();
881
- if (balance.available < additionalReserve) {
882
- throw new Error(`Insufficient buyer deposits for reserve top-up: available=${balance.available} required=${additionalReserve}`);
883
- }
884
- }
885
- catch (err) {
886
- if (err instanceof Error && err.message.startsWith('Insufficient buyer deposits')) {
887
- throw err;
888
- }
889
- debugWarn(`[BuyerPayment] topUpReserve: unable to verify buyer deposits before signing top-up: ` +
890
- `${err instanceof Error ? err.message : err}`);
891
- }
892
- debugLog(`[BuyerPayment] topUpReserve: channel=${session.sessionId.slice(0, 18)}... ceiling ${prevCeiling} → ${newCeiling}`);
893
- // Sign ReserveAuth with new maxAmount
894
- const channelsDomain = this._channelsDomain;
895
- const reserveMsg = {
896
- channelId: session.sessionId,
897
- maxAmount: newCeiling,
898
- deadline: BigInt(deadline),
899
- };
900
- const reserveAuthSig = await signReserveAuth(this._signer, channelsDomain, reserveMsg);
901
- const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
902
- const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
903
- const metadataHashHex = computeMetadataHash(currentMeta);
904
- const encodedMetadata = encodeMetadata(currentMeta);
905
- const salt = this._reserveSalt.get(sellerPeerId) ?? '0x' + '00'.repeat(32);
906
- // Send ReserveAuth sig with reserve fields (same pattern as initial authorizeSpending).
907
- // The seller uses this to call topUp() on-chain with the new maxAmount.
908
- try {
909
- paymentMux.sendSpendingAuth({
910
- channelId: session.sessionId,
911
- cumulativeAmount: currentCumulative.toString(),
912
- metadataHash: metadataHashHex,
913
- metadata: encodedMetadata,
914
- spendingAuthSig: reserveAuthSig,
915
- reserveSalt: salt,
916
- reserveMaxAmount: newCeiling.toString(),
917
- reserveDeadline: deadline,
918
- });
919
- // Only commit the new ceiling after the message is delivered
920
- this._currentReserveCeiling.set(sellerPeerId, newCeiling);
921
- debugLog(`[BuyerPayment] topUpReserve sent: newCeiling=${newCeiling}`);
922
- }
923
- catch {
924
- debugLog(`[BuyerPayment] topUpReserve: connection closed before ReserveAuth could be sent`);
925
- }
926
- }
927
- // ── Queries ───────────────────────────────────────────────────
928
- /** Max USDC overdraft (unverified exposure) from buyer config. */
929
- get maxPerRequestUsdc() {
930
- return this._config.maxPerRequestUsdc;
931
- }
932
- /** Max USDC per ReserveAuth signature from buyer config. */
933
- get maxReserveAmountUsdc() {
934
- return this._config.maxReserveAmountUsdc;
935
- }
936
- /** Current buyer-verified cost for a seller. */
937
- getVerifiedCost(sellerPeerId) {
938
- return this._verifiedCost.get(sellerPeerId) ?? 0n;
939
- }
940
- /** Current reserve ceiling for a seller (may be higher than initial after top-ups). */
941
- getReserveCeiling(sellerPeerId) {
942
- return this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
943
- }
944
- /** Current cumulative signed amount for a seller. */
945
- getCumulativeAmount(sellerPeerId) {
946
- return this._cumulativeAmount.get(sellerPeerId) ?? 0n;
947
- }
948
- /** Live cumulative token counts for a seller (in-memory, always up-to-date). */
949
- getCumulativeTokens(sellerPeerId) {
950
- const meta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
951
- return { inputTokens: meta.cumulativeInputTokens, outputTokens: meta.cumulativeOutputTokens };
952
- }
953
- /**
954
- * Accumulate response token counts and persist to the channel store.
955
- * Tracks its own running totals independently of signPerRequestAuth metadata,
956
- * so the persisted data is always up-to-date after each response.
957
- */
958
- recordAndPersistTokens(sellerPeerId, inputTokens, outputTokens) {
959
- const session = this.getActiveSession(sellerPeerId);
960
- if (!session)
961
- return;
962
- const prev = this._responseTokenTotals.get(sellerPeerId) ?? {
963
- input: Number(session.tokensDelivered),
964
- output: Number(session.previousConsumption),
965
- requests: session.requestCount,
966
- };
967
- debugLog(`[BuyerPayment] recordTokens: thisReq in=${inputTokens} out=${outputTokens} | ` +
968
- `prevTotal in=${prev.input} out=${prev.output} reqs=${prev.requests}`);
969
- const totals = {
970
- input: prev.input + inputTokens,
971
- output: prev.output + outputTokens,
972
- requests: prev.requests + 1,
973
- };
974
- this._responseTokenTotals.set(sellerPeerId, totals);
975
- this._channelStore.upsertChannel({
976
- ...session,
977
- tokensDelivered: String(totals.input),
978
- previousConsumption: String(totals.output),
979
- requestCount: totals.requests,
980
- updatedAt: Date.now(),
981
- });
982
- }
983
- /** Register which service the buyer requested for a given requestId. */
984
- trackRequestService(requestId, service) {
985
- this._requestService.track(requestId, service);
986
- }
987
- /** Get the live response token totals for a seller, or null if none recorded this session. */
988
- getResponseTokenTotals(sellerPeerId) {
989
- return this._responseTokenTotals.get(sellerPeerId) ?? null;
990
- }
991
- /** Get the session pricing for a seller+service. Resolves service-specific pricing first, then defaults. */
992
- getSessionPricing(sellerPeerId, service) {
993
- const map = this._sessionPricing.get(sellerPeerId);
994
- if (!map)
995
- return null;
996
- if (service) {
997
- const servicePricing = map.services[service];
998
- if (servicePricing)
999
- return servicePricing;
1000
- }
1001
- return map.defaults;
1002
- }
1003
- /** Check if a session has been confirmed via AuthAck. */
1004
- isAuthorized(sellerPeerId) {
1005
- return this._confirmedPeers.has(sellerPeerId);
1006
- }
1007
- /** Alias for isAuthorized (used by polling loop). */
1008
- isLockConfirmed(sellerPeerId) {
1009
- return this.isAuthorized(sellerPeerId);
1010
- }
1011
- /** Check if the lock was explicitly rejected (not just never-contacted). */
1012
- isLockRejected(sellerPeerId) {
1013
- return this._rejectedPeers.has(sellerPeerId);
1014
- }
1015
- /** Mark a peer as having rejected our spending auth. */
1016
- markRejected(sellerPeerId) {
1017
- this._rejectedPeers.add(sellerPeerId);
1018
- debugLog(`[BuyerPayment] Peer ${sellerPeerId.slice(0, 12)}... marked as rejected`);
1019
- }
1020
- getSessionHistory(sellerPeerId) {
1021
- const session = this._channelStore.getLatestChannelByPeerAndBuyer(sellerPeerId, 'buyer', this._identity.wallet.address);
1022
- return session ? [session] : [];
1023
- }
1024
- // ── Deposit operations ──────────────────────────────────────────
1025
- async deposit(amount) {
1026
- debugLog(`[BuyerPayment] Depositing ${amount} to deposits`);
1027
- const buyer = this._identity.wallet.address;
1028
- return this._depositsClient.deposit(this._signer, buyer, amount);
1029
- }
1030
- async withdraw(amount) {
1031
- debugLog(`[BuyerPayment] Withdrawing ${amount} from deposits`);
1032
- return this._depositsClient.withdraw(this._signer, this._identity.wallet.address, amount);
1033
- }
1034
- async getBalance() {
1035
- const buyerAddr = this._identity.wallet.address;
1036
- const info = await this._depositsClient.getBuyerBalance(buyerAddr);
1037
- return { available: info.available, reserved: info.reserved };
1038
- }
1039
- }
1
+ // Moved to @antseed/buyer-core; re-exported for compatibility.
2
+ export * from '@antseed/buyer-core/buyer-payment-manager';
1040
3
  //# sourceMappingURL=buyer-payment-manager.js.map