# @zkp2p/cash

Route Relay-supported EVM assets or NEAR Intents 1Click external deposits into
Base USDC, then cash out to fiat on Venmo, Revolut, Wise, Alipay, Zelle, and
more at a zero-spread Chainlink market rate with no centralized off-ramp
provider. Existing corridors bind the live oracle when a buyer signals;
Alipay/CNY and UPI/INR fix fresh Chainlink snapshots when the SDK prepares
the deposit (Ethereum for CNY; Polygon for INR).

Peer Cash is an **offramp-only** SDK for the [ZKP2P](https://peer.xyz)
protocol. The cashing-out user is the maker: their USDC becomes a deposit in
the protocol contracts, a buyer pays them fiat and proves the payment, and the
SDK gives the integrator a small set of typed verbs plus readable order state.
No hosted widget, no provider custody, no quote engine to maintain.

**[npm](https://www.npmjs.com/package/@zkp2p/cash)** · **[Lifecycle and recovery](docs/lifecycle-and-recovery.md)** · **[Partner integrations](docs/partner-integrations.md)** · **[Agent integration manual](AGENTS.md)**

## Install

```sh
npm install @zkp2p/cash viem
```

## Quickstart

```ts
import { createCashClient, usdc } from '@zkp2p/cash';

const cash = createCashClient({ environment: 'production' });

const est = await cash.estimate({ amount: usdc(1000), currency: 'USD' });
// { rate: 1, receiveAmount: 1000, kind: 'oracle-estimate', eta: { seconds, label } }
// "≈", never a locked quote. Base USDC remains the default source.

// Progressive UI: render rate/receive first, then resolve the exact pair ETA.
const rateOnly = await cash.estimate(
  { amount: usdc(1000), currency: 'USD' },
  { includeEta: false },
);
const fillStats = await cash.fillStats();
const pairStats = fillStats['venmo:USD'];
const multiCurrencyStats = fillStats['revolut:EUR+GBP+USD'];

const { depositId, accessPolicyTxHashes } = await cash.cashout(
  {
    amount: usdc(1000),
    receive: { platform: 'venmo', currency: 'USD', payee: '@you' },
  },
  { signer }, // any viem WalletClient on Base, including an EOA
);
// Venmo and PayPal return only after their access policy confirms.
console.log(depositId, accessPolicyTxHashes);

// One method can offer several currencies. The buyer chooses the fill
// currency, and each option resolves at its own live oracle rate.
const fastFill = await cash.cashout(
  {
    amount: usdc(1000),
    receive: {
      platform: 'revolut',
      currencies: ['EUR', 'GBP', 'USD'],
      payee: { offchainId: 'revtag' },
    },
  },
  { signer },
);

// One order can also offer several platforms (each at most once). The buyer
// picks the leg they can pay. Inspect capabilities().platforms[].pricing for
// the exact rate-binding semantics of each corridor.
const widestReach = await cash.cashout(
  {
    amount: usdc(1000),
    receive: [
      { platform: 'venmo', currency: 'USD', payee: '@you' },
      { platform: 'revolut', currencies: ['EUR', 'GBP'], payee: { offchainId: 'revtag' } },
    ],
  },
  { signer },
);

// Alipay/CNY is the explicit creation-time exception. New Alipay payees need
// the identity attestation prepared by first-party Peer web.
const alipayEstimate = await cash.estimate({
  amount: usdc(1000),
  platform: 'alipay',
  currency: 'CNY',
});
// alipayEstimate.binding === 'deposit-creation'

for await (const order of cash.watch(depositId)) {
  console.log(order.state, order.explain());
  if (order.state === 'delivered') break;
}
```

### Staging and preproduction UPI cash-out

UPI requires the canonical UPI/INR catalog from `@zkp2p/sdk` 0.14.2-rc.1
or its approved successor; a missing catalog entry keeps the corridor disabled.
UPI is available in every environment without an opt-in. Any valid UPI ID from any bank can receive a cash-out. The seller
does not connect a bank account, install an extension, or complete a separate
registration flow:

```ts
const cash = createCashClient({
  environment: 'preproduction',
});

await cash.cashout(
  {
    amount: usdc(25),
    receive: { platform: 'upi', currency: 'INR', payee: 'seller@bank' },
  },
  { signer },
);
```

Buyers pay and verify through Amazon Pay using standard UPI. UPI Lite and
merchant payments are unsupported. The seller may receive at a valid UPI ID
from any bank.

INR pricing uses the [Chainlink Polygon INR/USD feed](https://data.chain.link/feeds/polygon/mainnet/inr-usd),
inverted into INR per USDC and fixed at deposit preparation. The SDK rejects
invalid rounds and readings older than 24 hours, including market-hour gaps.
Only oracle reads use Polygon; funds and transactions stay on Base. Override
the Polygon reader with `upiCreationRateTransport` or `upiCreationRateRpcUrl`.
The existing `creationRateTransport`/`creationRateRpcUrl` options remain Ethereum-only for CNY.

## Pick the right SDK

Peer Cash and the general ZKP2P SDK serve different integration depths:

| Package       | Use it when                                       | Boundary                                                                                                                                                                                                                       |
| ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `@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. |
| `@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.                                      |

Peer Cash is a narrow facade over `@zkp2p/sdk`, not a replacement for it. It
cannot express custom spreads, buyer-side proof flows, vaults, disputes, or
arbitrary protocol operations.

## The core verbs

| Verb                                                           | What it does                                                                                                                    |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `prepareVenmoGmailConnect(payee)`                              | Optional Venmo handle registration; returns the payee hash and hosted URL                                                       |
| `openVenmoGmailConnect(payeeDetails)`                          | Optional browser popup/tab; call directly from a click                                                                          |
| `isVenmoGmailConnected(payeeDetails)`                          | Read whether an active Google receipt credential exists                                                                         |
| `capabilities()`                                               | Sync discovery: Base USDC destination/default source, platforms × currencies × payee hints × amount bounds                      |
| `capabilities({ includeRelaySources: true })`                  | Async discovery: adds live Relay SDK EVM source chains/tokens                                                                   |
| `capabilities({ includeNearIntentsSources: true })`            | Async discovery: adds live NEAR Intents 1Click source assets                                                                    |
| `fillStats()`                                                  | Cached 30-day fill counts and median first-fill time per exact `platform:currency` pair or sorted multi-currency set            |
| `quoteSource(input)` / `executeSourceQuote(quote, { signer })` | Relay SDK EVM source routing into Base USDC before cashout                                                                      |
| `relayStatus(requestId)`                                       | Relay request status from the Relay SDK request path                                                                            |
| `quoteNearIntentsSource(input)`                                | Signed 1Click quote with an origin-chain deposit address and optional memo                                                      |
| `submitNearIntentsDeposit(input)` / `nearIntentsStatus(input)` | Optionally register an origin tx, then track 1Click delivery/refund evidence                                                    |
| `estimate({ amount, currency, platform? }, { includeEta? })`   | Base USDC market-rate estimate with an explicit `binding`; optionally skip historical ETA                                       |
| `cashout(input, { signer })`                                   | Creates the order with any viem wallet; restricted methods then attach the Peer Pay merchant policy                             |
| `prepare(input)` / `finalizePreparedCashout(receipt)`          | Prepare external signing, resolve the deposit, then iterate `accessPolicyPaymentMethods` for follow-ups                         |
| `prepareAccessPolicy(depositId, paymentMethod)`                | Prepare one post-deposit, method-scoped Peer Pay merchant policy transaction                                                    |
| `order(depositId)` / `orders(owner)`                           | Resume any order from its id alone; list all orders for a wallet                                                                |
| `watch(depositId)`                                             | Async iterator: yields on every state change until terminal, abort, or timeout                                                  |
| `withdraw(depositId, { signer, amount? })`                     | The ONE unwind verb - partial with an `amount` (live intents don't block it), full close without (prunes expired intents first) |
| `topUp(depositId, amount, { signer })`                         | Add USDC to a live order - same payee, same market rate                                                                         |
| `buyer(address)`                                               | A buyer's track record from their intent history - who just matched your order?                                                 |

Base-USDC cashout, withdraw, and top-up have unsigned counterparts (`prepare`,
`prepareWithdraw`, `prepareTopUp`). The unsigned path returns raw `txs[]` plus
a same-index `steps[]` plan such as `approve`, `createDeposit`, or
`withdrawDeposit`, so wallets, AA systems, and agents can show what each
transaction does before signing. `prepare()` is Base-USDC-only and rejects a
`source` with `SOURCE_ROUTE_UNSUPPORTED_IN_PREPARE`. A signer-backed app can
use `cashout({ source }, { signer, sourceSigner })`; a custody-separated host
must execute and confirm its Relay route before preparing the Base-USDC
cashout. After externally executing a prepared `createDeposit`, pass its
confirmed receipt to `finalizePreparedCashout()` to recover the same
`CashoutResult` shape as `cashout()` without importing protocol ABIs. Every
Peer Cash transaction, including approves, carries ERC-8021 attribution:
`peer-cash` first, optional `peer-ref-XXXXXX` from `referralCode` next, and your
analytics-only `referrer` code(s) after it.

Order reads fail closed against the same active catalog. If any method on an
indexed deposit is unsupported, `orders()` excludes the whole deposit and
`order()` returns `ORDER_NOT_FOUND`; Peer Cash never partially reclassifies a
mixed historical deposit.

## Optional Venmo receipt linking

Partners such as Tailgate can offer **Link Venmo** separately from cash-out.
`cashout()` and `prepare()` never open Gmail, check receipt credentials, or
require this step. Skipping or cancelling linking leaves cash-out available.

```ts
import { createCashClient, VenmoGmailConnectError } from '@zkp2p/cash';

const cash = createCashClient({ environment: 'production' });
// Run when the user selects their Venmo account, before enabling Link Venmo.
const link = await cash.prepareVenmoGmailConnect('@yourhandle');

// Call directly from the button click, with no async work before this call.
linkButton.onclick = () => {
  cash.openVenmoGmailConnect(link.payeeDetails).catch((error: unknown) => {
    if (error instanceof VenmoGmailConnectError) {
      console.log(error.code, error.message);
    }
  });
};

// After completion, returning from a tab, or an ambiguous popup closure:
const connected = await cash.isVenmoGmailConnected(link.payeeDetails);
```

The handle is registered with the environment's Curator and its authoritative
payee hash goes to the hosted link. Re-prepare if the selected handle changes.
Persist the hash with the environment and user account for later status checks.
`link.url` also works as a normal link for redirect or native-browser hosts;
those hosts must check status after the user returns.

Production uses `app.peer.xyz`, preproduction `ramp-preprod.peer.xyz`, and
staging `ramp-staging.peer.xyz`. `peerOrigin` and `curatorUrl` allow explicit
overrides. Browser helpers require no React, extension, wallet, or signer.
Preparation and status reads also work on the server.

Desktop opens a popup; mobile requests a new browser tab (including Safari).
The browser controls presentation and may sever the opener connection. In that
case check status rather than assuming `connection_closed` or
`connection_timeout` means verification failed. Status is true only for an
active `google_oauth` credential; lookup failures reject.

Gmail and Google-hosted school/custom-domain inboxes are accepted. The hosted
flow verifies actual receipts; there is no email-domain precheck. A missing
receipt reports `venmo_google_oauth_receipt_not_found` through
`VenmoGmailConnectError.code`. Cancellation, popup blocking, and other hosted
errors retain their codes. This error class is separate from `CashError`;
registration and status service errors also reject. Emails and Google tokens
never pass through the partner app or Cash SDK.

See [the browser example](examples/venmo-link.ts). JSON codecs are exported for
`PreparedVenmoGmailConnect` and `VenmoGmailConnectResult`.

## Payout rails and access policies

| Payout rail           | Access-policy behavior                                                     | New payee registration                                                                 |
| --------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Venmo                 | Peer Pay merchant policy attaches for that payment method                  | Curator validates the live handle                                                      |
| PayPal                | Same method-scoped Peer Pay follow-up                                      | Requires a Peer TEE browser-extension identity attestation                             |
| Cash App              | No access-policy follow-up; non-chargebackable and no stake required       | Curator validates the live handle                                                      |
| Wise                  | No access-policy follow-up                                                 | Requires a Peer TEE browser-extension identity attestation                             |
| UPI                   | No access-policy follow-up                                                 | Any valid UPI ID; no account connection or identity attestation                        |
| 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 |

No platform requires an atomic access-policy flow. `cashout()` and `prepare()`
work with any viem `WalletClient`, including a local or externally connected
EOA; no Privy wallet or signer API is required. The deprecated
`requiresAtomicAccessPolicy` capability remains for wire compatibility and is
always `false`.

Venmo and PayPal cash-outs restrict intent signaling to the Peer Pay
merchant group by default. Each restricted payout method gets its own policy.
Signed `cashout()` creates the deposit first, then uses the same wallet to
submit and confirm every required policy transaction; this intentionally
leaves a brief interval where any taker can signal. Method-scoped dispute
protection is already default-on for these rails, so Cash neither
readiness-gates deposit creation nor submits
`setDisputeProtectionEnabled(true)`. Prepared integrations receive
`accessPolicyPaymentMethods`; after confirming `createDeposit`, call
`finalizePreparedCashout(receipt)`, then submit
`prepareAccessPolicy(depositId, paymentMethod)` once for every returned method.
Other platforms do not need the follow-up.

If policy attachment fails, `ACCESS_POLICY_CONFIGURATION_FAILED.recovery`
identifies the existing deposit and any submitted policy transaction. Never
create another cash-out. When `recovery.transactionHash` is present, inspect
that transaction before resubmitting; otherwise prepare the policy again with
the same depositor wallet.

`capabilities()` presents Zelle as one platform. A cashout with
`receive.platform: 'zelle'` attaches only the generic Zelle payment method to
the deposit. Bank-specific capture routing is outside this maker-side SDK and
never changes the on-chain payment method.

`capabilities()` tells you which platforms need a verified identity for a new
payee registration (`requiresIdentityAttestation` - Wise and PayPal today).
The SDK accepts an `identityAttestation` in structured payee data but does not
mint one. First-party Peer web obtains it through the Peer TEE browser
extension. An already-registered Wise or PayPal handle can be reused with bare
payee data. A new handle without its signed attestation fails during curator
registration with `PAYEE_VERIFICATION_REQUIRED`, before funds move on-chain.

## Source routing

### Relay (signed EVM route)

The default/minimal flow is unchanged: pass Base USDC base units to
`estimate()` and `cashout()`. For any other source asset, pass `source` to
`cashout()` with a source-chain signer. The SDK settles the Base allowance,
executes the Relay route into Base USDC, then creates the Peer Cash order. Use
`EXACT_INPUT` in cash-out UIs so `amount` always means source-token base units.
The destination is always canonical Base USDC
(`8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913`); source support is
discovered and quoted by `@relayprotocol/relay-sdk`, not a static token
allowlist.

```ts
const { depositId, accessPolicyTxHashes, source } = await cash.cashout(
  {
    amount: 10_000_000n, // exact input: 10 USDC in source-token base units
    source: {
      chainId: 1,
      currency: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
      tradeType: 'EXACT_INPUT',
    },
    receive: { platform: 'venmo', currency: 'USD', payee: { offchainId: '@you' } },
  },
  { signer, sourceSigner },
);

// source.amount is Relay's guaranteed minimum Base USDC output and the exact
// amount deposited into the cash-out order. It is not the route's actual output.
console.log(source?.amount, source?.requestId);
console.log(source?.transactions?.origin, source?.transactions?.destination);
console.log(accessPolicyTxHashes); // one entry because this example uses Venmo
```

Routes that submit more than one source-chain transaction (approve, then
route) require a nonce-managed source signer -
`privateKeyToAccount(pk, { nonceManager })` from viem. Without one the SDK
refuses the route preflight with `SOURCE_NONCE_MANAGER_REQUIRED` instead of
letting the route transaction reuse the approval's nonce and revert
mid-route. Browser wallets are unaffected.

### NEAR Intents (external-deposit route)

NEAR Intents 1Click supports non-EVM origins such as Zcash, so the SDK does
not pretend a viem wallet can execute the source transfer. It returns a signed
quote with an origin-chain `depositAddress` and optional `depositMemo`; your
wallet sends exactly once, then the SDK tracks the provider route into
canonical Base USDC. Use `EXACT_OUTPUT` when the Peer order amount must be
known before the origin send.

```ts
const cash = createCashClient({
  environment: 'production',
  nearIntents: {
    // Browser-safe same-origin proxy; it keeps the 1Click JWT server-side.
    apiUrl: '/api/v1/near',
    transport: 'proxy',
  },
});

const sources = await cash.capabilities({ includeNearIntentsSources: true });
const zec = sources.source.nearIntents?.assets.find((asset) => asset.symbol === 'ZEC');
const quote = await cash.quoteNearIntentsSource({
  sourceAsset: zec!.assetId,
  amount: 1_000_000n, // exact 1 Base USDC output
  recipient: baseSigner.account.address,
  refundTo: transparentZcashRefundAddress,
  tradeType: 'EXACT_OUTPUT',
  deadline: new Date(Date.now() + 3 * 60_000).toISOString(),
});

persist(quote); // before sending: address, memo, signed response, deadline
const originTxHash = await zcashWallet.send(quote.depositAddress!, quote.inputAmount);
await cash.submitNearIntentsDeposit({
  depositAddress: quote.depositAddress!,
  ...(quote.depositMemo ? { depositMemo: quote.depositMemo } : {}),
  txHash: originTxHash,
});
const route = await cash.nearIntentsStatus({
  depositAddress: quote.depositAddress!,
  ...(quote.depositMemo ? { depositMemo: quote.depositMemo } : {}),
  expectedQuote: quote,
});
// On SUCCESS, reconcile the Base receipt/balance, then call Base-only cashout().
```

Direct server integrations may pass `nearIntents: { token }`. Browser code
must use a same-origin proxy and must never receive the 1Click JWT. Never reuse
an expired deposit address, resend funds after an uncertain wallet submission,
or infer success from a wallet-wide balance alone. If optional deposit
registration fails, retry only `submitNearIntentsDeposit()` with the same hash;
1Click can also detect the transfer on-chain.

## Source-route recovery

Persist `depositId`, transaction hashes, and the Relay `requestId` as soon as
they are available. A source-routed result includes both a flat
`source.txHashes` list and chain-aware `source.transactions.origin` /
`.destination` entries.

- `SOURCE_EXECUTION_FAILED` where only the approval landed: the Relay request
  can stay in `relayStatus` `waiting` indefinitely. Decide from the error's
  recovery payload and origin transactions, never by waiting for a terminal
  Relay status.
- `SOURCE_DEPOSIT_SUBMISSION_FAILED`: the NEAR Intents origin transaction may
  already be final. Retry only the provider notification with the same address
  and hash; never resend source funds.
- `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED`: Relay completed, but the Base
  cashout was not created. Do not route again. Retry a Base-USDC-only
  `cashout()` with `BigInt(error.recovery.amount)`.
- `SOURCE_CASHOUT_SUBMISSION_UNKNOWN`: Relay completed, but Base submission
  returned no transaction hash. Inspect recent Base wallet activity and
  `orders(error.recovery.depositor)` to prove no deposit exists before retrying.
- `SOURCE_CASHOUT_STATUS_UNKNOWN`: the Base cashout transaction was submitted,
  but its receipt is unknown. Do not route or submit again. Inspect
  `error.recovery.depositTxHash`; recover the `depositId` from its
  `DepositReceived` log if it succeeded, or use the recovery amount for a
  Base-USDC-only retry only after confirming it reverted.
- `TRANSACTION_SUBMISSION_UNKNOWN`: a Base-only cashout or another mutation
  returned no hash. Treat it as potentially broadcast. Inspect recent Base
  wallet activity and the supplied recovery action before any retry.
- `ACCESS_POLICY_CONFIGURATION_FAILED`: the deposit exists, but its required
  Venmo or PayPal policy was not confirmed. Do not cash out again;
  inspect `recovery.transactionHash` when present, then retry
  `prepareAccessPolicy(error.recovery.depositId, error.recovery.paymentMethod)`
  only if the prior policy
  transaction did not succeed.

Wallet clients pinned to the wrong chain fail with `SIGNER_CHAIN_MISMATCH`
before a quote or transaction is submitted. Chainless wallets are checked
through `getChainId()`; a disconnected wallet returns retryable
`SIGNER_CHAIN_UNAVAILABLE`. Indexer and oracle transport outages are typed as
retryable `INDEXER_UNAVAILABLE` and `ORACLE_READ_FAILED` reads; retry the read
without repeating any transaction. `TRANSACTION_STATUS_UNKNOWN` carries the
submitted hash in `error.recovery.transactionHash` so recovery never depends
on parsing an error message.

## Lifecycle

```
            buyer signals            fiat proven
awaiting-buyer ──────────► matched ──────────► delivered
      │                       │    (partial fills pass through "delivering")
      │ withdraw()            │ buyer never pays → intent expires
      ▼                       ▼ withdraw() prunes + returns funds
   returned ◄─────────────────┘
```

- **You are the maker.** Pricing is zero-spread. Existing corridors resolve
  from the on-chain Chainlink oracle when a buyer signals an intent.
- **Alipay/CNY binds earlier.** Base has no CNY oracle adapter, so the SDK reads
  Chainlink CNY/USD on Ethereum, rejects stale or invalid data, and fixes the
  resulting CNY-per-USDC maker floor when it prepares the deposit. A buyer may
  signal at that floor or a better rate for the maker.
- **Read `binding`.** `estimate().binding` is `intent-signal` by default and
  `deposit-creation` for Alipay/CNY and UPI/INR. An estimate remains approximate until its
  stated binding point.
- **ETA is historical.** `estimate().eta` is just `{ seconds, label }`, backed
  by the same rolling 30-day, intent-attributed pair sampler as `fillStats()`,
  measured from deposit creation to the first fulfilled fill through the pair.
  The raw snapshot is cached for 15 minutes per client and each ETA is still
  resolved from its exact normalized `platform:currency` key. Multi-currency
  deposits also produce sorted keys such as `revolut:EUR+GBP+USD`, measured to
  the first fill in any offered currency. Use
  `{ includeEta: false }` when rate and receive amount should render first.
- **Availability thresholds belong to the consumer.** `fillStats()` returns raw
  evidence. A recommended gate is `fills >= 10 && medianFillSeconds <= 48h`.
  Fail open to the full `capabilities()` catalog when stats are unavailable or
  the gate would empty the offered catalog.
- **Everything is resumable.** An order is reconstructed from the chain by
  `depositId` alone. Close the tab, switch devices, crash the process - then
  call `order(depositId)`.
- **Unwind is one verb.** Buyer never paid? Their intent expires; `withdraw()`
  prunes it and returns your USDC. You never choose between cancel and recover.

Deep dive: [docs/lifecycle-and-recovery.md](docs/lifecycle-and-recovery.md).

## Earn the integration share

Use the same six-character referral code shown in your Peer mobile or web app.
No API key, registration transaction, or separate receiving address is needed:
the referral code already belongs to your Peer Privy wallet.

```ts
const cash = createCashClient({
  environment: 'production',
  referralCode: 'ABC123',
});
```

The SDK normalizes the value and stamps `peer-ref-ABC123` into ERC-8021
attribution on the deposit transaction. When that liquidity is filled, Curator
pays the code owner 50 bps, capped by the configured Peer service fee. This is
the deposit-level integration path: it replaces the maker L1/L2 referral split
for that deposit instead of enrolling the cashing-out user as your referee.

The mapping is permanent. If you later customize your displayed Peer referral
code, open deposits carrying the old code still pay the same wallet. Include at
most one `peer-ref-XXXXXX` marker; an unknown or conflicting marker receives no
integration share. The existing `referrer` option remains available for
analytics-only ERC-8021 codes such as `acme-app`.

## For agents

- `cashout`/`withdraw`/`topUp` have unsigned counterparts (`prepare`,
  `prepareWithdraw`, `prepareTopUp`) - inspect readable `steps[]` and calldata
  before signing, then submit the matching `txs[]` in order.
- Mutating tool calls return unsigned transactions by default; signing stays
  with the host that owns custody, policy, and user approval.
- After a prepared restricted cash-out confirms, the host adapter must call
  `finalizePreparedCashout(receipt)` and submit
  `prepareAccessPolicy(depositId, paymentMethod)` for every value in
  `accessPolicyPaymentMethods`; these receipt/signing operations are
  `CashClient` methods, not built-in tool calls.
- Every error carries `code`, `retryable`, and a `remediation` sentence.
- Every order carries `nextActions: ('wait' | 'withdraw')[]` - no heuristics.
- Every wire type has a zod schema + JSON codec - state crosses process
  boundaries losslessly.
- Everything arrives decoded: platform ids and currency codes instead of
  bytes32 hashes, plain-number rates instead of 1e18 bigints.
- Fills are receipts: the locked rate and fiat owed at signal, then the
  verified fiat paid, currency, platform payment id, released USDC, and
  fill latency once the proof lands.
- `@zkp2p/cash/tools` exports a JSON-schema tool manifest of the verbs.

Start at [AGENTS.md](AGENTS.md), or load the
[`peer-cash-integration` skill](skills/peer-cash-integration/SKILL.md).

## React

```ts
import { useEstimate, useCashout, useOrder, useOrders } from '@zkp2p/cash/react';
```

React is an optional peer dependency - the root entry never imports it.

## Environments

`production` | `preproduction` | `staging` selects contracts, curator, and
indexer. Preproduction defaults to `https://api-preprod.zkp2p.xyz`; staging
defaults to `https://api-staging.zkp2p.xyz`. Indexer, curator, and Relay
options remain overridable via `createCashClient` options. Base USDC on Base
is the default source and the only destination asset for cashout orders.

## Examples

Runnable first-party examples in [`examples/`](examples):

- [`node-cashout.ts`](examples/node-cashout.ts) - server-side cash-out with a private-key signer, plus order tracking.
- [`upi-staging-cashout.ts`](examples/upi-staging-cashout.ts) - UPI/INR cash-out to any valid UPI ID on staging or preproduction.
- [`agent-tool-use.ts`](examples/agent-tool-use.ts) - wiring the verbs into an agent tool-use loop with host-side signing.
- [`carpe-diem-provider-cashout`](examples/carpe-diem-provider-cashout) - cash out confirmed Carpe Diem provider DIEM revenue through the connected Base wallet.
- [`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.
- [`onchain-demo`](examples/onchain-demo) - the Peer Cash Demo: the express sell flow as one page that bundles the SDK and is stored on Base as contract bytecode, served by an immutable ERC-5219 wrapper,
  [live on Base](https://basescan.org/address/0x6d6c7af86bfc6f49f32761e1718cf982224cf343).

## Trust model, honestly

The published package depends on `@zkp2p/sdk` for protocol internals; that
dependency currently ships from private source. Onchain custody is enforced by
the protocol: only the contract holds funds, and only the maker can withdraw
an unmatched deposit.

## Contributing

[CLAUDE.md](https://github.com/zkp2p/peer-cash/blob/main/CLAUDE.md) is the
contributor guide: ground rules, repo layout, the `bun run ci` gate, and the
release/publish process. `AGENTS.md` is the shipped manual for agents using
the package, not the contributor entry point.

## License

MIT
