@zkp2p/cash 0.1.8 → 0.2.0

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.
package/dist/index.js CHANGED
@@ -287,35 +287,41 @@ function toPricing(tuple) {
287
287
  };
288
288
  }
289
289
  function derivePayouts(paymentMethods, currencies, catalog) {
290
- return paymentMethods.flatMap((method) => {
290
+ const payouts = [];
291
+ for (const method of paymentMethods) {
291
292
  const platformHash = method.paymentMethodHash ?? "";
292
293
  if (!platformHash) return [];
293
294
  let platform;
294
295
  try {
295
296
  platform = resolvePaymentMethodNameFromHash(platformHash, catalog);
296
297
  } catch {
297
- platform = void 0;
298
+ return [];
298
299
  }
300
+ if (!platform) return [];
299
301
  const tuples = currencies.filter(
300
302
  (c) => (c.paymentMethodHash ?? "").toLowerCase() === platformHash.toLowerCase()
301
303
  );
302
304
  const base2 = {
303
- ...platform !== void 0 ? { platform } : {},
305
+ platform,
304
306
  platformHash,
305
307
  payeeHash: method.payeeDetailsHash ?? "",
306
308
  active: method.active ?? true
307
309
  };
308
- if (tuples.length === 0) return [{ ...base2, pricing: toPricing(void 0) }];
309
- return tuples.map((tuple) => {
310
+ if (tuples.length === 0) {
311
+ payouts.push({ ...base2, pricing: toPricing(void 0) });
312
+ continue;
313
+ }
314
+ for (const tuple of tuples) {
310
315
  const currency = tuple.currencyCode != null ? getCurrencyCodeFromHash(tuple.currencyCode) : void 0;
311
- return {
316
+ payouts.push({
312
317
  ...base2,
313
318
  ...currency !== void 0 ? { currency } : {},
314
319
  ...tuple.currencyCode != null ? { currencyHash: tuple.currencyCode } : {},
315
320
  pricing: toPricing(tuple)
316
- };
317
- });
318
- });
321
+ });
322
+ }
323
+ }
324
+ return payouts;
319
325
  }
320
326
 
321
327
  // src/engine/buyerProfile.ts
@@ -387,26 +393,6 @@ function parseCompositeDepositId(compositeId) {
387
393
  const onchainDepositId = BigInt(rawDepositId);
388
394
  return { escrowAddress: canonicalEscrowAddress, onchainDepositId };
389
395
  }
390
-
391
- // src/client/platformGroups.ts
392
- var PLATFORM_METHOD_GROUPS = {
393
- zelle: ["zelle", "zelle-chase", "zelle-bofa", "zelle-citi"]
394
- };
395
- var METHOD_TO_BASE_PLATFORM = new Map(
396
- Object.entries(PLATFORM_METHOD_GROUPS).flatMap(
397
- ([platform, methods]) => methods.map((method) => [method, platform])
398
- )
399
- );
400
- function basePlatformForMethod(method) {
401
- return METHOD_TO_BASE_PLATFORM.get(method) ?? method;
402
- }
403
- function paymentMethodsForPlatform(platform, catalog) {
404
- const configured = PLATFORM_METHOD_GROUPS[platform];
405
- const methods = configured ?? [platform];
406
- return methods.filter((method) => catalog[method] !== void 0);
407
- }
408
-
409
- // src/client/capabilities.ts
410
396
  var MIN_CASHOUT_AMOUNT = 10000n;
411
397
  var RECOMMENDED_MIN_CASHOUT_AMOUNT = 1000000n;
