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
): boolIt 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
- User Subscribes: The user connects their wallet and signs the PayStreamer
SubscribePTB. - Content Fetched: Your platform fetches the encrypted blob from Walrus.
- Decryption Request: The client requests the decryption key from Seal, targeting your
seal_approvefunction with the user'sSubscriptionAccountobject. - On-Chain Verification: Seal's key servers dry-run
seal_approve. It checks theidnamespace, then delegates tohas_active_subscriptionto confirm the subscription is actually active. - 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
- Learn how to deploy a Platform in PayStreamer: Registering a Platform
- Read the Seal documentation (opens in a new tab) for the full client SDK flow (encrypting on upload, session keys, fetching key shares).
- Read the Sui documentation on Walrus Sites (opens in a new tab) to learn how to host your dApp completely decentralized!