Automatic fixed fills: upcoming breaking change
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 50 | One payment for the full total |
| 600 | 2 × 300 |
| 700 | 2 × 350 |
| 750 | 3 × 250 |
| 800 | 2 × 400 |
| 900 | 3 × 300 |
| 1,000–10,000, in steps of 500 | 2–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:
| Code | Meaning |
|---|---|
FIXED_AMOUNT_NOT_PRESET | Select a total from recovery.allowedAmounts, encoded as USDC base-unit strings. |
FIXED_AMOUNT_UNSPLITTABLE | A custom increment cannot split the total within the chunk and payment-count limits. |
INVALID_MIN_CHUNK_SIZE | The increment is not a whole-USDC value from 1 through 500. |
FIXED_CURRENCY_UNSUPPORTED | Every fixed payout leg must offer USD only. |
INVALID_FILL_CONFIGURATION | Unknown 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.