Edge Cases & Errors

Edge Cases & Errors

When building on PayStreamer, it's important to understand how the smart contracts handle edge cases around user funds and subscription state, and what to do when a transaction aborts.

Insufficient Funds

The most common edge case: a user's account balance can't cover the next billing cycle.

process_due_payment handles this without aborting the transaction — a single insufficient-balance attempt does not cancel or pause anything by itself:

  1. The scheduler triggers process_due_payment because the subscription is due.
  2. Balance check: the contract checks account::balance(account) < amount.
  3. Graceful failure, not a revert: if the balance is too low, the contract calls record_failed_payment and returns — it does not abort. This matters because an abort would waste the scheduler's gas and could loop; a graceful return lets the scheduler move on and still collect its incentive for the attempt.
  4. Three strikes, then pause: record_failed_payment increments the subscription's attempt_count. Only once attempt_count >= 3 does the contract set status = 1 (paused, not cancelled) — subscription.move's record_failed_payment. Each failed attempt emits a PaymentFailed event (payment.move) you can subscribe to via queryPaymentFailedEvents.
⚠️

Paused ≠ cancelled. A subscription auto-paused by 3 failed attempts is status == 1, not status == 2 (cancelled). Once the user tops up their balance, resume it with buildResumeSubscriptionTx — there's no need to create a new subscription. A brand-new create_subscription call is only required if the subscription was explicitly cancelled (status == 2), which auto-pause never does on its own.

Handling on the Frontend

Listen for PaymentFailed events (or poll attempt_count/status on the account) to show the right UI — a warning after 1-2 failures, a "resume after topping up" prompt once paused:

import { queryPaymentFailedEvents } from "@paystreamer/sdk/core";
 
const failures = await queryPaymentFailedEvents(accountId, platformId, "testnet");
if (failures.length > 0) {
  console.log(`${failures.length} failed payment attempt(s) recorded.`);
}

Over-depositing

Depositing more than one billing cycle's worth is fine and common (the SDK's own recommended-deposit calculation in useSubscribe/SetupSubscriptionModal targets several cycles of buffer by default). The excess belongs entirely to the user — the platform can never withdraw more than the tier amount per cycle, and the user can withdraw their own excess at any time via buildWithdrawTx, gated by their AccountCap.

Security model: access to an account's funds is gated by Sui's object capability model — the AccountCap returned when an account is created. There's no separate "deposit capability"; anyone can deposit into an account (deposits don't need authorization), but only the AccountCap holder can withdraw.

Error Reference

Every Move abort surfaces as a numeric code in the transaction error (e.g. MoveAbort ... abort code: 32769). Sui reports these in decimal; the contracts define them in hex — the table below has both. All codes verified directly against the current contract source, not assumed.

account.move

DecimalHexCodeMeaning
40970x01001EInvalidCapThe AccountCap doesn't match the account it's presented against (wrong account, not just unauthorized).
40990x01003EAccountClosedThe account is closed.
41000x01004EZeroAmountA zero-amount deposit was attempted.
41010x01005EInsufficientBalancewithdraw for more than the account's live balance.
41060x0100AEAccountNotPausedresume_subscription-style call on an account that isn't paused.
245780x06002ESubscriptionNotFoundReserved — not currently asserted anywhere in the contract.
245790x06003ESubscriptionAlreadyExistsTried to create a subscription for a platform the account already has a non-cancelled subscription with.
245820x06006EAccountPausedThe account itself (not a specific subscription) is paused.

subscription.move

DecimalHexCodeMeaning
245800x06004ESubscriptionNotActiveThe operation requires an active subscription — it's paused or cancelled.
245810x06005ESubscriptionNotPausedThe operation requires status == 1 (paused), but it isn't.

payment.move

DecimalHexCodeMeaning
368650x09001ENotDuecan_bill returned false — not active, or not yet time to bill.
368660x09002EInvalidAmountInvalid payment amount.
368670x09003EInsufficientBalanceBalance check failed inside the payment path itself.
368690x09005EPolicyViolationThe account's own policy limiters rejected this payment (per-tx max, monthly max, min balance, or frequency).
368700x09006EZeroAmountZero-amount payment — a programmer/configuration error, aborts before any funds move.
368710x09007EInvalidPotatoThe RoutingPotato presented to process_routed_payment doesn't match this payment settlement — see Advanced Routing.

platform.move

DecimalHexCodeMeaning
327690x08001EInvalidOwnerCaller isn't platform.owner — every owner-gated function (tier management, treasury changes) aborts with this on mismatch.
327700x08002EInvalidTierTier validation failed (e.g. duplicate tier name in create_tier).
327710x08003ETooManyTierscreate_tier would exceed the 20-tier cap (MAX_TIERS).
327720x08004ETierNotFoundtier_index is out of range for this platform.
327730x08005EZeroAddressA zero address was supplied where a real one is required (e.g. a treasury change target).
327740x08006EInvalidAmountZero tier billing amount.
327750x08007EInvalidFrequencyZero frequency_ms for a tier.
327760x08008ETreasuryChangeAlreadyPendingpropose_treasury_change called while another change is already pending — cancel it first.
327770x08009ENoPendingTreasuryChangeaccept/cancel_treasury_change called with no pending change on the platform.
327780x0800AETreasuryChangeNotYetDueaccept_treasury_change called before the 48-hour timelock elapsed.
327790x0800BEInvalidReceiptThe PlatformRegistrationReceipt doesn't match the Platform being registered — see Register Your Platform.

policies.move

Self-imposed spending limits a user can set on their own account (per_tx_max, monthly_max, min_balance, frequency_min_ms) — these protect the user, not the platform, and only trigger if the user configured them.

DecimalHexCodeMeaning
286730x07001EPerTxExceededPayment exceeds the account's per-transaction limit.
286740x07002EMonthlyExceededPayment would exceed the account's rolling 30-day spending cap.
286750x07003EMinBalanceViolatedPayment would drop the account below its configured minimum balance.
286760x07004EFrequencyViolatedPayment attempted before the account's configured minimum interval.
286770x07005EInvalidLimitAn invalid (e.g. contradictory) limit configuration was supplied.