Skip to content

NFT Memberships on XRPL

Your subscription isn’t only a row in a cluster database. When a paid subscription activates, the cluster mints you a subscription NFT on XRPL — the canonical ledger for the HSUITE ecosystem. It’s a public, verifiable receipt that the subscription exists and belongs to your wallet.


The HSUITE token and its subscription/membership NFTs are XRPL-canonical. Subscriptions are paid in HSUITE, and the receipt for that payment lives where HSUITE lives. (Hedera remains in the platform for HCS consensus messaging, but the subscription NFT itself is minted on XRPL.)


The NFT is minted from the platform’s issuer account. That account’s master key is disabled after genesis, so it signs only through its native SignerList: a quorum of the validators’ DKG signing slots co-signs every mint — never one stored private key.

The mint is an NFTokenMint transaction with:

  • a fixed subscription taxon (the collection id for subscription NFTs),
  • Flags: 1 = tfBurnable only — burnable by the issuer, and not transferable: the issuer’s own sell offer delivers it to the developer’s wallet, and the holder can never resell or move it,
  • a URI of the form smart-engines://subscription/{appId}.
sequenceDiagram
    participant Dev as Developer wallet
    participant Cl as Cluster (validators)
    participant TSS as Validator quorum (DKG multisig)
    participant XRPL as XRPL ledger
    Dev->>Cl: deposit confirmed → activate
    Cl->>TSS: request signatures on NFTokenMint
    TSS-->>Cl: quorum of slot signatures (no single key)
    Cl->>XRPL: submit NFTokenMint (taxon, Flags 1, URI)
    XRPL-->>Cl: tesSUCCESS + NFTokenID
    Cl->>XRPL: issuer sell offer to the developer wallet
    Dev->>XRPL: accept the offer

What the URI encodes — and what it does not

Section titled “What the URI encodes — and what it does not”

The NFT URI is exactly smart-engines://subscription/{appId}. It carries only the appId. It deliberately does not encode the tier.

Why this matters: a tier change (upgrade or downgrade) never staleness-breaks the NFT, because the NFT never claimed a tier in the first place. Re-minting on a tier change would only reproduce an identical token, so the platform doesn’t do it. The tier lives off-NFT, in the cluster’s record for your app.


When the validator decides whether a request is allowed, it resolves the tier for the appId from the cluster’s app registry (defaulting to the most conservative builder for unknown apps). The NFT is the on-chain anchor that the subscription is real; the tier record is what drives quota and capability gating (see Gating & quotas).

The platform also runs a cross-check every few minutes that compares the cluster’s record against the on-chain reality: if the subscription NFT is in neither the developer’s wallet nor the issuer’s — for example because it was burned — the subscription is forced to EXPIRED. 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. Holding it is a live condition of access, not a one-time check.


Burn-on-cancel: not built

Nothing burns a subscription NFT today. A paid subscription has no cancel route and ends by expiring; when it does, the NFT stays in the wallet and the subscription reads expired. The token is minted tfBurnable, so an issuer-signed burn is possible, but no part of the subscription lifecycle triggers one.


A subscription record can track NFT references per chain (chainNfts) for subscriptions that span more than one network, but the subscription NFT of record is the XRPL one.


Customer subscription NFTs (an app’s own members)

Section titled “Customer subscription NFTs (an app’s own members)”

Smart-apps built on the platform can gate their own end-customers behind a subscription, using the same XRPL rails. When a customer subscribes to an app, the cluster mints a customer-subscription NFT — a Flags: 1 (burnable, not transferable) token under the customer-subscription taxon — to the customer’s wallet, proving that customer’s entitlement to that specific app. The customer can’t resell or move it; the ledger refuses any offer on it from anyone but the app. Two properties are worth calling out.

