Skip to main content

Automatic fixed fills: upcoming breaking change

Unreleased

This documents Peer Cash PR #117. The published @zkp2p/cash@0.7.3 package does not expose fillMode. This change requires a new minor release; use the examples below only with a version containing that change. This page does not announce a release.

What changes​

In 0.7.3, cash-outs without an explicit intentAmountRange accept arbitrary totals from 0.01 USDC. The default minimum is 1 USDC or the whole amount when smaller, and the maximum is the full amount.

After upgrading, requests without fillMode or intentAmountRange default to automatic fixed sizing when every offered payout leg is USD Venmo, PayPal, or Cash App. One deposit holds the full total. Every buyer intent must equal the selected chunk: min = max.

Accepted total (USDC)Automatic payments (USDC)
50–500, in steps of 50One payment for the full total
6002 × 300
7002 × 350
7503 × 250
8002 × 400
9003 × 300
1,000–10,000, in steps of 5002–20 × 500

With the default 50-USDC increment, every other total rejects before registration or funding. For example, 950 USDC previously allowed payments from 1 to 950 USDC; it now raises FIXED_AMOUNT_NOT_PRESET. So do 5, 550, 650, 850, 1,200, 1,250, fractional totals, and values above 10,000. There is no rounding or smaller-chunk fallback. These are product presets, not payment-platform account limits.

Preserve arbitrary amounts​

After upgrading, pass fillMode: "flexible" on previews and submission:

import { usdc } from "@zkp2p/cash";

const amount = usdc(950);
const fillMode = "flexible" as const;

const estimate = await cash.estimate({
amount,
currency: "USD",
platform: "venmo",
fillMode,
});

const result = await cash.cashout(
{
amount,
fillMode,
receive: { platform: "venmo", currency: "USD", payee: "@recipient" },
},
{ signer },
);

Pass the same mode to prepare() for external signing or to useEstimate() for a React preview. Existing explicit ranges remain flexible overrides when mode is omitted. Other rails, non-USD PayPal, and mixed payout sets containing another rail retain flexible defaults. Existing deposits keep their bounds.

Adopt fixed presets​

Offer approved totals and preview with the selected platform. For example:

const estimate = await cash.estimate(
{ amount: usdc(900), currency: "USD", platform: "venmo" },
{ includeEta: false },
);
// estimate.intentAmountRange = { min: 300_000_000n, max: 300_000_000n }

prepare() returns the same bounds, and cashout() returns them on order. Resumed orders read current indexed bounds; older serialized results can lack this field. JSON codecs use decimal base-unit strings. Planned payment count is the gross Base-USDC total divided by the equal bound, not a separate input.

Explicit fillMode: "fixed" selects the same presets on any supported USD-only payout set. Non-USD legs reject. Fixed mode has no manual ticket-size option and cannot be combined with a raw range.

Custom minimum chunk and increment​

To choose a smaller denomination while leaving sizing automatic, pass minChunkSize in USDC base units:

const fill = { fillMode: "fixed" as const, minChunkSize: usdc(25) };
const amount = usdc(950);
const estimate = await cash.estimate(
{ amount, currency: "USD", platform: "venmo", ...fill },
{ includeEta: false },
);
// intentAmountRange: { min: 475_000_000n, max: 475_000_000n }
await cash.cashout(
{
amount,
receive: { platform: "venmo", currency: "USD", payee: "@recipient" },
...fill,
},
{ signer },
);

The increment is also the minimum chunk: 25 permits chunks of 25, 50, 75, 100, and other multiples of 25 through 500. It does not select the actual ticket. Omitting the increment or passing usdc(50) keeps the original preset table unchanged, including 900 → 3 × 300 and rejection of 650/950.

For another increment, the SDK chooses the fewest equal payments whose chunk is a multiple of that increment and no greater than 500 USDC. The increment must be a whole-USDC value from 1 through 500; the total must be at least that value and at most 10,000. Plans may have at most 3 payments below 1,000 USDC and at most 20 overall. No exact plan means rejection, without rounding.

With increment 25, 650 → 2 × 325, 900 → 2 × 450, and 950 → 2 × 475. 575 rejects because its only eligible split requires 23 payments. Use identical fill options in estimate() / useEstimate(), prepare() and cashout(). minChunkSize requires explicit fillMode: "fixed" and is a decimal base-unit string in JSON codecs and tools.

capabilities().amount remains the general product floor, not the fixed allowlist. A currency-only estimate does not validate a platform default. For mixed payout sets, explicitly choose the preview mode that matches the whole set; a single-platform estimate does not describe every leg.

Update types and error handling​

CashoutInput, EstimateInput, and UseEstimateOptions now include a discriminated union. Replace interface inheritance with an intersection:

type AppCashout = CashoutInput & { requestId: string };

Handle the new error codes:

CodeMeaning
FIXED_AMOUNT_NOT_PRESETSelect a total from recovery.allowedAmounts, encoded as USDC base-unit strings.
FIXED_AMOUNT_UNSPLITTABLEA custom increment cannot split the total within the chunk and payment-count limits.
INVALID_MIN_CHUNK_SIZEThe increment is not a whole-USDC value from 1 through 500.
FIXED_CURRENCY_UNSUPPORTEDEvery fixed payout leg must offer USD only.
INVALID_FILL_CONFIGURATIONUnknown mode, fixed plus raw bounds, a chunk increment without explicit fixed mode, or an attempted intentAmount override.

Exhaustive recovery handlers must support kind: "select-fixed-amount". Narrow recovery.kind before accessing source-route fields: the new variant contains only allowedAmounts.

Funding and full completion​

Relay amount remains source-token units. Fixed eligibility is checked against the quote's guaranteed Base-USDC output before registration, approvals, or route execution. Exact-input outputs often are not presets; use explicit flexible mode when cashing out arbitrary source amounts. A fresh quote can invalidate an earlier preview. Never repeat a completed source transfer to repair sizing. NEAR Intents funding stays external: reconcile the Base receipt before the Base-only cash-out.

Top-ups, partial withdrawals, manual releases, or external range changes can invalidate the original payment count. The SDK does not relax bounds or repair the balance automatically.

The existing ETA measures first fill, not full completion. Use includeEta: false on a full-completion preview. Strict full settlement requires actual FULFILLED principal to cover the funded amount with no residual, pending, or returned principal; state === "delivered" and deliveredAt alone are insufficient. Report manual completion separately. Twenty planned payments does not imply a completion-time guarantee.