@zkp2p/cash 0.5.4-rc.1 → 0.5.4-rc.2

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/AGENTS.md CHANGED
@@ -8,7 +8,7 @@ You are integrating Peer Cash: an offramp that routes Relay-supported EVM
8
8
  assets or NEAR Intents 1Click external deposits into Base USDC, then converts
9
9
  Base USDC to fiat (Venmo, Revolut, Wise, Alipay, Zelle, ...) at a zero-spread
10
10
  Chainlink market rate. Existing corridors bind at intent signal; Alipay/CNY
11
- fixes a fresh Ethereum Chainlink snapshot during deposit preparation. The user whose USDC you
11
+ fixes a fresh Ethereum Chainlink snapshot during deposit preparation; opt-in UPI/INR uses Polygon Chainlink. The user whose USDC you
12
12
  manage is the **maker**; a buyer pays them fiat and proves it with TEE-TLS; the
13
13
  protocol releases the USDC. Funds are held by the protocol, and only the maker
14
14
  can withdraw an unmatched deposit.
@@ -70,10 +70,10 @@ deposit-level integration share instead of applying maker L1/L2.
70
70
  - **Venmo, Revolut, Cash App, Monzo** validate the handle against the live
71
71
  platform at registration - the account must exist. The rest (Zelle, Chime,
72
72
  etc.) are format-checked only. Match handles to the `payeeHint`.
73
- - **UPI is staging-only and opt-in.** Construct the client with
73
+ - **UPI is staging/preproduction-only and opt-in.** Construct the client with
74
74
  `features: { upi: true }`. Any valid UPI ID from any bank can receive; there
75
75
  is no seller bank login, extension, identity attestation, or prior
76
- registration flow. Buyers currently prove from HDFC Bank through Gmail.
76
+ registration flow. Buyers pay and verify through Amazon Pay using standard UPI.
77
77
 
78
78
  ## The loop
79
79
 
