Skip to content

Renewal Lifecycle

A subscription isn’t a one-time purchase; it’s a living thing with a clock. Smart Engines keeps that clock: it watches expiries, applies tier changes at the right moment, and expires a subscription cleanly when the conditions stop holding. Renewing is your step: you run hsuite renew (or call the renew route) before the lock lapses.


The status field walks through these values: pending_deposit → deposit_confirmed → active, with expired and cancelled as terminal-ish ends. Every tier is deposit-backed, so every subscription walks the full path — there is no free tier that skips the deposit.

stateDiagram-v2
    [*] --> PENDING_DEPOSIT: request (any tier — all paid)
    PENDING_DEPOSIT --> DEPOSIT_CONFIRMED: HSUITE deposit verified
    PENDING_DEPOSIT --> CANCELLED: discard (never paid)
    DEPOSIT_CONFIRMED --> ACTIVE: activate + mint NFT
    ACTIVE --> ACTIVE: renew (extend lock)
    ACTIVE --> ACTIVE: top-up applied (float re-funded, same tier)
    ACTIVE --> ACTIVE: upgrade applied (top-up confirmed)
    ACTIVE --> ACTIVE: downgrade applied (at renewal)
    ACTIVE --> EXPIRED: lock lapses / NFT left wallet
    EXPIRED --> ACTIVE: renew (unpaid renewal only)
    CANCELLED --> [*]

With renewal in its default, unpaid mode, a subscription can be renewed out of EXPIRED back to ACTIVE — expiry is recoverable, not a dead end. When paid renewal is turned on, a lapsed subscription is not renewed: you subscribe again (see Paid renewal).

For node-runner memberships the activate step and the NFT mint are decoupled: activation flips the application to ACTIVE and requests the mint, and the on-chain mint follows out-of-band (exactly once across the cluster). So membership goes ACTIVE on activation even if the mint follows a beat later — see node-runner membership NFTs. Activation itself is guarded by wallet ownership: only the owning wallet can activate its own application.

Activation delivers the subscription NFT to the developer’s wallet as an offer the wallet must accept. If that acceptance didn’t finish — the process was interrupted, or the offer was left open — re-running the subscribe flow for the same app resumes the existing active subscription and completes the outstanding NFT acceptance, rather than erroring or starting a second paid subscription. An already-active subscription is detected from its status, so the resume is idempotent: it finishes the pending step and stops, without asking for a fresh deposit.

The same idempotent replay also repairs a subscribe that stalled before the deposit landed. A request returns a price-locked order whose quote holds for ~30 minutes; if you come back after it’s lapsed, re-running subscribe doesn’t hand you back stale, untagged payment instructions. For a subscription still at PENDING_DEPOSIT:

  • a live order still inside its window comes back unchanged;
  • an expired or underpaid order is re-quoted at the current rate for the same locked USD price, keeping the original destinationTag (so anything already paid still counts and you owe only the remainder);
  • a paid/converting order — payment already in hand — is never re-quoted out from under you;
  • a fulfilled order returns nothing to pay.

Because the re-quote reuses the destination tag on the same deposit wallet, a resumed payment can’t be stranded as untagged and unattributed.


Renewing a subscription (POST /api/v3/subscription/renew, or hsuite renew) extends its deposit lock period. By default renewal is unpaid: it takes no fresh deposit, the extension is the configured duration (additionalDays can override it, 1–365), the status returns to ACTIVE, expiresAt is pushed out, and a scheduled downgrade is applied. Every tier is paid and deposit-backed, so renewal always extends a real lock.

Off by default

When paid renewal is turned on (on every cluster alike), a renewal is no longer free: POST /api/v3/subscription/renew (or hsuite renew) makes a price-locked renew order at the price of the tier your renewed period will be on, through the same flow as a subscribe, an upgrade and a top-up: a rate snapshot, a 30-minute deadline, a Destination Tag, and every validator checking the payment against its own ledger read (account_tx by tag, delivered_amount). Nothing is extended until the order is paid. When it is, the lock is extended by one term from your current expiry (paying early loses no days), and what was delivered tops up the float, capped at 3× as below.

  • Which tier: the tier you will HAVE for the renewed period. With no change scheduled that is your current tier; if a downgrade is scheduled it applies at this renewal, so the order is quoted at the lower tier’s price. (An upgrade is applied when its top-up is paid, not at the boundary, so none is pending beside a renewal.) Scheduling or cancelling a tier change is refused while a renewal is pending, so the tier quoted is the tier applied. Every validator checks the order’s tier against its own copy of your subscription: it must be your current tier or the one your scheduled downgrade moves you to.
  • When: from the start of the renewal window (by default 30 days before expiry) until the subscription lapses. A lapsed subscription is final and is not renewed: subscribe again.
  • How to pay: in HSUITE, or in XRP when XRP payment is turned on (it is off by default). A swap asset is not offered for a renewal. hsuite renew only shows the order; hsuite renew --pay-with hsuite|xrp pays it from the developer wallet.
  • Asking again while an order is open returns that same order; an order that lapsed unpaid is replaced by a fresh one, and a short payment is topped up by a requote on the same tag. additionalDays is not accepted: an order prices the configured term.

