@antseed/node 0.2.128 → 0.2.130

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 (77) hide show
  1. package/dist/billing/index.d.ts +1 -1
  2. package/dist/billing/index.d.ts.map +1 -1
  3. package/dist/billing/index.js +1 -1
  4. package/dist/billing/index.js.map +1 -1
  5. package/dist/buyer-identity-context.d.ts +87 -0
  6. package/dist/buyer-identity-context.d.ts.map +1 -0
  7. package/dist/buyer-identity-context.js +226 -0
  8. package/dist/buyer-identity-context.js.map +1 -0
  9. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  10. package/dist/discovery/metadata-codec.js +95 -4
  11. package/dist/discovery/metadata-codec.js.map +1 -1
  12. package/dist/discovery/metadata-validator.d.ts.map +1 -1
  13. package/dist/discovery/metadata-validator.js +6 -4
  14. package/dist/discovery/metadata-validator.js.map +1 -1
  15. package/dist/discovery/peer-metadata.d.ts +1 -1
  16. package/dist/discovery/peer-metadata.d.ts.map +1 -1
  17. package/dist/discovery/peer-metadata.js +1 -1
  18. package/dist/discovery/peer-metadata.js.map +1 -1
  19. package/dist/discovery/service-catalog.d.ts +6 -2
  20. package/dist/discovery/service-catalog.d.ts.map +1 -1
  21. package/dist/discovery/service-catalog.js +13 -8
  22. package/dist/discovery/service-catalog.js.map +1 -1
  23. package/dist/health/model-health-checker.d.ts +1 -1
  24. package/dist/health/model-health-checker.d.ts.map +1 -1
  25. package/dist/health/model-health-checker.js +4 -3
  26. package/dist/health/model-health-checker.js.map +1 -1
  27. package/dist/index.d.ts +5 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/interfaces/seller-provider.d.ts +4 -2
  32. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  33. package/dist/node.d.ts +45 -11
  34. package/dist/node.d.ts.map +1 -1
  35. package/dist/node.js +221 -132
  36. package/dist/node.js.map +1 -1
  37. package/dist/p2p/connection-manager.d.ts.map +1 -1
  38. package/dist/p2p/connection-manager.js +7 -1
  39. package/dist/p2p/connection-manager.js.map +1 -1
  40. package/dist/p2p/outbound-connection.d.ts +14 -0
  41. package/dist/p2p/outbound-connection.d.ts.map +1 -0
  42. package/dist/p2p/outbound-connection.js +88 -0
  43. package/dist/p2p/outbound-connection.js.map +1 -0
  44. package/dist/payments/channel-store.d.ts +4 -0
  45. package/dist/payments/channel-store.d.ts.map +1 -1
  46. package/dist/payments/channel-store.js +53 -6
  47. package/dist/payments/channel-store.js.map +1 -1
  48. package/dist/payments/seller-payment-manager.d.ts +60 -1
  49. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  50. package/dist/payments/seller-payment-manager.js +399 -10
  51. package/dist/payments/seller-payment-manager.js.map +1 -1
  52. package/dist/resources/resource-ownership-store.d.ts +30 -0
  53. package/dist/resources/resource-ownership-store.d.ts.map +1 -0
  54. package/dist/resources/resource-ownership-store.js +72 -0
  55. package/dist/resources/resource-ownership-store.js.map +1 -0
  56. package/dist/seller-request-handler.d.ts +34 -0
  57. package/dist/seller-request-handler.d.ts.map +1 -1
  58. package/dist/seller-request-handler.js +322 -56
  59. package/dist/seller-request-handler.js.map +1 -1
  60. package/dist/storage/migrations/channels/006_add_video_payments.d.ts +3 -0
  61. package/dist/storage/migrations/channels/006_add_video_payments.d.ts.map +1 -0
  62. package/dist/storage/migrations/channels/006_add_video_payments.js +24 -0
  63. package/dist/storage/migrations/channels/006_add_video_payments.js.map +1 -0
  64. package/dist/storage/migrations/channels/index.d.ts.map +1 -1
  65. package/dist/storage/migrations/channels/index.js +2 -1
  66. package/dist/storage/migrations/channels/index.js.map +1 -1
  67. package/dist/storage/migrations/metering/003_create_resource_tables.d.ts +3 -0
  68. package/dist/storage/migrations/metering/003_create_resource_tables.d.ts.map +1 -0
  69. package/dist/storage/migrations/metering/003_create_resource_tables.js +34 -0
  70. package/dist/storage/migrations/metering/003_create_resource_tables.js.map +1 -0
  71. package/dist/storage/migrations/metering/index.d.ts.map +1 -1
  72. package/dist/storage/migrations/metering/index.js +2 -1
  73. package/dist/storage/migrations/metering/index.js.map +1 -1
  74. package/dist/verification/samples.d.ts.map +1 -1
  75. package/dist/verification/samples.js +2 -0
  76. package/dist/verification/samples.js.map +1 -1
  77. package/package.json +4 -4