@@ -196,8 +196,8 @@ const route = await cash.nearIntentsStatus({
196
196
 
197
197
  - **Respect the declared binding point.** `estimate()` is
198
198
  `kind: 'oracle-estimate'`. Its `binding` is `intent-signal` for existing
199
- on-chain oracle corridors and `deposit-creation` for Alipay/CNY. Do not call
200
- an estimate locked before that point. Once Alipay/CNY is prepared, its fresh
199
+ on-chain oracle corridors and `deposit-creation` for Alipay/CNY and UPI/INR. Do not call
200
+ an estimate locked before that point. Once a creation-time corridor is prepared, its fresh
201
201
  Chainlink snapshot is the on-chain maker floor.
202
202
  - **Do not invent an ETA.** Use `estimate().eta`: `{ seconds, label }` backed
203
203
  by the same rolling 30-day, intent-attributed pair sample as `fillStats()`,
@@ -342,4 +342,4 @@ read-only RPC with `upiCreationRateRpcUrl` or `upiCreationRateTransport`.
342
342
  Alipay/CNY retains the Ethereum registry and `creationRateRpcUrl` /
343
343
  `creationRateTransport`. UPI rejects the wrong chain, invalid rounds, and
344
344
  observations older than 24 hours; market closures do not bypass freshness.
345
- This does not change the staging-only UPI opt-in gate.
345
+ UPI remains opt-in on staging/preproduction and disabled in production.
package/README.md CHANGED
@@ -4,8 +4,8 @@ Route Relay-supported EVM assets or NEAR Intents 1Click external deposits into
4
4
  Base USDC, then cash out to fiat on Venmo, Revolut, Wise, Alipay, Zelle, and
5
5
  more at a zero-spread Chainlink market rate with no centralized off-ramp
6
6
  provider. Existing corridors bind the live oracle when a buyer signals;
7
- Alipay/CNY fixes a fresh Ethereum Chainlink snapshot when the SDK prepares the
8
- deposit.
7
+ Alipay/CNY and opt-in UPI/INR fix fresh Chainlink snapshots when the SDK prepares
8
+ the deposit (Ethereum for CNY; Polygon for INR).
9
9
 
10
10
  Peer Cash is an **offramp-only** SDK for the [ZKP2P](https://peer.xyz)
11
11
  protocol. The cashing-out user is the maker: their USDC becomes a deposit in
@@ -94,16 +94,18 @@ for await (const order of cash.watch(depositId)) {
94
94
  }
95
95
  ```
96
96
 
97
- ### Staging UPI cash-out
97
+ ### Staging and preproduction UPI cash-out
98
98
 
99
- UPI is an opt-in staging corridor until the payment method exists in production
100
- contracts. Any valid UPI ID from any bank can receive a cash-out. The seller
99
+ UPI requires the canonical UPI/INR catalog from `@zkp2p/sdk` 0.14.2-rc.1
100
+ or its approved successor; a missing catalog entry keeps the corridor disabled.
101
+ UPI is an opt-in staging and preproduction corridor. Production remains disabled
102
+ pending rollout review. Any valid UPI ID from any bank can receive a cash-out. The seller
101
103
  does not connect a bank account, install an extension, or complete a separate
102
104
  registration flow:
103
105
 
104
106
  ```ts
105
107
  const cash = createCashClient({
106
- environment: 'staging',
108
+ environment: 'preproduction',
107
109
  features: { upi: true },
108
110
  });
109
111
 
@@ -116,18 +118,25 @@ await cash.cashout(
116
118
  );
117
119
  ```
118
120
 
119
- Buyers currently prove UPI payments from HDFC Bank through one-shot, read-only
120
- Gmail access. That buyer limitation does not restrict which bank issued the
121
- seller's UPI ID.
121
+ Buyers pay and verify through Amazon Pay using standard UPI. UPI Lite and
122
+ merchant payments are unsupported. The seller may receive at a valid UPI ID
123
+ from any bank.
124
+
125
+ INR pricing uses the [Chainlink Polygon INR/USD feed](https://data.chain.link/feeds/polygon/mainnet/inr-usd),
126
+ inverted into INR per USDC and fixed at deposit preparation. The SDK rejects
127
+ invalid rounds and readings older than 24 hours, including market-hour gaps.
128
+ Only oracle reads use Polygon; funds and transactions stay on Base. Override
129
+ the Polygon reader with `upiCreationRateTransport` or `upiCreationRateRpcUrl`.
130
+ The existing `creationRateTransport`/`creationRateRpcUrl` options remain Ethereum-only for CNY.
122
131
 
123
132
  ## Pick the right SDK
124
133
 
125
134
  Peer Cash and the general ZKP2P SDK serve different integration depths:
126
135
 
127
- | Package | Use it when | Boundary |
128
- | ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
129
- | `@zkp2p/cash` | Cash-out is the product | Offramp only. The user is always the maker, the destination is Base USDC, pricing is zero-spread Chainlink (signal-time by default; creation-time for Alipay/CNY), and the SDK owns the resumable order lifecycle. |
130
- | `@zkp2p/sdk` | You are composing directly with the Peer protocol | General maker and taker operations, deposits, intents, proofs, quotes, vaults, rate managers, referrals, hooks, and API helpers. Your application owns the workflow and protocol choices. |
136
+ | Package | Use it when | Boundary |
137
+ | ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
138
+ | `@zkp2p/cash` | Cash-out is the product | Offramp only. The user is always the maker, the destination is Base USDC, pricing is zero-spread Chainlink (signal-time by default; creation-time for Alipay/CNY and UPI/INR), and the SDK owns the resumable order lifecycle. |
139
+ | `@zkp2p/sdk` | You are composing directly with the Peer protocol | General maker and taker operations, deposits, intents, proofs, quotes, vaults, rate managers, referrals, hooks, and API helpers. Your application owns the workflow and protocol choices. |
131
140
 
132
141
  Peer Cash is a narrow facade over `@zkp2p/sdk`, not a replacement for it. It
133
142
  cannot express custom spreads, buyer-side proof flows, vaults, disputes, or
@@ -177,14 +186,14 @@ mixed historical deposit.
177
186
 
178
187
  ## Payout rails and access policies
179
188
 
180
- | Payout rail | Access-policy behavior | New payee registration |
181
- | --------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
182
- | Venmo | Peer Pay merchant policy attaches for that payment method | Curator validates the live handle |
183
- | PayPal | Same method-scoped Peer Pay follow-up | Requires a Peer TEE browser-extension identity attestation |
184
- | Cash App | No access-policy follow-up; non-chargebackable and no stake required | Curator validates the live handle |
185
- | Wise | No access-policy follow-up | Requires a Peer TEE browser-extension identity attestation |
186
- | UPI (staging opt-in) | No access-policy follow-up | Any valid UPI ID; no account connection or identity attestation |
187
- | Other supported rails | No access-policy follow-up; use `capabilities()` for currencies and format | Follow the `payeeHint`; live-validation behavior is described in the integration guide |
189
+ | Payout rail | Access-policy behavior | New payee registration |
190
+ | ---------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
191
+ | Venmo | Peer Pay merchant policy attaches for that payment method | Curator validates the live handle |
192
+ | PayPal | Same method-scoped Peer Pay follow-up | Requires a Peer TEE browser-extension identity attestation |
193
+ | Cash App | No access-policy follow-up; non-chargebackable and no stake required | Curator validates the live handle |
194
+ | Wise | No access-policy follow-up | Requires a Peer TEE browser-extension identity attestation |
195
+ | UPI (staging/preprod opt-in) | No access-policy follow-up | Any valid UPI ID; no account connection or identity attestation |
196
+ | Other supported rails | No access-policy follow-up; use `capabilities()` for currencies and format | Follow the `payeeHint`; live-validation behavior is described in the integration guide |
188
197
 
189
198
  No platform requires an atomic access-policy flow. `cashout()` and `prepare()`
190
199
  work with any viem `WalletClient`, including a local or externally connected
@@ -380,7 +389,7 @@ awaiting-buyer ──────────► matched ───────
380
389
  resulting CNY-per-USDC maker floor when it prepares the deposit. A buyer may
381
390
  signal at that floor or a better rate for the maker.
382
391
  - **Read `binding`.** `estimate().binding` is `intent-signal` by default and
383
- `deposit-creation` for Alipay/CNY. An estimate remains approximate until its
392
+ `deposit-creation` for Alipay/CNY and UPI/INR. An estimate remains approximate until its
384
393
  stated binding point.
385
394
  - **ETA is historical.** `estimate().eta` is just `{ seconds, label }`, backed
386
395
  by the same rolling 30-day, intent-attributed pair sampler as `fillStats()`,
@@ -474,7 +483,7 @@ is the default source and the only destination asset for cashout orders.
474
483
  Runnable first-party examples in [`examples/`](examples):
475
484
 
476
485
  - [`node-cashout.ts`](examples/node-cashout.ts) - server-side cash-out with a private-key signer, plus order tracking.
477
- - [`upi-staging-cashout.ts`](examples/upi-staging-cashout.ts) - opt-in UPI/INR cash-out to any valid UPI ID on staging.
486
+ - [`upi-staging-cashout.ts`](examples/upi-staging-cashout.ts) - opt-in UPI/INR cash-out to any valid UPI ID on staging or preproduction.
478
487
  - [`agent-tool-use.ts`](examples/agent-tool-use.ts) - wiring the verbs into an agent tool-use loop with host-side signing.
479
488
  - [`carpe-diem-provider-cashout`](examples/carpe-diem-provider-cashout) - cash out confirmed Carpe Diem provider DIEM revenue through the connected Base wallet.
480
489
  - [`mpp-merchant-cashout`](examples/mpp-merchant-cashout) - turn confirmed MPP merchant revenue into an unsigned Peer Cash plan while the merchant keeps custody and signing.
@@ -498,12 +507,3 @@ the package, not the contributor entry point.
498
507
  ## License
499
508
 
500
509
  MIT
501
-
502
- UPI/INR reads the live Chainlink Polygon mainnet proxy
503
- `0xDA0F8Df6F5dB15b346f4B8D1156722027E194E60` (chain 137), inverts
504
- USD per INR, and rounds the creation-time maker floor up. Configure its
505
- read-only RPC with `upiCreationRateRpcUrl` or `upiCreationRateTransport`.
506
- Alipay/CNY retains the Ethereum registry and `creationRateRpcUrl` /
507
- `creationRateTransport`. UPI rejects the wrong chain, invalid rounds, and
508
- observations older than 24 hours; market closures do not bypass freshness.
509
- This does not change the staging-only UPI opt-in gate.
package/dist/index.cjs CHANGED
@@ -142,26 +142,18 @@ async function readCashCreationRate(publicClient, platform, currency, nowSeconds
142
142
  async function readAlipayCnyCreationRate(publicClient, nowSeconds = Math.floor(Date.now() / 1e3)) {
143
143
  return readCashCreationRate(publicClient, "alipay", "CNY", nowSeconds);
144
144
  }
145
- var UPI_STAGING_PAYMENT_METHOD_HASH = "0xe99a5081226cbbff9440a63da5caa04fa30f210c12c4dd9976132ac075054cd9";
146
145
  function getCashPaymentMethodsCatalog(environment, features = {}) {
147
146
  const catalog = sdk.getPaymentMethodsCatalog(8453, environment);
148
- if (environment !== "staging" || features.upi !== true || catalog.upi) {
147
+ if ((environment === "staging" || environment === "preproduction") && features.upi === true) {
149
148
  return catalog;
150
149
  }
151
- return {
152
- ...catalog,
153
- upi: {
154
- paymentMethodHash: UPI_STAGING_PAYMENT_METHOD_HASH,
155
- currencies: [sdk.currencyInfo.INR.currencyCodeHash],
156
- timestampBuffer: 30,
157
- providerHashes: []
158
- }
159
- };
150
+ const enabledCatalog = { ...catalog };
151
+ delete enabledCatalog.upi;
152
+ return enabledCatalog;
160
153
  }
161
154
  function resolveCashPaymentMethodHash(processorName, catalog) {
162
- const normalized = processorName.trim().toLowerCase();
163
- if (normalized === "upi" && catalog.upi) {
164
- return catalog.upi.paymentMethodHash;
155
+ if (processorName.trim().toLowerCase() === "upi" && !catalog.upi) {
156
+ throw new Error("UPI is not enabled in this environment catalog");
165
157
  }
166
158
  return sdk.resolvePaymentMethodHashFromCatalog(processorName, catalog);
167
159
  }
@@ -665,9 +657,7 @@ function buildCapabilities(environment, features = {}) {
665
657
  requiresIdentityAttestation: IDENTITY_ATTESTATION_PLATFORMS.has(platform),
666
658
  requiresAtomicAccessPolicy: false
667
659
  };
668
- }).filter(
669
- (p) => p.currencies.length > 0 && (p.platform !== "upi" || environment === "staging" && features.upi === true)
670
- ).sort((a, b) => a.platform.localeCompare(b.platform));
660
+ }).filter((p) => p.currencies.length > 0).sort((a, b) => a.platform.localeCompare(b.platform));
671
661
  const currencies = [...new Set(platforms.flatMap((p) => p.currencies))].sort();
672
662
  const baseUsdc = { address: BASE_USDC_ADDRESS, symbol: "USDC", decimals: USDC_DECIMALS };
673
663
  return {
package/dist/index.d.cts CHANGED
@@ -255,9 +255,8 @@ declare function buildIntentAmountRange(amount: bigint): {
255
255
  *
256
256
  * Registers payee details with the curator (no auth), resolves payment-method
257
257
  * hashes + the gating service from the catalog, and assembles the override
258
- * arrays with signal-time oracle configs. Alipay/CNY is the explicit exception:
259
- * it fixes a fresh Chainlink Ethereum snapshot as the maker floor because Base
260
- * has no CNY oracle adapter.
258
+ * arrays with signal-time oracle configs. Alipay/CNY and UPI/INR instead fix
259
+ * fresh Chainlink snapshots from Ethereum and Polygon as their maker floors.
261
260
  */
262
261
  declare function prepareCashDepositParams(client: Zkp2pClient, input: CashDepositInput, adapters?: OracleAdapterOverrides, creationRateReader?: CreationRateReader, features?: CashCatalogFeatures): Promise<CreateDepositParamsArg>;
263
262
 
package/dist/index.d.ts CHANGED
@@ -255,9 +255,8 @@ declare function buildIntentAmountRange(amount: bigint): {
255
255
  *
256
256
  * Registers payee details with the curator (no auth), resolves payment-method
257
257
  * hashes + the gating service from the catalog, and assembles the override
258
- * arrays with signal-time oracle configs. Alipay/CNY is the explicit exception:
259
- * it fixes a fresh Chainlink Ethereum snapshot as the maker floor because Base
260
- * has no CNY oracle adapter.
258
+ * arrays with signal-time oracle configs. Alipay/CNY and UPI/INR instead fix
259
+ * fresh Chainlink snapshots from Ethereum and Polygon as their maker floors.
261
260
  */
262
261
  declare function prepareCashDepositParams(client: Zkp2pClient, input: CashDepositInput, adapters?: OracleAdapterOverrides, creationRateReader?: CreationRateReader, features?: CashCatalogFeatures): Promise<CreateDepositParamsArg>;
263
262
 
package/dist/index.js CHANGED
@@ -120,26 +120,18 @@ async function readCashCreationRate(publicClient, platform, currency, nowSeconds
120
120
  async function readAlipayCnyCreationRate(publicClient, nowSeconds = Math.floor(Date.now() / 1e3)) {
121
121
  return readCashCreationRate(publicClient, "alipay", "CNY", nowSeconds);
122
122
  }
123
- var UPI_STAGING_PAYMENT_METHOD_HASH = "0xe99a5081226cbbff9440a63da5caa04fa30f210c12c4dd9976132ac075054cd9";
124
123
  function getCashPaymentMethodsCatalog(environment, features = {}) {
125
124
  const catalog = getPaymentMethodsCatalog(8453, environment);
126
- if (environment !== "staging" || features.upi !== true || catalog.upi) {
125
+ if ((environment === "staging" || environment === "preproduction") && features.upi === true) {
127
126
  return catalog;
128
127
  }
129
- return {
130
- ...catalog,
131
- upi: {
132
- paymentMethodHash: UPI_STAGING_PAYMENT_METHOD_HASH,
133
- currencies: [currencyInfo.INR.currencyCodeHash],
134
- timestampBuffer: 30,
135
- providerHashes: []
136
- }
137
- };
128
+ const enabledCatalog = { ...catalog };
129
+ delete enabledCatalog.upi;
130
+ return enabledCatalog;
138
131
  }
139
132
  function resolveCashPaymentMethodHash(processorName, catalog) {
140
- const normalized = processorName.trim().toLowerCase();
141
- if (normalized === "upi" && catalog.upi) {
142
- return catalog.upi.paymentMethodHash;
133
+ if (processorName.trim().toLowerCase() === "upi" && !catalog.upi) {
134
+ throw new Error("UPI is not enabled in this environment catalog");
143
135
  }
144
136
  return resolvePaymentMethodHashFromCatalog(processorName, catalog);
145
137
  }
@@ -643,9 +635,7 @@ function buildCapabilities(environment, features = {}) {
643
635
  requiresIdentityAttestation: IDENTITY_ATTESTATION_PLATFORMS.has(platform),
644
636
  requiresAtomicAccessPolicy: false
645
637
  };
646
- }).filter(
647
- (p) => p.currencies.length > 0 && (p.platform !== "upi" || environment === "staging" && features.upi === true)
648
- ).sort((a, b) => a.platform.localeCompare(b.platform));
638
+ }).filter((p) => p.currencies.length > 0).sort((a, b) => a.platform.localeCompare(b.platform));
649
639
  const currencies = [...new Set(platforms.flatMap((p) => p.currencies))].sort();
650
640
  const baseUsdc = { address: BASE_USDC_ADDRESS, symbol: "USDC", decimals: USDC_DECIMALS };
651
641
  return {
package/dist/tools.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // package.json
4
4
  var package_default = {
5
- version: "0.5.4-rc.1"};
5
+ version: "0.5.4-rc.2"};
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.5.4-rc.1"};
3
+ version: "0.5.4-rc.2"};
4
4
 
5
5
  // src/tools/index.ts
6
6
  var bigintString = {
@@ -2,15 +2,19 @@
2
2
  import { createCashClient, usdc } from '@zkp2p/cash';
3
3
  import type { WalletClient } from 'viem';
4
4
 
5
- export async function cashOutToUpi(signer: WalletClient, upiId: string) {
5
+ export async function cashOutToUpi(
6
+ signer: WalletClient,
7
+ upiId: string,
8
+ environment: 'staging' | 'preproduction' = 'staging',
9
+ ) {
6
10
  const cash = createCashClient({
7
- environment: 'staging',
11
+ environment,
8
12
  features: { upi: true },
9
13
  });
10
14
 
11
15
  const upi = cash.capabilities().platforms.find(({ platform }) => platform === 'upi');
12
16
  if (!upi?.currencies.includes('INR')) {
13
- throw new Error('UPI is not enabled in this staging SDK build.');
17
+ throw new Error('UPI is not enabled in this environment.');
14
18
  }
15
19
 
16
20
  return cash.cashout(
package/llms.txt CHANGED
@@ -25,7 +25,7 @@ Key facts:
25
25
  then cash out Base-only. Use EXACT_OUTPUT when the order amount must be fixed.
26
26
  - estimate() reports its binding point. Existing corridors bind the on-chain
27
27
  oracle when an intent is signaled; Alipay/CNY fixes a fresh Ethereum
28
- Chainlink snapshot during deposit preparation. ETA is `{ seconds, label }` from the same rolling
28
+ Chainlink snapshot during deposit preparation; UPI/INR uses a fresh Polygon Chainlink snapshot. ETA is `{ seconds, label }` from the same rolling
29
29
  30-day, intent-attributed pair sampler as fillStats(), not a guarantee.
30
30
  - fillStats() returns raw `{ fills, medianFillSeconds? }` evidence keyed by
31
31
  `platform:currency` or a sorted set such as `revolut:EUR+GBP+USD`. Set
@@ -38,11 +38,11 @@ Key facts:
38
38
  fillStats() separately.
39
39
  - capabilities() exposes one Zelle platform. A zelle cashout attaches only the
40
40
  generic Zelle payment method to the on-chain deposit.
41
- - UPI/INR is staging-only and fail-closed. Enable it with
42
- `createCashClient({ environment: 'staging', features: { upi: true } })`.
41
+ - UPI/INR is staging/preproduction-only and fail-closed. Enable it with
42
+ `createCashClient({ environment: 'preproduction', features: { upi: true } })`.
43
43
  Any valid UPI ID from any bank can receive without a seller login,
44
44
  extension, identity attestation, or prior registration flow. Buyers
45
- currently prove HDFC Bank payments through Gmail.
45
+ pay and verify through Amazon Pay using standard UPI.
46
46
  - Resume any order from its depositId alone (composite escrow_onchainId).
47
47
  - One unwind verb: withdraw(depositId) - prunes expired intents automatically;
48
48
  pass amount for a partial withdrawal of the unlocked balance.
@@ -136,4 +136,4 @@ read-only RPC with `upiCreationRateRpcUrl` or `upiCreationRateTransport`.
136
136
  Alipay/CNY retains the Ethereum registry and `creationRateRpcUrl` /
137
137
  `creationRateTransport`. UPI rejects the wrong chain, invalid rounds, and
138
138
  observations older than 24 hours; market closures do not bypass freshness.
139
- This does not change the staging-only UPI opt-in gate.
139
+ UPI remains opt-in on staging/preproduction and disabled in production.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkp2p/cash",
3
- "version": "0.5.4-rc.1",
3
+ "version": "0.5.4-rc.2",
4
4
  "description": "Peer Cash - offramp-only SDK for routing Relay or NEAR Intents assets to Base USDC, then cashing out to fiat at zero-spread Chainlink market rates.",
5
5
  "license": "MIT",
6
6
  "author": "Peer (https://peer.xyz)",
@@ -32,7 +32,7 @@ protocol-held funds and no custodial off-ramp provider.
32
32
  `oracleRateConfig { spreadBps: 0 }`; the binding rate is the Chainlink rate
33
33
  when a buyer signals. Alipay/CNY is the exception: because Base has no CNY
34
34
  oracle adapter, the SDK reads Chainlink CNY/USD on Ethereum and fixes that
35
- fresh snapshot as the maker floor during deposit preparation. Read
35
+ fresh snapshot as the maker floor during deposit preparation. UPI/INR does the same using the direct Polygon INR/USD feed, rejecting wrong-chain, stale and invalid data. Read
36
36
  `estimate().binding` and `capabilities().platforms[].pricing`.
37
37
  - **Custody story.** Funds are held by the protocol contract only. An unmatched
38
38
  deposit is withdrawable by the maker at any time. The SDK never holds keys.
@@ -183,11 +183,11 @@ platforms outright: a previously registered handle can be reused with bare
183
183
  payee data. Handle `PAYEE_VERIFICATION_REQUIRED` when registration is still
184
184
  needed.
185
185
 
186
- UPI is a staging-only opt-in until production contracts support it. Enable it
187
- with `features: { upi: true }` on a staging client. Accept any locally valid
186
+ UPI is a staging/preproduction-only opt-in while production rollout awaits review. Enable it
187
+ with `features: { upi: true }` on a staging or preproduction client. Accept any locally valid
188
188
  UPI ID from any bank; do not add a seller bank-login, extension, identity
189
- attestation, or pre-registration step. Buyers currently prove HDFC Bank UPI
190
- payments through Gmail.
189
+ attestation, or pre-registration step. Buyers pay and verify through Amazon Pay using standard UPI; UPI Lite and
190
+ merchant payments are unsupported.
191
191
 
192
192
  ## 4. Order management - indexer-native
193
193
 
@@ -264,6 +264,8 @@ Prove both routes without waiting for a buyer:
264
264
  If withdrawal fails with funds stuck: stop, do not retry blindly, escalate to
265
265
  a human with the `depositId` and tx hashes.
266
266
 
267
+ ## UPI oracle checks
268
+
267
269
  UPI/INR reads the live Chainlink Polygon mainnet proxy
268
270
  `0xDA0F8Df6F5dB15b346f4B8D1156722027E194E60` (chain 137), inverts
269
271
  USD per INR, and rounds the creation-time maker floor up. Configure its
@@ -271,7 +273,7 @@ read-only RPC with `upiCreationRateRpcUrl` or `upiCreationRateTransport`.
271
273
  Alipay/CNY retains the Ethereum registry and `creationRateRpcUrl` /
272
274
  `creationRateTransport`. UPI rejects the wrong chain, invalid rounds, and
273
275
  observations older than 24 hours; market closures do not bypass freshness.
274
- This does not change the staging-only UPI opt-in gate.
276
+ UPI remains opt-in on staging/preproduction and disabled in production.
275
277
 
276
278
  Before a funded UPI QA run, call
277
279
  `cash.estimate({ amount: 1000000n, platform: 'upi', currency: 'INR' }, { includeEta: false })`
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: upi-preprod-qa
3
+ description: Verify the Cash SDK Amazon Pay UPI corridor on preproduction, including catalog gates, a small maker deposit, indexing, and cleanup.
4
+ ---
5
+
6
+ # UPI preproduction QA
7
+
8
+ Use the exact published Cash candidate and record its SDK/contracts versions.
9
+ Require `createCashClient({ environment: 'preproduction', features: { upi: true } })`
10
+ to advertise UPI/INR. Without the flag, and in production even with the flag,
11
+ UPI must be absent and unsigned preparation must reject before registration or rate reads.
12
+ The canonical contracts catalog must contain the live UPI method and INR currency;
13
+ never invent a catalog entry when the package or registry omits one.
14
+
15
+ From a clean consumer of the candidate, run this catalog check before creating
16
+ any transaction:
17
+
18
+ ```ts
19
+ import { createCashClient } from '@zkp2p/cash';
20
+
21
+ for (const environment of ['staging', 'preproduction', 'production'] as const) {
22
+ for (const upi of [false, true]) {
23
+ const client = createCashClient({ environment, features: { upi } });
24
+ const method = client.capabilities().platforms.find((item) => item.platform === 'upi');
25
+ const expected = upi && environment !== 'production';
26
+ if (Boolean(method?.currencies.includes('INR')) !== expected) {
27
+ throw new Error(`Unexpected UPI catalog for ${environment}, flag=${upi}`);
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Use an explicitly authorized low-value wallet and verified recipient UPI ID.
34
+ Keep the identity and operation ledger in ignored private storage, outside this skill.
35
+ `cashout` accepts any valid VPA; no seller bank login or identity attestation is required.
36
+ Verify INR pricing is a fresh Polygon Chainlink snapshot fixed at deposit creation.
37
+ The official [INR/USD feed](https://data.chain.link/feeds/polygon/mainnet/inr-usd)
38
+ is proxy `0xDA0F8Df6F5dB15b346f4B8D1156722027E194E60` on chain 137;
39
+ it is not registered in Ethereum's Feed Registry. Before funding, call
40
+ `cash.estimate({ amount: 5_000_000n, platform: 'upi', currency: 'INR' }, { includeEta: false })`
41
+ against the live default or explicitly configured Polygon RPC. Confirm a positive
42
+ rate, fresh `oracleUpdatedAt`, and `binding: 'deposit-creation'`. Do not accept
43
+ mocked-rate unit tests as proof of a functioning live corridor. The reader must
44
+ reject the wrong chain, invalid/incomplete rounds, future timestamps and data
45
+ older than 86,400 seconds. Forex market-hour gaps must fail closed; do not
46
+ substitute a static price or relax freshness to make QA pass.
47
+
48
+ With bounded task authorization, create one small Base-USDC deposit, recording
49
+ transaction hash and deposit ID before any retry. Confirm the receipt, then
50
+ `order`/`orders` and the matching preproduction indexer's MethodCurrency and
51
+ QuoteCandidate. Check exact UPI/INR hashes, payee hash, amounts, and rate. Share
52
+ this one fixture with authorized buyer QA instead of funding duplicate deposits.
53
+ Keep quotes within the maker minimum and available liquidity.
54
+
55
+ Use `examples/upi-staging-cashout.ts` with its explicit `preproduction`
56
+ environment argument as the maker API example. Use the supported Cash client
57
+ methods for signing and finalization; never copy an old escrow address or
58
+ transaction payload. For GraphQL, filter `MethodCurrency` and `QuoteCandidate`
59
+ by the recorded `depositId`, `paymentMethodHash`, and `currencyCode`. Require
60
+ exactly one matching tuple, Base USDC, a positive matching conversion rate,
61
+ `isActive: true`, `hasMinLiquidity: true`, and enough available liquidity.
62
+ Inspect `disputeProtectionRequiresStake` before buyer QA so stake admission is
63
+ not mistaken for a payment-proof failure.
64
+
65
+ Buyer QA belongs to the client and attestation repositories. Amazon Pay is the
66
+ sole client flow: no HDFC/Gmail selector or fallback copy. Pay with standard UPI
67
+ to a person, then verify from the same Amazon account. Exclude UPI Lite, merchant
68
+ payments and incoming transactions. Keep session evidence encrypted. A real
69
+ intent requires the exact payment identity, allowed payment-time window, and an
70
+ unused payment nullifier. Report maker deposit, buyer signal, evidence proof,
71
+ and settlement as separate checkpoints. Backend HDFC support is retained until
72
+ a separately authorized retirement; do not remove its server routes here.
73
+
74
+ After buyer intents are cancelled or otherwise terminal, call the supported
75
+ `withdraw` path and verify the receipt and returned order state. If a transaction
76
+ has an unknown result, inspect its receipt and existing deposit before retrying.
77
+ Stop further spending on an unexplained balance or identity mismatch.
78
+
79
+ Run `bun run ci` for SDK changes. Keep private VPA, cookies, raw receipts, keys, and request
80
+ bodies out of committed evidence; publish only redacted checkpoint results.
81
+
82
+ ## Live order reconstruction regression
83
+
84
+ After the exact UPI fixture is indexed, require both `cash.order(depositId)` and `cash.orders(owner)` to return it. Fixed UPI/INR creation-rate deposits must have positive fixed-rate evidence and the indexed `peer-cash` attribution marker. Do not classify unrelated Advanced Sell deposits as Cash orders. A quoteable indexer row alone is insufficient: run order lookup, partial-fill observation and withdrawal checks too. Keep the method/currency pair explicit; UPI/CNY must be rejected.
85
+
86
+ ## Small-fixture visibility and dust reconciliation
87
+
88
+ The web orderbook hides deposits below USD 5 by default. Enable its low-liquidity
89
+ filter before diagnosing an active small fixture as missing. Selector liquidity
90
+ can include a deposit that the orderbook display hides. A verified read-only
91
+ Express `getQuote` accepted INR 10 against a 1 USDC fixture; quoteability still
92
+ depends on the current maker minimum, available liquidity and rate. This does
93
+ not authorize a payment or another fixture, and a quote is not a settlement.
94
+
95
+ The verified contract dust threshold is 0.1 USDC. A remaining amount below that
96
+ threshold may be collected as protocol dust rather than refunded to the maker.
97
+ Reconcile `DustCollected` events and actual token transfers with the successful
98
+ receipt, buyer proceeds, fees and maker balance. Do not interpret Cash
99
+ `returnedAmount` alone as a maker refund. Confirm the deployed threshold again
100
+ if the contract version changes, and retain exact amounts in private receipts.