The mint is idempotent. POST /api/v3/subscription/customer/mint sits behind a fully retryable call chain, so a client whose response is lost after the on-chain mint already landed will retry the identical request. The mint resolves an idempotency key before any on-chain work — the caller’s explicit idempotencyKey, or a deterministic hash of {appId, customerAddress, customerChain, metadataCid} — and claims it in a unique-indexed record. A completed claim replays the stored {nftSerial, txHash} instead of minting again, so a retried subscribe never double-mints the membership (a duplicate would be a second paid entitlement for one payment).

Active status enforces expiry, not just holding. Reading a customer’s subscription status (GET /api/v3/baas/marketplace/apps/:appId/subscription) resolves the on-chain NFT and compares its expiry: active is true only when the wallet holds the NFT and its expiresAt is still in the future. Nothing burns an expired customer NFT — it stays in the wallet indefinitely — so merely holding the token is not enough. Without the expiry compare a single payment would gate the app forever; with it, an expired subscription reads active: false.

The tier and expiry are read straight off the ledger, and the newest NFT wins. The resolver reads a customer subscription’s terms directly from the NFT’s on-ledger URI — of the form hsub1;t=<tier>;e=<epochSeconds> — so a status read needs no IPFS round-trip. Because a renewal leaves the customer holding more than one subscription NFT (the old one isn’t burned), the resolver picks the latest-expiring record regardless of ledger order — that one is the active tier. A malformed hsub1 record is skipped and never shadows a valid one, and the resolver falls back to the legacy ipfs:// metadata path only when no valid hsub1 record exists. Expiry is still compared downstream, so holding the newest NFT doesn’t make an expired subscription read active.


Developer subscriptions aren’t the only on-chain membership. Running a node (validator, host, or gateway) has its own membership: an operator applies, locks a deposit, activates, and receives a membership NFT that proves the node’s place in the network. This runs the same XRPL + threshold-signing rails as the subscription NFT above. Node operator slots are not open to the public yet; this section describes how the membership works.

Only the owning wallet can apply or activate. The membership apply and activate endpoints are guarded by wallet ownership, not an operator shared secret. On apply, the wallet the membership is billed to (accountId in the request) must equal the authenticated wallet — you cannot apply for membership on behalf of someone else’s wallet. On activate, the application’s recorded ownerAccountId must equal the authenticated wallet — only the owner can activate their own application. The guard fails closed: no session, a wallet mismatch, or an application whose owner can’t be established is refused.

A membership can carry multiple roles, and they’re preserved. One application can declare more than one node role (e.g. validator and gateway), and the deposit is charged as the sum of each declared role’s deposit. The application’s roles list and its networkType are persisted and read back verbatim — an operator who pays for three roles has all three recorded, not silently collapsed to one. networkType in particular is an authorization input downstream (it scopes which role a membership may act as), so an application with no recorded networkType stays genuinely indeterminate and is treated fail-closed rather than defaulted to validator.

The NFT mint is event-driven. Activation doesn’t mint inline. It requests the mint as an event; a dedicated subscriber performs the on-chain mint (an explicit-issuer, threshold-signed NFTokenMint delivered to the recipient wallet — the issuer’s master key is disabled post-genesis, so this is DKG-multisigned, never wallet-signed). The subscriber runs on every cluster node, so it takes an atomic mint-claim before minting: exactly one node mints per application, and a repeated request is skipped once a serial already exists. When the mint lands, the on-chain serial is recorded on the membership.

sequenceDiagram
    participant Op as Node operator wallet
    participant Guard as Ownership guard
    participant Svc as Membership record
    participant Sub as Mint subscriber (per node)
    participant XRPL as XRPL ledger
    Op->>Guard: apply / activate (authenticated wallet)
    Guard->>Guard: caller wallet owns this membership
    Guard->>Svc: activate application
    Svc->>Sub: mint requested (event)
    Sub->>Sub: atomic mint-claim (exactly-once)
    Sub->>XRPL: threshold-signed NFTokenMint to operator
    XRPL-->>Sub: serial + NFTokenID
    Sub->>Svc: minted (serial recorded)