package/dist/node.js CHANGED
@@ -7,6 +7,7 @@ import { TrustSignalsClient } from './payments/evm/trust-signals-client.js';
7
7
  import { loadOrCreateIdentity } from "./p2p/identity.js";
8
8
  import { peerIdToAddress } from "./types/peer.js";
9
9
  import { MeteringStorage } from "./metering/storage.js";
10
+ import { ResourceOwnershipStore } from "./resources/resource-ownership-store.js";
10
11
  import { ReceiptGenerator } from "./metering/receipt-generator.js";
11
12
  import { SellerSessionTracker, } from "./metering/seller-session-tracker.js";
12
13
  import { ConnectionState } from "./types/connection.js";
@@ -30,6 +31,7 @@ import { FrameDecoder, encodeFrame } from "./p2p/message-protocol.js";
30
31
  import { KeepaliveManager, buildPongPayload } from "./p2p/keepalive.js";
31
32
  import { MessageType } from "./types/protocol.js";
32
33
  import { NatTraversal } from "./p2p/nat-traversal.js";
34
+ import { getOrOpenOutboundConnection } from "./p2p/outbound-connection.js";
33
35
  import { signUtf8 } from "./p2p/identity.js";
34
36
  import { BalanceManager, DepositsClient, ChannelsClient, FreeUsageClient, BuyerFreeUsageManager, SellerFreeUsageManager, SellerFreeTierLimiter, StakingClient, ChannelStore, CHANNEL_KIND, CHANNEL_ROLE, CHANNEL_STATUS, } from "./payments/index.js";
35
37
  import { debugLog, debugWarn } from "./utils/debug.js";
@@ -46,6 +48,7 @@ import { SellerRequestHandler } from "./seller-request-handler.js";
46
48
  import { BuyerRequestHandler, } from "./buyer-request-handler.js";
47
49
  import { buildSybilContext, computeOnChainSybilRisk, } from "./reputation/sybil-risk.js";
48
50
  import { buyerFault } from "./errors.js";
51
+ import { BuyerIdentityContext, DEFAULT_BUYER_IDENTITY, isValidBuyerIdentityName, } from "./buyer-identity-context.js";
49
52
  /** Store the trust score on the peer; leaves the fields untouched when the peer is unscored. */