412
398
  var PAYEE_HINTS = {
@@ -425,20 +411,13 @@ var PAYEE_HINTS = {
425
411
  var IDENTITY_ATTESTATION_PLATFORMS = /* @__PURE__ */ new Set(["wise", "paypal"]);
426
412
  function buildCapabilities(environment) {
427
413
  const catalog = getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
428
- const currenciesByPlatform = /* @__PURE__ */ new Map();
429
- for (const [method, entry] of Object.entries(catalog)) {
430
- const platform = basePlatformForMethod(method);
414
+ const platforms = Object.entries(catalog).map(([platform, entry]) => {
431
415
  const currencies2 = (entry.currencies ?? []).map((hash) => getCurrencyCodeFromHash(hash)).filter(
432
416
  (code) => code != null && isMarketRateSupported(code)
433
417
  );
434
- const aggregate = currenciesByPlatform.get(platform) ?? /* @__PURE__ */ new Set();
435
- for (const currency of currencies2) aggregate.add(currency);
436
- currenciesByPlatform.set(platform, aggregate);
437
- }
438
- const platforms = [...currenciesByPlatform.entries()].map(([platform, currencies2]) => {
439
418
  return {
440
419
  platform,
441
- currencies: [...currencies2].sort(),
420
+ currencies: [...new Set(currencies2)].sort(),
442
421
  payeeHint: PAYEE_HINTS[platform] ?? "Your payment handle for this platform",
443
422
  requiresIdentityAttestation: IDENTITY_ATTESTATION_PLATFORMS.has(platform)
444
423
  };
@@ -517,7 +496,7 @@ function computeFillStatsSample(deposits, nowSeconds, environment) {
517
496
  }
518
497
  const currency = normalizeCurrencyCode(intent.fiatCurrency);
519
498
  if (!method || !currency) continue;
520
- const pair = `${basePlatformForMethod(method)}:${currency}`;
499
+ const pair = `${method}:${currency}`;
521
500
  fillCounts.set(pair, (fillCounts.get(pair) ?? 0) + 1);
522
501
  if (createdAt === void 0 || createdAt < windowStart || fulfilledAt < createdAt) continue;
523
502
  const previousPairFill = firstFillByPair.get(pair);
@@ -580,18 +559,18 @@ async function readFillStatsSample(client, environment) {
580
559
  }
581
560
  return computeFillStatsSample(deposits, now, environment);
582
561
  }
583
- async function readFillStats(client, environment) {
584
- return (await readFillStatsSample(client, environment)).stats;
585
- }
586
- async function readFillEta(client, input) {
587
- const sample = await readFillStatsSample(client, input.environment);
562
+ function fillEtaFromSample(sample, input) {
588
563
  const currency = input.currency.toUpperCase();
589
- const seconds = input.platform ? sample.stats[`${basePlatformForMethod(input.platform)}:${currency}`]?.medianFillSeconds : sample.medianFillSecondsByCurrency.get(currency);
564
+ const seconds = input.platform ? sample.stats[`${input.platform}:${currency}`]?.medianFillSeconds : sample.medianFillSecondsByCurrency.get(currency);
590
565
  return {
591
566
  ...seconds !== void 0 ? { seconds } : {},
592
567
  label: etaLabel(seconds)
593
568
  };
594
569
  }
570
+ async function readFillEta(client, input) {
571
+ const sample = await readFillStatsSample(client, input.environment);
572
+ return fillEtaFromSample(sample, input);
573
+ }
595
574
  var RELAY_API_URL = MAINNET_RELAY_API;
596
575
  var NATIVE_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000";
597
576
  var BASE_USDC_ASSET = {
@@ -1127,13 +1106,18 @@ async function readEstimate(publicClient, input, context = {}) {
1127
1106
  }
1128
1107
  } : {}
1129
1108
  };
1130
- if (context.indexerClient && context.environment) {
1109
+ if (context.includeEta !== false && context.environment) {
1131
1110
  try {
1132
- estimate.eta = await readFillEta(context.indexerClient, {
1111
+ const etaInput = {
1133
1112
  environment: context.environment,
1134
1113
  currency,
1135
1114
  ...input.platform ? { platform: input.platform } : {}
1136
- });
1115
+ };
1116
+ if (context.etaReader) {
1117
+ estimate.eta = await context.etaReader(etaInput);
1118
+ } else if (context.indexerClient) {
1119
+ estimate.eta = await readFillEta(context.indexerClient, etaInput);
1120
+ }
1137
1121
  } catch {
1138
1122
  }
1139
1123
  }
@@ -1142,6 +1126,7 @@ async function readEstimate(publicClient, input, context = {}) {
1142
1126
 
1143
1127
  // src/client/createCashClient.ts
1144
1128
  var DEFAULT_RPC_URL = "https://mainnet.base.org";
1129
+ var FILL_STATS_CACHE_MS = 15 * 60 * 1e3;
1145
1130
  var CASH_ATTRIBUTION_CODE = "peer-cash";
1146
1131
  var DEFAULT_CURATOR_URLS = {
1147
1132
  preproduction: "https://api-preprod.zkp2p.xyz",
@@ -1240,6 +1225,25 @@ function createCashClient(options) {
1240
1225
  });
1241
1226
  }
1242
1227
  const readClient = buildSdkClient(createWalletClient({ chain: base, transport }));
1228
+ let fillStatsCache = null;
1229
+ let fillStatsRequest = null;
1230
+ async function getFillStatsSample() {
1231
+ if (fillStatsCache && fillStatsCache.expiresAt > Date.now()) {
1232
+ return fillStatsCache.sample;
1233
+ }
1234
+ if (fillStatsRequest) return fillStatsRequest;
1235
+ fillStatsRequest = readFillStatsSample(readClient, environment);
1236
+ try {
1237
+ const sample = await fillStatsRequest;
1238
+ fillStatsCache = {
1239
+ sample,
1240
+ expiresAt: Date.now() + FILL_STATS_CACHE_MS
1241
+ };
1242
+ return sample;
1243
+ } finally {
1244
+ fillStatsRequest = null;
1245
+ }
1246
+ }
1243
1247
  const signingClients = /* @__PURE__ */ new WeakMap();
1244
1248
  async function signingClient(verb, opts) {
1245
1249
  const signer = opts?.signer;
@@ -1254,7 +1258,6 @@ function createCashClient(options) {
1254
1258
  }
1255
1259
  function validatePayout(input) {
1256
1260
  const { receive } = input;
1257
- const catalog = getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
1258
1261
  const platform = buildCapabilities(environment).platforms.find(
1259
1262
  (capability) => capability.platform === receive.platform
1260
1263
  );
@@ -1265,13 +1268,14 @@ function createCashClient(options) {
1265
1268
  if (!platform.currencies.includes(receive.currency)) {
1266
1269
  throw errors.unsupportedPlatformCurrency(receive.platform, receive.currency);
1267
1270
  }
1268
- const paymentMethods = paymentMethodsForPlatform(receive.platform, catalog);
1269
1271
  return {
1270
- payouts: paymentMethods.map((processorName) => ({
1271
- processorName,
1272
- currency: receive.currency,
1273
- payeeData: receive.payee
1274
- }))
1272
+ payouts: [
1273
+ {
1274
+ processorName: receive.platform,
1275
+ currency: receive.currency,
1276
+ payeeData: receive.payee
1277
+ }
1278
+ ]
1275
1279
  };
1276
1280
  }
1277
1281
  function validateDepositInput(amount, input, payoutInput = validatePayout(input)) {
@@ -1288,6 +1292,9 @@ function createCashClient(options) {
1288
1292
  ...range ? { intentAmountRange: range } : {}
1289
1293
  };
1290
1294
  }
1295
+ function isCashPayoutSet(payouts) {
1296
+ return payouts.length === 1 && payouts.every((payout) => payout.pricing.marketRate && payout.pricing.spreadBps === 0);
1297
+ }
1291
1298
  async function buildDepositParams(client, depositInput) {
1292
1299
  try {
1293
1300
  return await prepareCashDepositParams(client, depositInput);
@@ -1342,9 +1349,10 @@ function createCashClient(options) {
1342
1349
  deposit.currencies ?? [],
1343
1350
  getPaymentMethodsCatalog(BASE_CHAIN_ID, environment)
1344
1351
  );
1352
+ if (!isCashPayoutSet(payouts)) throw errors.orderNotFound(compositeId);
1345
1353
  return deriveCashOrder(compositeId, deposit.intents ?? [], {
1346
1354
  ...depositOrderOptions(deposit),
1347
- ...payouts.length > 0 ? { payouts } : {}
1355
+ payouts
1348
1356
  });
1349
1357
  }
1350
1358
  function escrowContext(depositId) {
@@ -1542,16 +1550,17 @@ function createCashClient(options) {
1542
1550
  async relayStatus(requestId) {
1543
1551
  return readRelayStatus(requestId, options.relay);
1544
1552
  },
1545
- async estimate(input) {
1553
+ async estimate(input, estimateOptions) {
1546
1554
  return readEstimate(readClient.publicClient, input, {
1547
- indexerClient: readClient,
1548
1555
  environment,
1556
+ ...estimateOptions?.includeEta !== void 0 ? { includeEta: estimateOptions.includeEta } : {},
1557
+ etaReader: async (etaInput) => fillEtaFromSample(await getFillStatsSample(), etaInput),
1549
1558
  ...options.relay ? { relay: options.relay } : {}
1550
1559
  });
1551
1560
  },
1552
1561
  async fillStats() {
1553
1562
  try {
1554
- return await readFillStats(readClient, environment);
1563
+ return (await getFillStatsSample()).stats;
1555
1564
  } catch (err) {
1556
1565
  throw errors.indexerUnavailable("fill stats", err);
1557
1566
  }
@@ -1790,7 +1799,7 @@ function createCashClient(options) {
1790
1799
  deposit.currencies ?? [],
1791
1800
  catalog
1792
1801
  );
1793
- if (payouts.length !== 1 || !payouts.every((payout) => payout.pricing.marketRate && payout.pricing.spreadBps === 0)) {
1802
+ if (!isCashPayoutSet(payouts)) {
1794
1803
  return [];
1795
1804
  }
1796
1805
  return [
@@ -2059,7 +2068,7 @@ var cashPayoutPricingJsonSchema = z.object({
2059
2068
  marketRate: z.boolean()
2060
2069
  });
2061
2070
  var cashPayoutInfoJsonSchema = z.object({
2062
- platform: z.string().optional(),
2071
+ platform: z.string(),
2063
2072
  platformHash: z.string(),
2064
2073
  currency: z.string().optional(),
2065
2074
  currencyHash: z.string().optional(),
package/dist/react.cjs CHANGED
@@ -9,6 +9,7 @@ function useEstimate({
9
9
  currency,
10
10
  platform,
11
11
  source,
12
+ includeEta = true,
12
13
  refreshIntervalMs = 0
13
14
  }) {
14
15
  const [estimate, setEstimate] = react.useState(null);
@@ -34,7 +35,7 @@ function useEstimate({
34
35
  }
35
36
  return;
36
37
  }
37
- const identity = { client, amount, currency, platform, source };
38
+ const identity = { client, amount, currency, platform, source, includeEta };
38
39
  if (isCurrent()) {
39
40
  loadingIdentityRef.current = identity;
40
41
  errorIdentityRef.current = null;
@@ -42,12 +43,15 @@ function useEstimate({
42
43
  setError(null);
43
44
  }
44
45
  try {
45
- const result = await client.estimate({
46
- amount,
47
- currency,
48
- ...platform ? { platform } : {},
49
- ...source ? { source } : {}
50
- });
46
+ const result = await client.estimate(
47
+ {
48
+ amount,
49
+ currency,
50
+ ...platform ? { platform } : {},
51
+ ...source ? { source } : {}
52
+ },
53
+ { includeEta }
54
+ );
51
55
  if (isCurrent()) {
52
56
  estimateIdentityRef.current = identity;
53
57
  setEstimate(result);
@@ -63,7 +67,7 @@ function useEstimate({
63
67
  } finally {
64
68
  if (isCurrent()) setIsLoading(false);
65
69
  }
66
- }, [client, currency, amount, platform, source]);
70
+ }, [client, currency, amount, platform, source, includeEta]);
67
71
  react.useEffect(() => {
68
72
  latestRequestRef.current += 1;
69
73
  estimateIdentityRef.current = null;
@@ -72,7 +76,7 @@ function useEstimate({
72
76
  setEstimate(null);
73
77
  setIsLoading(false);
74
78
  setError(null);
75
- }, [client, amount, currency, platform, source]);
79
+ }, [client, amount, currency, platform, source, includeEta]);
76
80
  react.useEffect(() => {
77
81
  mountedRef.current = true;
78
82
  void refresh();
@@ -85,7 +89,7 @@ function useEstimate({
85
89
  if (timerRef.current) clearInterval(timerRef.current);
86
90
  };
87
91
  }, [refresh, refreshIntervalMs]);
88
- const matchesCurrentIdentity = (identity) => identity?.client === client && identity.amount === amount && identity.currency === currency && identity.platform === platform && identity.source === source;
92
+ const matchesCurrentIdentity = (identity) => identity?.client === client && identity.amount === amount && identity.currency === currency && identity.platform === platform && identity.source === source && identity.includeEta === includeEta;
89
93
  return {
90
94
  estimate: matchesCurrentIdentity(estimateIdentityRef.current) ? estimate : null,
91
95
  isLoading: matchesCurrentIdentity(loadingIdentityRef.current) ? isLoading : false,
package/dist/react.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CurrencyType } from '@zkp2p/sdk';
2
- import { r as CashClient, G as EstimateInput, i as CashEstimate, E as CashoutOptions, h as CashoutResult, D as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-BIzOLHjF.cjs';
2
+ import { r as CashClient, G as EstimateInput, i as CashEstimate, E as CashoutOptions, h as CashoutResult, D as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-CTEXn9FF.cjs';
3
3
  import { WalletClient } from 'viem';
4
4
  import '@relayprotocol/relay-sdk';
5
5
 
@@ -12,6 +12,8 @@ interface UseEstimateOptions {
12
12
  platform?: string | null | undefined;
13
13
  /** Optional Relay source. Omit for the Base USDC default path. */
14
14
  source?: EstimateInput['source'] | null | undefined;
15
+ /** Disable to render the oracle rate before loading pair fill stats separately. */
16
+ includeEta?: boolean;
15
17
  /** Re-fetch interval (ms) so the displayed rate tracks the market. 0 = no auto-refresh. */
16
18
  refreshIntervalMs?: number;
17
19
  }
@@ -20,7 +22,7 @@ interface UseEstimateOptions {
20
22
  * estimate; the binding rate resolves at the Chainlink oracle when a buyer
21
23
  * fills - there is no committed quote to show.
22
24
  */
23
- declare function useEstimate({ client, amount, currency, platform, source, refreshIntervalMs, }: UseEstimateOptions): {
25
+ declare function useEstimate({ client, amount, currency, platform, source, includeEta, refreshIntervalMs, }: UseEstimateOptions): {
24
26
  estimate: CashEstimate | null;
25
27
  isLoading: boolean;
26
28
  error: Error | null;
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CurrencyType } from '@zkp2p/sdk';
2
- import { r as CashClient, G as EstimateInput, i as CashEstimate, E as CashoutOptions, h as CashoutResult, D as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-BIzOLHjF.js';
2
+ import { r as CashClient, G as EstimateInput, i as CashEstimate, E as CashoutOptions, h as CashoutResult, D as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-CTEXn9FF.js';
3
3
  import { WalletClient } from 'viem';
4
4
  import '@relayprotocol/relay-sdk';
5
5
 
@@ -12,6 +12,8 @@ interface UseEstimateOptions {
12
12
  platform?: string | null | undefined;
13
13
  /** Optional Relay source. Omit for the Base USDC default path. */
14
14
  source?: EstimateInput['source'] | null | undefined;
15
+ /** Disable to render the oracle rate before loading pair fill stats separately. */
16
+ includeEta?: boolean;
15
17
  /** Re-fetch interval (ms) so the displayed rate tracks the market. 0 = no auto-refresh. */
16
18
  refreshIntervalMs?: number;
17
19
  }
@@ -20,7 +22,7 @@ interface UseEstimateOptions {
20
22
  * estimate; the binding rate resolves at the Chainlink oracle when a buyer
21
23
  * fills - there is no committed quote to show.
22
24
  */
23
- declare function useEstimate({ client, amount, currency, platform, source, refreshIntervalMs, }: UseEstimateOptions): {
25
+ declare function useEstimate({ client, amount, currency, platform, source, includeEta, refreshIntervalMs, }: UseEstimateOptions): {
24
26
  estimate: CashEstimate | null;
25
27
  isLoading: boolean;
26
28
  error: Error | null;
package/dist/react.js CHANGED
@@ -7,6 +7,7 @@ function useEstimate({
7
7
  currency,
8
8
  platform,
9
9
  source,
10
+ includeEta = true,
10
11
  refreshIntervalMs = 0
11
12
  }) {
12
13
  const [estimate, setEstimate] = useState(null);
@@ -32,7 +33,7 @@ function useEstimate({
32
33
  }
33
34
  return;
34
35
  }
35
- const identity = { client, amount, currency, platform, source };
36
+ const identity = { client, amount, currency, platform, source, includeEta };
36
37
  if (isCurrent()) {
37
38
  loadingIdentityRef.current = identity;
38
39
  errorIdentityRef.current = null;
@@ -40,12 +41,15 @@ function useEstimate({
40
41
  setError(null);
41
42
  }
42
43
  try {
43
- const result = await client.estimate({
44
- amount,
45
- currency,
46
- ...platform ? { platform } : {},
47
- ...source ? { source } : {}
48
- });
44
+ const result = await client.estimate(
45
+ {
46
+ amount,
47
+ currency,
48
+ ...platform ? { platform } : {},
49
+ ...source ? { source } : {}
50
+ },
51
+ { includeEta }
52
+ );
49
53
  if (isCurrent()) {
50
54
  estimateIdentityRef.current = identity;
51
55
  setEstimate(result);
@@ -61,7 +65,7 @@ function useEstimate({
61
65
  } finally {
62
66
  if (isCurrent()) setIsLoading(false);
63
67
  }
64
- }, [client, currency, amount, platform, source]);
68
+ }, [client, currency, amount, platform, source, includeEta]);
65
69
  useEffect(() => {
66
70
  latestRequestRef.current += 1;
67
71
  estimateIdentityRef.current = null;
@@ -70,7 +74,7 @@ function useEstimate({
70
74
  setEstimate(null);
71
75
  setIsLoading(false);
72
76
  setError(null);
73
- }, [client, amount, currency, platform, source]);
77
+ }, [client, amount, currency, platform, source, includeEta]);
74
78
  useEffect(() => {
75
79
  mountedRef.current = true;
76
80
  void refresh();
@@ -83,7 +87,7 @@ function useEstimate({
83
87
  if (timerRef.current) clearInterval(timerRef.current);
84
88
  };
85
89
  }, [refresh, refreshIntervalMs]);
86
- const matchesCurrentIdentity = (identity) => identity?.client === client && identity.amount === amount && identity.currency === currency && identity.platform === platform && identity.source === source;
90
+ const matchesCurrentIdentity = (identity) => identity?.client === client && identity.amount === amount && identity.currency === currency && identity.platform === platform && identity.source === source && identity.includeEta === includeEta;
87
91
  return {
88
92
  estimate: matchesCurrentIdentity(estimateIdentityRef.current) ? estimate : null,
89
93
  isLoading: matchesCurrentIdentity(loadingIdentityRef.current) ? isLoading : false,
package/dist/tools.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // package.json
4
4
  var package_default = {
5
- version: "0.1.8"};
5
+ version: "0.2.0"};
6
6
 
7
7
  // src/tools/index.ts
8
8
  var bigintString = {
package/dist/tools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // package.json
2
2
  var package_default = {
3
- version: "0.1.8"};
3
+ version: "0.2.0"};
4
4
 
5
5
  // src/tools/index.ts
6
6
  var bigintString = {
@@ -130,11 +130,16 @@ measure buyer signal to fulfillment; that would miss the buyer-arrival wait
130
130
  that users actually care about. The public shape is small: `{ seconds, label }`.
131
131
 
132
132
  `fillStats()` exposes the sampler's raw evidence for catalog filtering as
133
- `Record<"platform:currency", { fills, medianFillSeconds? }>`. Bank-scoped Zelle
134
- methods aggregate to `zelle:USD`. Consumers own thresholding; the recommended
133
+ `Record<"platform:currency", { fills, medianFillSeconds? }>`. Generic Zelle
134
+ fills are reported as `zelle:USD`. Consumers own thresholding; the recommended
135
135
  gate is `fills >= 10 && medianFillSeconds <= 48h`, with a fail-open fallback to
136
136
  the full capability catalog when the read fails or filtering would empty it.
137
137
  Medians are per-deposit first-fill latencies, never means or censored cohorts.
138
+ The client caches one raw environment snapshot for 15 minutes and de-duplicates
139
+ concurrent reads; ETA resolution still uses only the requested normalized
140
+ `platform:currency` key. A progressive UI can call
141
+ `estimate(input, { includeEta: false })` to render rate/receive immediately,
142
+ then read that pair from `fillStats()` without coupling the two loading states.
138
143
 
139
144
  - **Buyer arrival time is market-driven.** A deposit at market rate should
140
145
  fill fast, but the ETA is only a recent historical sample.
@@ -190,6 +195,11 @@ platform, currency, payee hash, and a pricing proof (`spreadBps: 0`,
190
195
  `kind: 'oracle_chainlink'`, `marketRate: true`): the zero-spread claim is a
191
196
  queryable fact, not marketing copy.
192
197
 
198
+ Reconstruction is fail-closed: every payment method on the indexed deposit
199
+ must resolve through the active SDK catalog, and the result must be exactly
200
+ one zero-spread oracle payout. `orders()` excludes unsupported or mixed rows;
201
+ `order()` returns `ORDER_NOT_FOUND` rather than partially reclassifying them.
202
+
193
203
  ## Who is this buyer?
194
204
 
195
205
  `buyer(address)` aggregates the matched buyer's full intent history into a
package/llms.txt CHANGED
@@ -24,8 +24,13 @@ Key facts:
24
24
  - fillStats() returns raw `{ fills, medianFillSeconds? }` evidence keyed by
25
25
  `platform:currency`. Recommended consumer gate: fills >= 10 and median <=
26
26
  48h; fail open to capabilities() if unavailable or filtering empties it.
27
- - capabilities() exposes one Zelle platform. A zelle cashout internally attaches
28
- the generic method plus Chase, Bank of America, and Citi buyer routes.
27
+ Its raw environment snapshot is cached for 15 minutes, while lookups remain
28
+ exact to the normalized platform:currency pair.
29
+ - Progressive UIs can call estimate(input, { includeEta: false }) so the
30
+ oracle rate is not blocked by indexer history, then load the exact pair from
31
+ fillStats() separately.
32
+ - capabilities() exposes one Zelle platform. A zelle cashout attaches only the
33
+ generic Zelle payment method to the on-chain deposit.
29
34
  - Resume any order from its depositId alone (composite escrow_onchainId).
30
35
  - One unwind verb: withdraw(depositId) - prunes expired intents automatically;
31
36
  pass amount for a partial withdrawal of the unlocked balance.
@@ -51,6 +56,8 @@ Key facts:
51
56
  payment id, released USDC, and fill latency after the proof.
52
57
  - Orders carry their payout legs (platform, currency, payee hash) plus a
53
58
  verifiable pricing proof (spreadBps: 0, oracle kind) from indexed data.
59
+ - Order reads fail closed when any deposit method is absent from the active
60
+ catalog; mixed historical deposits are never partially reclassified.
54
61
  - buyer(address) aggregates a buyer's track record (fulfilled/pruned/success
55
62
  rate) from their full intent history.
56
63
  - Default path is same-chain Base USDC. Optional `source` on `cashout()` runs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkp2p/cash",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Peer Cash - offramp-only SDK for routing crypto to Base USDC, then cashing out to fiat at the live oracle market rate.",
5
5
  "license": "MIT",
6
6
  "author": "Peer (https://peer.xyz)",
@@ -106,7 +106,7 @@
106
106
  },
107
107
  "dependencies": {
108
108
  "@relayprotocol/relay-sdk": "^6.1.3",
109
- "@zkp2p/sdk": "^0.8.1",
109
+ "@zkp2p/sdk": "^0.9.0",
110
110
  "zod": "^3.25.76"
111
111
  },
112
112
  "peerDependencies": {