The prepaid float rolls over (capped at 3×)

Section titled “The prepaid float rolls over (capped at 3×)”

The deposit is a prepaid float, not a use-it-or-lose-it monthly allotment. Any unspent float carries forward across a renewal — leftover credit is not forfeited. Whenever credit is added to the float (a paid renewal, an upgrade top-up or a same-tier top-up), the outstanding float is clamped to 3× the tier’s monthly credit, so credit never piles up without bound. The cap is read from the tier you’re on at the time, so an upgrade raises the ceiling and a downgrade lowers it.


Requesting an upgrade doesn’t raise your tier on a promise. It records a pendingTierChange with direction: 'upgrade' (and the orderId of its quote, but no effectiveAt: it applies when the top-up is verified, not on a date) and returns deposit instructions for the top-up — the price delta between your current tier and the target, in HSUITE. That delta is quoted as a price-locked order (kind: upgrade): the HSUITE/USD rate is fixed at quote time and the payment carries the order’s destination tag, exactly as the initial subscribe deposit does. The tier flips to the new level only once the validators confirm the top-up has landed on-chain. Until then you stay on your current tier.

The top-up funds the float as credit, not as a charge: when it confirms, fundedAmount grows by the confirmed top-up (nothing accrues), clamped to 3× the new tier’s monthly credit. So upgrading both raises your quotas and adds spendable float, with the same rollover cap applied at the new tier’s level.


Upgrading is not the only way to add credit — and it’s the wrong tool when your plan is fine but your balance ran dry. A pay-per-use subscription that simply consumed its float (remainingBalance reached zero after normal usage) doesn’t need a bigger plan; it needs more balance on the plan it already has. hsuite topup re-funds the float in place, on the same tier, with no renewal and no tier change.

Why this needs its own path: the float is seeded at activation and usage only draws it down (remainingBalance = fundedAmount − settledAmount). An unpaid renewal extends the deposit lock but adds no balance. Once remainingBalance hits zero, overage charging stops. The app keeps running: neither per-request gating nor the deployer scale-down gate looks at the balance.

This is the load-bearing distinction. Your deposit wallet holds two conceptually different things, and a same-tier top-up moves only one of them:

Deposit bond (depositAmount) Spendable float (remainingBalance)
What it is The collateral the app must maintain for its tier The prepaid credit usage draws down
A same-tier top-up unchanged — the bond you owe didn’t change grows by the confirmed credit (capped at 3×)

A top-up therefore does not raise your collateral obligation; it adds operating balance.

Terminal window
hsuite topup # re-fund one month of the current tier
hsuite topup --amount 5000 # a specific credit, in HSUITE smallest-units
hsuite topup --no-autopay # just print the deposit wallet + amount; pay it yourself
  • Default credit is one month of your current tier — the same monthly-fee math as the base deposit and the upgrade top-up, so all three sit on one scale.
  • A partial amount is valid: any credit that lifts remainingBalance above zero restores overage-charging headroom. You don’t have to fund a whole month to get unblocked.
  • --amount is HSUITE smallest-units and must be a non-negative integer. HSUITE on XRPL has no decimals, so one smallest-unit is one HSUITE.
  • On testnet the credit costs nothing real — hsuite faucet dispenses HSUITE-testnet first.
sequenceDiagram
    participant Dev as Developer (hsuite topup)
    participant API as Validator API
    participant Chain as Deposit wallet (XRPL)
    Dev->>API: POST /api/v3/subscription/topup (ownership-guarded)
    Note over API: record pendingTopUp<br/>capture on-chain baseline
    API-->>Dev: existing deposit wallet + credit + expectedTotal
    Dev->>Chain: pay the credit in HST (SendMax covers the 0.25% fee)
    loop each check
        API->>Chain: read on-chain balance
        alt balance grew by at least the credit
            Note over API: apply credit → float (capped 3x)<br/>bond unchanged, pendingTopUp cleared
        else not yet
            Note over API: leave pending, change nothing
        end
    end
  1. POST /api/v3/subscription/topup records a pending credit and returns the existing deposit wallet plus the amount to send (and the expectedTotal the wallet should reach), quoted as a price-locked order (kind: topup) — the rate is fixed at quote time and the payment carries the order’s destination tag. The route is ownership-guarded — only the owning wallet can top up its own app — and it fails closed on a malformed appId: a non-string or out-of-charset appId is rejected outright. The same fail-closed guard protects every owner-scoped subscription route.
  2. The CLI auto-pays that credit in HST (a SendMax covers the 0.25% transfer fee), unless you passed --no-autopay.
  3. The validators apply it on their next check, capped by the 3× rollover cap exactly as an upgrade top-up is.