50
53
  function applyTrust(peer) {
51
54
  const trust = computeTrustScore(peer);
@@ -92,6 +95,7 @@ export class AntseedNode extends EventEmitter {
92
95
  _keepalives = new Map();
93
96
  _nat = null;
94
97
  _metering = null;
98
+ _resourceOwnership = null;
95
99
  _receiptGenerator = null;
96
100
  _balanceManager = null;
97
101
  _depositsClient = null;
@@ -109,6 +113,8 @@ export class AntseedNode extends EventEmitter {
109
113
  _verificationMuxes = new Map();
110
114
  _sweepMuxes = new Map();
111
115
  _peerCapabilities = new Map();
116
+ /** Buyer identities beyond the default one, by name. */
117
+ _buyerIdentities = new Map();
112
118
  /** Seller-side deposit-sweep relayer (initialized when configured + enabled). */
113
119
  _depositRelayer = null;
114
120
  /** Seller-side request handler (provider matching, execution, load tracking). */
@@ -293,6 +299,11 @@ export class AntseedNode extends EventEmitter {
293
299
  if (!this._started) {
294
300
  return;
295
301
  }
302
+ const extraIdentities = [...this._buyerIdentities.values()];
303
+ this._buyerIdentities.clear();
304
+ await Promise.all(extraIdentities.map((context) => context.stop().catch((err) => {
305
+ debugWarn(`[Node] Failed to stop buyer identity "${context.name}": ${err instanceof Error ? err.message : err}`);
306
+ })));
296
307
  // Give in-transit NeedAuth messages time to arrive on the DataChannel,
297
308
  // then wait for their handlers to finish. This ensures the seller has a
298
309
  // valid SpendingAuth for settlement before we close the connection.
@@ -371,6 +382,15 @@ export class AntseedNode extends EventEmitter {
371
382
  }
372
383
  this._metering = null;
373
384
  }
385
+ if (this._resourceOwnership) {
386
+ try {
387
+ this._resourceOwnership.close();
388
+ }
389
+ catch {
390
+ // ignore close errors
391
+ }
392
+ this._resourceOwnership = null;
393
+ }
374
394
  if (this._verificationStorage) {
375
395
  try {
376
396
  this._verificationStorage.close();
@@ -798,7 +818,12 @@ export class AntseedNode extends EventEmitter {
798
818
  * Eagerly open a connection to a peer and wire up the mux.
799
819
  * Subsequent sendRequest / sendRequestStream calls will reuse this connection.
800
820
  */
801
- async connectToPeer(peer) {
821
+ async connectToPeer(peer, buyerIdentity) {
822
+ const context = this._buyerContext(buyerIdentity);
823
+ if (context) {
824
+ await context.connect(peer);
825
+ return;
826
+ }
802
827
  const conn = await this._getOrCreateConnection(peer);
803
828
  this._getOrCreateMux(peer.peerId, conn);
804
829
  const negotiator = this._buyerNegotiator;
@@ -821,7 +846,9 @@ export class AntseedNode extends EventEmitter {
821
846
  * whichever cumulative is higher.
822
847
  */
823
848
  async requestChannelClose(sellerPeerId, opts = {}) {
824
- const negotiator = this._buyerNegotiator;
849
+ const { buyerIdentity, ...closeOptions } = opts;
850
+ const context = this._buyerContext(buyerIdentity);
851
+ const negotiator = context ? context.negotiator : this._buyerNegotiator;
825
852
  if (!negotiator) {
826
853
  throw new Error('Buyer payments are not configured on this node');
827
854
  }
@@ -829,19 +856,7 @@ export class AntseedNode extends EventEmitter {
829
856
  throw new Error('Node not started');
830
857
  }
831
858
  const peerId = sellerPeerId;
832
- let conn = this._connectionManager.getConnection(peerId);
833
- if (!conn || (conn.state !== ConnectionState.Open && conn.state !== ConnectionState.Authenticated)) {
834
- const peer = await this.findPeer(sellerPeerId);
835
- if (!peer) {
836
- throw new Error(`Seller ${sellerPeerId.slice(0, 12)}... is not connected and could not be found on the network. ` +
837
- `A cooperative close needs the seller online — otherwise use the on-chain request-close flow.`);
838
- }
839
- await this.connectToPeer(peer);
840
- conn = this._connectionManager.getConnection(peer.peerId);
841
- if (!conn) {
842
- throw new Error(`Failed to establish a connection to seller ${sellerPeerId.slice(0, 12)}...`);
843
- }
844
- }
859
+ const conn = await this._sellerConnectionForClose(peerId, context);
845
860
  // A seller that predates this feature drops the 0x59 frame silently, so
846
861
  // without this the buyer would wait out the full 60s response timeout and
847
862
  // get a vague "did not answer". Note the capability must come from the
@@ -856,15 +871,41 @@ export class AntseedNode extends EventEmitter {
856
871
  `(missing ${CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1}). ` +
857
872
  `Use the on-chain request-close flow instead.`);
858
873
  }
859
- return negotiator.requestChannelClose(peerId, conn, opts);
874
+ return negotiator.requestChannelClose(peerId, conn, closeOptions);
875
+ }
876
+ /** The paying identity's open connection to a seller, reconnecting when needed. */
877
+ async _sellerConnectionForClose(peerId, context) {
878
+ if (context) {
879
+ const live = context.liveConnection(peerId);
880
+ if (live)
881
+ return live;
882
+ }
883
+ else {
884
+ const live = this._connectionManager.getConnection(peerId);
885
+ if (live && (live.state === ConnectionState.Open || live.state === ConnectionState.Authenticated))
886
+ return live;
887
+ }
888
+ const peer = await this.findPeer(peerId);
889
+ if (!peer) {
890
+ throw new Error(`Seller ${peerId.slice(0, 12)}... is not connected and could not be found on the network. ` +
891
+ `A cooperative close needs the seller online — otherwise use the on-chain request-close flow.`);
892
+ }
893
+ if (context)
894
+ return context.connect(peer);
895
+ await this.connectToPeer(peer);
896
+ const conn = this._connectionManager.getConnection(peer.peerId);
897
+ if (!conn) {
898
+ throw new Error(`Failed to establish a connection to seller ${peerId.slice(0, 12)}...`);
899
+ }
900
+ return conn;
860
901
  }
861
902
  /**
862
903
  * Query session stats for a specific seller peer.
863
904
  * Combines channel store data (authoritative payment/session info) with
864
905
  * metering events when available.
865
906
  */
866
- getMeteringStatsByPeer(sellerPeerId) {
867
- const buyerAddress = this._identity?.wallet.address ?? null;
907
+ getMeteringStatsByPeer(sellerPeerId, buyerIdentity) {
908
+ const { address: buyerAddress, paymentManager } = this._buyerAccount(buyerIdentity);
868
909
  const channel = (buyerAddress != null)
869
910
  ? (this._channelStore?.getActiveChannelByPeerAndBuyer(sellerPeerId, CHANNEL_ROLE.BUYER, buyerAddress)
870
911
  ?? this._channelStore?.getLatestChannelByPeerAndBuyer(sellerPeerId, CHANNEL_ROLE.BUYER, buyerAddress))
@@ -875,7 +916,7 @@ export class AntseedNode extends EventEmitter {
875
916
  ? this._channelStore?.getTotalsByPeerAndBuyer(sellerPeerId, CHANNEL_ROLE.BUYER, buyerAddress)
876
917
  : this._channelStore?.getTotalsByPeer(sellerPeerId, CHANNEL_ROLE.BUYER)
877
918
  ?? null;
878
- const liveTotals = this._buyerPaymentManager?.getResponseTokenTotals(sellerPeerId);
919
+ const liveTotals = paymentManager?.getResponseTokenTotals(sellerPeerId);
879
920
  const inputTokens = (liveTotals != null) ? liveTotals.input
880
921
  : (channel != null) ? Number(channel.tokensDelivered || '0')
881
922
  : 0;
@@ -887,7 +928,7 @@ export class AntseedNode extends EventEmitter {
887
928
  inputTokens,
888
929
  outputTokens,
889
930
  totalTokens: inputTokens + outputTokens,
890
- reservedUsdc: this._buyerPaymentManager?.getReserveCeiling(sellerPeerId)?.toString() ?? null,
931
+ reservedUsdc: paymentManager?.getReserveCeiling(sellerPeerId)?.toString() ?? null,
891
932
  consumedUsdc: channel?.authMax ?? null,
892
933
  channelStatus: channel?.status ?? null,
893
934
  reservedAt: channel?.reservedAt ?? null,
@@ -906,17 +947,20 @@ export class AntseedNode extends EventEmitter {
906
947
  * Combines the persistent ChannelStore (session metadata + cumulative signed
907
948
  * amount) with the in-memory reserve ceiling tracked by BuyerPaymentManager.
908
949
  *
909
- * The ReserveAuth ceiling lives only in the payment manager. When it is not
910
- * available, return null rather than substituting the unrelated cumulative
911
- * SpendingAuth amount stored in authMax.
950
+ * One-off channels use their own persisted confirmed reserve, never the
951
+ * seller's session reserve. Unavailable reserves are null rather than the
952
+ * unrelated cumulative SpendingAuth amount stored in authMax.
912
953
  */
913
- getActiveBuyerChannels() {
914
- const buyerAddress = this._identity?.wallet.address ?? null;
954
+ getActiveBuyerChannels(buyerIdentity) {
955
+ const { address: buyerAddress, paymentManager } = this._buyerAccount(buyerIdentity);
915
956
  if (!buyerAddress || !this._channelStore)
916
957
  return [];
917
- const stored = this._channelStore.getActiveChannelsByBuyer(CHANNEL_ROLE.BUYER, buyerAddress);
958
+ const stored = this._channelStore.getBuyerPaymentChannels(buyerAddress)
959
+ .filter((channel) => channel.status === CHANNEL_STATUS.ACTIVE);
918
960
  return stored.map((c) => {
919
- const liveReserve = this._buyerPaymentManager?.getReserveCeiling(c.peerId);
961
+ const liveReserve = c.channelKind === CHANNEL_KIND.ONE_OFF
962
+ ? BigInt(c.confirmedReserveAmount ?? '0')
963
+ : paymentManager?.getReserveCeiling(c.peerId);
920
964
  return {
921
965
  channelId: c.sessionId,
922
966
  peerId: c.peerId,
@@ -938,14 +982,16 @@ export class AntseedNode extends EventEmitter {
938
982
  });
939
983
  }
940
984
  /** All buyer channels (any local status), used for history views. */
941
- getAllBuyerChannels() {
942
- const buyerAddress = this._identity?.wallet.address ?? null;
985
+ getAllBuyerChannels(buyerIdentity) {
986
+ const { address: buyerAddress, paymentManager } = this._buyerAccount(buyerIdentity);
943
987
  if (!buyerAddress || !this._channelStore)
944
988
  return [];
945
- const stored = this._channelStore.getAllChannelsByBuyer('buyer', buyerAddress);
989
+ const stored = this._channelStore.getBuyerPaymentChannels(buyerAddress);
946
990
  return stored.map((c) => {
947
991
  const liveReserve = c.status === CHANNEL_STATUS.ACTIVE
948
- ? this._buyerPaymentManager?.getReserveCeiling(c.peerId)
992
+ ? c.channelKind === CHANNEL_KIND.ONE_OFF
993
+ ? BigInt(c.confirmedReserveAmount ?? '0')
994
+ : paymentManager?.getReserveCeiling(c.peerId)
949
995
  : null;
950
996
  return {
951
997
  channelId: c.sessionId,
@@ -973,8 +1019,8 @@ export class AntseedNode extends EventEmitter {
973
1019
  * columns are semantically overloaded vs seller rows. Set in
974
1020
  * buyer-payment-manager.recordAndPersistTokens.
975
1021
  */
976
- getBuyerUsageTotals() {
977
- const buyerAddress = this._identity?.wallet.address ?? null;
1022
+ getBuyerUsageTotals(buyerIdentity) {
1023
+ const { address: buyerAddress } = this._buyerAccount(buyerIdentity);
978
1024
  if (!buyerAddress || !this._channelStore)
979
1025
  return EMPTY_BUYER_USAGE;
980
1026
  // Paid channels plus free-usage sessions — both carry real traffic and
@@ -1027,6 +1073,9 @@ export class AntseedNode extends EventEmitter {
1027
1073
  inputTokens: s.inputTokens,
1028
1074
  cachedInputTokens: s.cachedInputTokens,
1029
1075
  outputTokens: s.outputTokens,
1076
+ outputImages: s.outputImages,
1077
+ videoGenerations: s.videoGenerations,
1078
+ videoSeconds: s.videoSeconds,
1030
1079
  requestCount: s.requestCount,
1031
1080
  }));
1032
1081
  return {
@@ -1041,14 +1090,135 @@ export class AntseedNode extends EventEmitter {
1041
1090
  };
1042
1091
  }
1043
1092
  async sendRequest(peer, req, options) {
1044
- if (!this._buyerHandler)
1045
- throw buyerFault("Node not started or not in buyer mode", "node-not-started");
1046
- return this._buyerHandler.sendRequest(peer, req, undefined, options);
1093
+ return this._dispatchBuyerRequest(peer, req, undefined, options);
1047
1094
  }
1048
1095
  async sendRequestStream(peer, req, callbacks, options) {
1096
+ return this._dispatchBuyerRequest(peer, req, callbacks, options);
1097
+ }
1098
+ _dispatchBuyerRequest(peer, req, callbacks, options) {
1049
1099
  if (!this._buyerHandler)
1050
1100
  throw buyerFault("Node not started or not in buyer mode", "node-not-started");
1051
- return this._buyerHandler.sendRequest(peer, req, callbacks, options);
1101
+ const { buyerIdentity, ...executionOptions } = options ?? {};
1102
+ const context = this._buyerContext(buyerIdentity);
1103
+ return context
1104
+ ? context.sendRequest(peer, req, callbacks, executionOptions)
1105
+ : this._buyerHandler.sendRequest(peer, req, callbacks, executionOptions);
1106
+ }
1107
+ /** Buyer payment settings shared by every identity; null when payments are off. */
1108
+ _buyerPaymentConfig(paymentsDir) {
1109
+ const payments = this._config.payments;
1110
+ if (!payments?.enabled || !payments.rpcUrl || !payments.depositsAddress || !payments.channelsAddress || !payments.usdcAddress) {
1111
+ return null;
1112
+ }
1113
+ return {
1114
+ rpcUrl: payments.rpcUrl,
1115
+ ...(payments.fallbackRpcUrls ? { fallbackRpcUrls: payments.fallbackRpcUrls } : {}),
1116
+ depositsContractAddress: payments.depositsAddress,
1117
+ channelsContractAddress: payments.channelsAddress,
1118
+ usdcAddress: payments.usdcAddress,
1119
+ identityRegistryAddress: payments.identityRegistryAddress ?? '',
1120
+ chainId: payments.chainId ?? 8453,
1121
+ defaultAuthDurationSecs: payments.defaultAuthDurationSecs ?? 900, // 15 min — seller must call reserve() promptly
1122
+ maxPerRequestUsdc: BigInt(payments.maxPerRequestUsdc ?? "500000"), // $0.50 default — covers most LLM requests
1123
+ maxReserveAmountUsdc: BigInt(payments.maxReserveAmountUsdc ?? "1000000"), // $1.00 default per session (matches FIRST_SIGN_CAP)
1124
+ maxVideoRequestUsdc: BigInt(payments.maxVideoRequestUsdc ?? "5000000"), // $5.00 default per video
1125
+ disableMetadataV2Services: payments.disableMetadataV2Services ?? false,
1126
+ dataDir: paymentsDir,
1127
+ };
1128
+ }
1129
+ /**
1130
+ * Let this buyer node also pay as another wallet. The identity reuses the
1131
+ * node's discovery, routing and chain clients; sellers see it as its own
1132
+ * buyer with its own connections, channels and deposits. Select it per
1133
+ * request with `buyerIdentity`.
1134
+ */
1135
+ async addBuyerIdentity(name, identity) {
1136
+ if (!this._started || !this._buyerHandler || !this._identity) {
1137
+ throw new Error("Buyer identities can only be added to a started buyer node");
1138
+ }
1139
+ if (!isValidBuyerIdentityName(name) || name === DEFAULT_BUYER_IDENTITY) {
1140
+ throw new Error(`Invalid buyer identity name "${name}"`);
1141
+ }
1142
+ if (this._buyerIdentities.has(name)) {
1143
+ throw new Error(`Buyer identity "${name}" is already loaded`);
1144
+ }
1145
+ const address = identity.wallet.address.toLowerCase();
1146
+ if (this.buyerIdentities().some((entry) => entry.address.toLowerCase() === address)) {
1147
+ throw new Error(`Wallet ${identity.wallet.address} is already loaded as another buyer identity`);
1148
+ }
1149
+ const payments = this._config.payments;
1150
+ const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
1151
+ const context = await BuyerIdentityContext.create({
1152
+ name,
1153
+ identity,
1154
+ paymentConfig: this._buyerPaymentConfig(join(dataDir, "payments")),
1155
+ freeUsage: this._freeUsageClient && payments?.freeUsageAddress
1156
+ ? {
1157
+ chainId: payments.chainId ?? 8453,
1158
+ freeUsageContractAddress: payments.freeUsageAddress,
1159
+ defaultAuthDurationSecs: payments.defaultAuthDurationSecs ?? 3600,
1160
+ disableMetadataV2Services: payments.disableMetadataV2Services ?? false,
1161
+ }
1162
+ : null,
1163
+ requestHandler: {
1164
+ requestTimeoutMs: this._config.requestTimeoutMs,
1165
+ maxStreamBufferBytes: this._config.maxStreamBufferBytes,
1166
+ maxStreamDurationMs: this._config.maxStreamDurationMs,
1167
+ },
1168
+ maxUploadBodyBytes: this._config.maxUploadBodyBytes,
1169
+ requireSecureTransport: this._config.requireSecureTransport,
1170
+ shared: {
1171
+ channelStore: this._channelStore,
1172
+ depositsClient: this._depositsClient,
1173
+ channelsClient: this._channelsClient,
1174
+ sellerAddressResolver: this._sellerAddressResolver,
1175
+ verificationStorage: this._verificationStorage,
1176
+ verificationSampler: this._verificationSampler,
1177
+ peerCapabilities: this._peerCapabilities,
1178
+ isChainReachable: () => this._rpcHealth?.reachable ?? true,
1179
+ onChainReadFailure: () => this._rpcHealth?.reportFailure(),
1180
+ },
1181
+ emit: (event, payload) => { this.emit(event, payload); },
1182
+ });
1183
+ this._buyerIdentities.set(name, context);
1184
+ debugLog(`[Node] Buyer identity "${name}" loaded (wallet=${identity.wallet.address.slice(0, 10)}...)`);
1185
+ return { name, address: identity.wallet.address };
1186
+ }
1187
+ /** Close an identity's connections, letting sellers settle what they already served. */
1188
+ async removeBuyerIdentity(name) {
1189
+ const context = this._buyerIdentities.get(name);
1190
+ if (!context)
1191
+ return;
1192
+ this._buyerIdentities.delete(name);
1193
+ await context.stop();
1194
+ }
1195
+ hasBuyerIdentity(name) {
1196
+ return name === DEFAULT_BUYER_IDENTITY ? this._identity !== null : this._buyerIdentities.has(name);
1197
+ }
1198
+ /** Every identity this node can pay as, the default one first. */
1199
+ buyerIdentities() {
1200
+ const identities = [];
1201
+ if (this._identity)
1202
+ identities.push({ name: DEFAULT_BUYER_IDENTITY, address: this._identity.wallet.address });
1203
+ for (const context of this._buyerIdentities.values()) {
1204
+ identities.push({ name: context.name, address: context.address });
1205
+ }
1206
+ return identities;
1207
+ }
1208
+ _buyerContext(name) {
1209
+ if (name === undefined || name === DEFAULT_BUYER_IDENTITY)
1210
+ return null;
1211
+ const context = this._buyerIdentities.get(name);
1212
+ if (!context)
1213
+ throw buyerFault(`Unknown buyer identity "${name}"`, "invalid-request");
1214
+ return context;
1215
+ }
1216
+ /** Wallet address and payment manager behind an identity name. */
1217
+ _buyerAccount(name) {
1218
+ const context = this._buyerContext(name);
1219
+ if (context)
1220
+ return { address: context.address, paymentManager: context.paymentManager };
1221
+ return { address: this._identity?.wallet.address ?? null, paymentManager: this._buyerPaymentManager };
1052
1222
  }
1053
1223
  _createDHTConfig(port, bootstrapNodes) {
1054
1224
  return {
@@ -1135,7 +1305,10 @@ export class AntseedNode extends EventEmitter {
1135
1305
  this._muxes.get(peerId)?.abortPendingUploads();
1136
1306
  this._muxes.delete(peerId);
1137
1307
  this._paymentMuxes.delete(peerId);
1138
- this._peerCapabilities.delete(peerId);
1308
+ // Other buyer identities share this map; keep it while one is still connected.
1309
+ if (![...this._buyerIdentities.values()].some((context) => context.liveConnection(peerId))) {
1310
+ this._peerCapabilities.delete(peerId);
1311
+ }
1139
1312
  this._verificationMuxes.get(peerId)?.close();
1140
1313
  this._verificationMuxes.delete(peerId);
1141
1314
  this._sweepMuxes.delete(peerId);
@@ -1191,6 +1364,12 @@ export class AntseedNode extends EventEmitter {
1191
1364
  catch (err) {
1192
1365
  debugWarn(`[Node] Metering storage unavailable: ${err instanceof Error ? err.message : err}`);
1193
1366
  }
1367
+ try {
1368
+ this._resourceOwnership = new ResourceOwnershipStore(join(dataDir, "metering.db"));
1369
+ }
1370
+ catch (err) {
1371
+ debugWarn(`[Node] Resource ownership storage unavailable; video services will be refused: ${err instanceof Error ? err.message : err}`);
1372
+ }
1194
1373
  if (this._metering) {
1195
1374
  this._receiptGenerator = new ReceiptGenerator({
1196
1375
  peerId: identity.peerId,
@@ -1326,6 +1505,7 @@ export class AntseedNode extends EventEmitter {
1326
1505
  channelsClient: this._channelsClient,
1327
1506
  announcer: this._announcer,
1328
1507
  maxUploadBodyBytes: this._config.maxUploadBodyBytes,
1508
+ resourceOwnershipStore: this._resourceOwnership,
1329
1509
  ...(this._config.payments?.reserveEstimateOverdraftUsdc != null
1330
1510
  ? { reserveEstimateOverdraftUsdc: BigInt(this._config.payments.reserveEstimateOverdraftUsdc) }
1331
1511
  : {}),
@@ -1386,24 +1566,11 @@ export class AntseedNode extends EventEmitter {
1386
1566
  }
1387
1567
  }
1388
1568
  if (this._channelStore) {
1389
- const buyerPaymentConfig = {
1390
- rpcUrl: payments.rpcUrl,
1391
- ...(payments.fallbackRpcUrls ? { fallbackRpcUrls: payments.fallbackRpcUrls } : {}),
1392
- depositsContractAddress: payments.depositsAddress,
1393
- channelsContractAddress: payments.channelsAddress,
1394
- usdcAddress: payments.usdcAddress,
1395
- identityRegistryAddress: payments.identityRegistryAddress ?? '',
1396
- chainId: payments.chainId ?? 8453,
1397
- defaultAuthDurationSecs: payments.defaultAuthDurationSecs ?? 900, // 15 min — seller must call reserve() promptly
1398
- maxPerRequestUsdc: BigInt(payments.maxPerRequestUsdc ?? "500000"), // $0.50 default — covers most LLM requests
1399
- maxReserveAmountUsdc: BigInt(payments.maxReserveAmountUsdc ?? "1000000"), // $1.00 default per session (matches FIRST_SIGN_CAP)
1400
- disableMetadataV2Services: payments.disableMetadataV2Services ?? false,
1401
- dataDir: paymentsDir,
1402
- };
1569
+ const buyerPaymentConfig = this._buyerPaymentConfig(paymentsDir);
1403
1570
  this._buyerPaymentManager = new BuyerPaymentManager(identity, buyerPaymentConfig, this._channelStore, this._sellerAddressResolver ?? undefined);
1404
1571
  // Re-emit per-request spend so callers can attribute it to whatever
1405
1572
  // issued the request — the node only knows the seller and requestId.
1406
- this._buyerPaymentManager.setSpendListener((event) => this.emit('payment:spend', event));
1573
+ this._buyerPaymentManager.setSpendListener((event) => this.emit('payment:spend', { ...event, buyerIdentity: DEFAULT_BUYER_IDENTITY }));
1407
1574
  debugLog(`[Node] Buyer payment manager initialized (wallet=${identity.wallet.address.slice(0, 10)}... chainId=${buyerPaymentConfig.chainId} deposits=${buyerPaymentConfig.depositsContractAddress.slice(0, 10)}...)`);
1408
1575
  // Create negotiator that wraps the BPM with 402 handling and per-request auth
1409
1576
  this._buyerNegotiator = new BuyerPaymentNegotiator(identity, this._buyerPaymentManager, this._depositsClient, this._channelsClient, this._channelStore, {
@@ -1765,81 +1932,7 @@ export class AntseedNode extends EventEmitter {
1765
1932
  if (!this._connectionManager || !this._identity) {
1766
1933
  throw buyerFault("Node not started", "node-not-started");
1767
1934
  }
1768
- const existing = this._connectionManager.getConnection(peer.peerId);
1769
- const peerCapabilities = new Set(peer.capabilities ?? peer.metadata?.capabilities ?? []);
1770
- this._peerCapabilities.set(peer.peerId, peerCapabilities);
1771
- let endpointChanged = false;
1772
- // Check if the peer's endpoint has changed (e.g. IP rotation).
1773
- // Only applies to outbound connections where we registered the endpoint;
1774
- // inbound connections (peer connected to us) have no registered endpoint
1775
- // and are not subject to pinned-peer routing.
1776
- if (existing && peer.publicAddress) {
1777
- const currentEndpoint = ConnectionManager.resolvePeerEndpoint(peer.peerId);
1778
- const { host: newHost, port: newPort } = parsePeerAddress(peer.publicAddress);
1779
- if (currentEndpoint && (currentEndpoint.host !== newHost || currentEndpoint.port !== newPort)) {
1780
- debugLog(`[Node] Peer ${peer.peerId.slice(0, 12)}... endpoint changed from ${currentEndpoint.host}:${currentEndpoint.port} to ${newHost}:${newPort}, reconnecting`);
1781
- existing.close();
1782
- this._peerCapabilities.set(peer.peerId, peerCapabilities);
1783
- endpointChanged = true;
1784
- }
1785
- }
1786
- if (existing && !endpointChanged &&
1787
- existing.state !== ConnectionState.Closed &&
1788
- existing.state !== ConnectionState.Failed) {
1789
- debugLog(`[Node] Reusing existing connection to ${peer.peerId.slice(0, 12)}... (state=${existing.state})`);
1790
- // If still connecting, wait for it to reach Open or Authenticated
1791
- if (existing.state === ConnectionState.Connecting) {
1792
- debugLog(`[Node] Waiting for connection to open...`);
1793
- await new Promise((resolve, reject) => {
1794
- const onState = (state) => {
1795
- if (state === ConnectionState.Open || state === ConnectionState.Authenticated) {
1796
- existing.off("stateChange", onState);
1797
- resolve();
1798
- }
1799
- else if (state === ConnectionState.Failed || state === ConnectionState.Closed) {
1800
- existing.off("stateChange", onState);
1801
- reject(new Error(`Connection to ${peer.peerId} failed`));
1802
- }
1803
- };
1804
- existing.on("stateChange", onState);
1805
- });
1806
- }
1807
- return existing;
1808
- }
1809
- // Register the peer endpoint so ConnectionManager can resolve it
1810
- if (peer.publicAddress) {
1811
- const { host, port } = parsePeerAddress(peer.publicAddress);
1812
- this._connectionManager.registerPeerEndpoint(peer.peerId, { host, port });
1813
- debugLog(`[Node] Connecting to ${peer.peerId.slice(0, 12)}... at ${host}:${port}`);
1814
- }
1815
- else {
1816
- debugWarn(`[Node] Peer ${peer.peerId.slice(0, 12)}... has no public address`);
1817
- }
1818
- const connConfig = {
1819
- remotePeerId: peer.peerId,
1820
- isInitiator: true,
1821
- remoteCapabilities: [...peerCapabilities],
1822
- };
1823
- const conn = this._connectionManager.createConnection(connConfig);
1824
- // Wait for connection to open
1825
- await new Promise((resolve, reject) => {
1826
- const onState = (state) => {
1827
- debugLog(`[Node] Connection state: ${state}`);
1828
- if (state === ConnectionState.Open || state === ConnectionState.Authenticated) {
1829
- conn.off("stateChange", onState);
1830
- resolve();
1831
- }
1832
- else if (state === ConnectionState.Failed || state === ConnectionState.Closed) {
1833
- conn.off("stateChange", onState);
1834
- reject(new Error(`Connection to ${peer.peerId} failed`));
1835
- }
1836
- };
1837
- conn.on("stateChange", onState);
1838
- });
1839
- debugLog(`[Node] Connected to ${peer.peerId.slice(0, 12)}... via ${conn.transportDescription}`);
1840
- this._peerCapabilities.set(peer.peerId, peerCapabilities);
1841
- this._wireConnection(conn, peer.peerId);
1842
- return conn;
1935
+ return getOrOpenOutboundConnection(this._connectionManager, peer, this._peerCapabilities, (conn) => this._wireConnection(conn, peer.peerId));
1843
1936
  }
1844
1937
  _getOrCreateMux(peerId, conn) {
1845
1938
  const existing = this._muxes.get(peerId);
@@ -2135,10 +2228,6 @@ export class AntseedNode extends EventEmitter {
2135
2228
  && cachedPrice === 0;
2136
2229
  }
2137
2230
  }
2138
- function parsePeerAddress(address) {
2139
- const parts = address.split(":");
2140
- return { host: parts[0], port: parseInt(parts[1] ?? "6882", 10) };
2141
- }
2142
2231
  /**
2143
2232
  * Predicate used by `discoverPeers(service)` to filter the enumerated peer
2144
2233
  * list against an optional service hint. Matches case-insensitively against