Seal & Walrus

Seal & Walrus Integration

PayStreamer is designed to integrate cleanly with Sui's advanced primitives, most notably Walrus (for decentralized storage) and Seal (for encrypted data access).

This guide explains how content platforms can use PayStreamer as the on-chain source of truth to conditionally decrypt content for active subscribers.

Scope: PayStreamer doesn't publish its own Seal policy module — you write a small one yourself, in your own platform's Move package, that calls PayStreamer's existing has_active_subscription helper. That's the entire integration surface; everything below is real, working Move, not a conceptual sketch.

The Architecture

When building a decentralized platform (like a video streaming dApp or a token-gated newsletter), you need a way to serve content only to users who have paid for it.

1. Walrus for Storage

Instead of relying on AWS S3, platforms upload their encrypted raw content (videos, images, articles) to Walrus (opens in a new tab). The content is stored as decentralized blobs on the Sui network.

2. Seal for Access Control

Seal (opens in a new tab) handles key management and decryption. You encrypt content off-chain and write an on-chain Move policy — a seal_approve function — that Seal's key servers dry-run before releasing a decryption key. If your function doesn't abort, access is granted.

3. PayStreamer for Billing

PayStreamer provides the dynamic SubscriptionAccount. Because it tracks subscription status (active, paused, cancelled) entirely on-chain, your seal_approve policy can read that state directly instead of reimplementing billing logic.


Implementing the Seal Policy

PayStreamer exposes a read-only helper in its account module built for exactly this:

public fun has_active_subscription<T>(
    account: &SubscriptionAccount<T>,
    platform_id: ID
): bool

It returns false if the platform was never subscribed to, if the account itself is paused or closed, or if that specific subscription is paused or cancelled — including PayStreamer's automatic pause after 3 consecutive failed payments (see the callout below).

Writing Your Custom Move Policy

Seal's real on-chain contract is an entry fun seal_approve(id: vector<u8>, ...) — it takes no return value; access is denied by aborting, granted by returning normally. The id argument is the identity being requested for decryption, and by convention should be namespaced under something only your policy controls (here, your platform's own ID) — encrypt each piece of content with an id like [platform_id][nonce], so this policy can never be tricked into approving a key for someone else's data.

module my_platform::content_policy {
    use paystreamer::account::{Self, SubscriptionAccount};

    /// Your platform's ID, as registered in PayStreamer.
    const PLATFORM_ID: address = @0x123...;

    const ENoAccess: u64 = 1;

    /// Called by Seal key servers (via dry-run) to decide whether to
    /// release a decryption key share for `id`. Must be an `entry fun`
    /// with no return value — Seal grants access if this doesn't abort.
    entry fun seal_approve<T>(
        id: vector<u8>,
        account: &SubscriptionAccount<T>,
    ) {
        assert!(is_namespaced_to_platform(id), ENoAccess);
        assert!(
            account::has_active_subscription(
                account,
                sui::object::id_from_address(PLATFORM_ID),
            ),
            ENoAccess,
        );
    }

    /// Enforces the `[platform_id][nonce]` id convention described above.
    fun is_namespaced_to_platform(id: vector<u8>): bool {
        let namespace = sui::address::to_bytes(PLATFORM_ID);
        if (namespace.length() > id.length()) return false;
        let mut i = 0;
        while (i < namespace.length()) {
            if (namespace[i] != id[i]) return false;
            i = i + 1;
        };
        true
    }
}

Automatic pausing: if a user fails 3 consecutive payments due to insufficient funds, PayStreamer automatically pauses that subscription. Once paused, has_active_subscription immediately returns false, and your seal_approve policy starts aborting — Seal's key servers stop releasing keys the moment billing lapses, with no extra code on your end.

The User Flow

  1. User Subscribes: The user connects their wallet and signs the PayStreamer Subscribe PTB.
  2. Content Fetched: Your platform fetches the encrypted blob from Walrus.
  3. Decryption Request: The client requests the decryption key from Seal, targeting your seal_approve function with the user's SubscriptionAccount object.
  4. On-Chain Verification: Seal's key servers dry-run seal_approve. It checks the id namespace, then delegates to has_active_subscription to confirm the subscription is actually active.
  5. Content Served: If the policy doesn't abort, Seal returns key shares, the client decrypts the Walrus blob, and the user gets their content.

Next Steps