Four properties keep this honest against a live, already-swept deposit wallet:

Baseline-relative confirm

The credit is confirmed against the wallet’s on-chain balance at request time, not against depositAmount. Settlement sweeps move settled HST out of the deposit wallet, so a wallet that has settled anything holds far less than its original deposit — requiring depositAmount + credit there would be unreachable. The check requires the balance to have grown by the credit since the request, which holds regardless of sweep history.

Cap on outstanding float, not lifetime funding

The 3× rollover cap trims against the outstanding (unspent) float — remainingBalance = fundedAmount − settledAmount — not against the monotonic lifetime fundedAmount. fundedAmount only ever grows, so a busy app can drive it up to the cap and then settle it back down; capping the lifetime total would leave a heavily-swept wallet permanently unable to re-fund even with an empty float. Capping the outstanding float means headroom reappears exactly as usage is settled, so a drained app can always top back up.

Decimal-safe comparison

On-chain HST balances are decimal (the 0.25% transfer fee alone leaves fractions), so amounts are compared in fixed-point units rather than coerced to integers — the credit still confirms against a balance like 142.7645.

Fail-closed and retryable

If the credit hasn’t landed, or the wallet bookkeeping can’t be recorded, nothing is applied and pendingTopUp survives — the next check re-verifies and re-applies. A paid-but-not-yet-observed top-up is reported honestly, not claimed as success.


A downgrade records a pendingTierChange with direction: 'downgrade' and is applied at the next renewal, not immediately. The answer (and the subscription status, while it is scheduled) carries { fromTier, toTier, direction, effectiveAt }: effectiveAt is the renewal boundary, your subscription’s expiresAt; you stay on fromTier until the renewal, which applies the downgrade. The deposit is not partially refunded mid-lock; the excess collateral stays locked. You can back out a pending change before it lands with POST /api/v3/subscription/tier-change/cancel. Builder is the lowest tier, so a Builder subscription cannot be downgraded further.


A subscription becomes EXPIRED when either of these happens:

  • the deposit lock lapses (the periodic expiry check moves past expiresAt), or
  • the subscription NFT leaves the wallet — an on-chain cross-check, run every few minutes, forces expiry if the subscription NFT is in neither the developer’s wallet nor the issuer’s (burned, for instance). A subscription activated or updated within the last 30 minutes is skipped, so an NFT still being delivered is not mistaken for a missing one. The NFT is minted non-transferable (Flags: 1), so the holder can’t move it to a third party — but a burn still removes it, and holding it stays a live condition of access.

A drained balance does not expire a subscription. A subscription is valid while its status is ACTIVE and it is not past expiresAt, nothing else: an empty float blocks only overage charging, never per-request admission and never the lifecycle. A drained-but-ACTIVE, non-expired subscription stays valid — you re-fund it with a same-tier top-up, you don’t wait for it to expire.

Expiry is announced as a lifecycle event. With renewal in its default, unpaid mode, the subscription can be revived by renewing.

The same expiry rule applies to an app’s own customer subscriptions: their active-status read compares expiry against the on-chain NFT, so a held-but-expired customer NFT reports inactive rather than gating the app forever — see Customer subscription NFTs.

A lapsed (expired) subscription doesn’t lock you out of your own app. Two operations stay available to the owning wallet regardless of subscription status:

  • Renew (or top up) — expiry is meant to be recoverable, so the path back is never gated.
  • Delete / retire the app — tearing an app down and stepping away from paying is not paid usage, so it isn’t gated on an active subscription. Only ownership and the owner-signed retire proof are required.

Discard an unpaid app (POST /api/v3/subscription/cancel). This path only ever cancels a subscription that was never paid for — one still at PENDING_DEPOSIT with nothing delivered. It is allowed only when the subscription is pending_deposit, no order of the app has counted a payment, and this validator’s own read of the deposit account’s validated payment history shows nothing paid; otherwise it refuses 409, the message beginning with the reason (not_pending_deposit:, payment_received:, or deposit_present:). The row becomes cancelled on every cluster and the app’s open quotes are closed. Nothing is paid out — a cancelled, never-paid deposit account (and the XRP reserve fronted for it) is reclaimed by the platform after its never-paid timeout. It is idempotent (a repeat answers alreadyCancelled: true, nothing changed) and owner-only: a non-owner and an unknown app get the same 404 Subscription not found, with no oracle. A paid subscription is never cancellable here — that is the payment_received: / deposit_present: 409.

A paid subscription has no cancel route. There is no developer route today that cancels a paid subscription and releases its collateral; a paid subscription ends by expiring. The subscription NFT is not burned on expiry — see NFT memberships → Revocation.


Every transition (created, renewed, expired, tier.changed, cancelled) is published as a lifecycle event, and membership lifecycle state is replicated across clusters — so every cluster shares one consistent view of who’s active. Anchoring lifecycle events on Hedera HCS as an audit trail is built, not yet enabled.