@tributary-so/payments
v1.0.0
Published
Stripe-compatible payments SDK for Tributary USDC subscriptions - zero API keys required
Readme
@tributary-so/payments
A minimal Stripe-compatible payments SDK for Tributary USDC subscriptions on Solana. Provides essential checkout session functionality with zero API keys required - developers can integrate immediately without registration.
Features
- Zero API Keys: No registration, no configuration, just install and use
- USDC Only: Single currency support (USDC on Solana)
- Tributary Payment Method: Only supports "tributary" payment method type
- Base64URL Encoding: Compact, URL-safe session encoding for sharing
- Dual Lookup Strategy: User-based OR gateway-based subscription status checking
- Real-time Status: Live subscription status using PaymentPolicy
paymentCount - Pure Frontend: No backend or webhooks required for basic functionality
- Type Safety: Full TypeScript support with Stripe-compatible types
Installation
npm install @tributary-so/paymentsQuick Start
import { PaymentsClient } from "@tributary-so/payments";
import { Connection } from "@solana/web3.js";
import { Tributary } from "@tributary-so/sdk";
// Initialize with connection and tributary
const connection = new Connection("https://api.mainnet-beta.solana.com");
const tributary = new Tributary(connection, wallet);
const stripe = new PaymentsClient(connection, tributary);
const session = await stripe.checkout.sessions.create({
payment_method_types: ["tributary"],
line_items: [
{
description: "Monthly premium access to all features",
unitPrice: 20.0, // $20.00
quantity: 1,
},
],
paymentFrequency: "monthly",
mode: "subscription",
success_url: "https://yourapp.com/success",
cancel_url: "https://yourapp.com/cancel",
tributaryConfig: {
gateway: "GATEWAY_PUBLIC_KEY_HERE",
recipient: "RECIPIENT_PUBLIC_KEY_HERE",
trackingId: "user_123_monthly_premium", // Your unique identifier
autoRenew: true,
memo: "Optional memo for the payment",
},
});
// Redirect to hosted checkout
window.location.href = session.url;Subscription Status Tracking
Check subscription status efficiently using our dual lookup strategy - either user-based OR gateway-based:
import { PaymentsClient } from "@tributary-so/payments";
import { Connection } from "@solana/web3.js";
import { Tributary } from "@tributary-so/sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const tributary = new Tributary(connection, wallet);
const stripe = new PaymentsClient(connection, tributary);
// Option 1: User-based lookup (for user-facing apps)
async function checkUserSubscription() {
const status = await stripe.subscriptions.checkStatus({
trackingId: "user_123_monthly_premium",
userPublicKey: "USER_PUBLIC_KEY_HERE",
tokenMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
});
if (status.status === "active") {
console.log("Subscription active!", {
paymentCount: status.paymentCount,
nextPaymentDue: status.nextPaymentDue,
});
// Grant access, update UI, etc.
} else if (status.status === "created") {
console.log("Subscription created, waiting for first payment...");
// Show pending status
} else {
console.log("Subscription not found or failed");
// Handle error case
}
}
// Option 2: Gateway-based lookup (for gateway management)
async function checkGatewaySubscription() {
const status = await stripe.subscriptions.checkStatus({
trackingId: "user_123_monthly_premium",
gatewayPublicKey: "GATEWAY_PUBLIC_KEY_HERE",
});
if (status.status === "active") {
console.log("Subscription active via gateway!", {
paymentCount: status.paymentCount,
nextPaymentDue: status.nextPaymentDue,
});
// Gateway management logic
}
}
// Enhanced session retrieval with real-time status
async function getSessionWithStatus() {
const session = await stripe.checkout.sessions.retrieve(
"session_id_or_encoded_url"
);
if (session.subscription) {
console.log("Subscription details:", {
id: session.subscription.id,
status: session.subscription.status,
paymentCount: session.subscription.paymentCount,
current_period_end: session.subscription.current_period_end,
});
}
}
// Check status on page load
checkUserSubscription();Status Flow
The subscription status follows a simple flow based on PaymentPolicy state:
pending: Subscription not yet createdcreated: Subscription created on-chain, waiting for first paymentactive: First payment executed, subscription is active
interface SubscriptionStatus {
id: string; // PaymentPolicy public key
object: "subscription";
status: "pending" | "created" | "active" | "failed";
paymentCount: number; // Number of payments executed
current_period_start?: number; // Unix timestamp
current_period_end?: number; // Unix timestamp
nextPaymentDue?: number; // Unix timestamp of next payment
metadata: {
trackingId: string;
userPublicKey?: string;
gatewayPublicKey?: string;
recipient: string;
tokenMint: string;
amount: number;
frequency: number;
};
}URL Encoding
The SDK uses Base64URL encoding to pack all subscription parameters into compact, shareable URLs:
// Session URL contains all necessary data
const session = await stripe.checkout.sessions.create({
// ... configuration
});
// URL format: https://checkout.tributary.so/subscribe/{base64url-encoded-data}
console.log(session.url);
// Example: https://checkout.tributary.so/subscribe/eyJ0bSI6IkVQakZXZGRBdWZxU1NxZTJxTjF6eWJhcEM4RzR3RUdHa3p3eVREdjF2Iiwi...The encoded data includes:
tm: Token mint (USDC)r: Recipient public keyg: Gateway public keya: Total amount (calculated from line items)ar: Auto-renew flagmr: Maximum renewalspf: Payment frequencyst: Start timetid: Tracking IDli: Line items (JSON array)
MEMO Format
Tracking IDs are stored in Solana transaction MEMO fields using the format:
tributary:payment:{trackingId}Example: tributary:payment:user_123_monthly_premium
This enables:
- Blockchain verification: Payment status can be verified by anyone
- No webhooks needed: Status checking via on-chain state
- Client-side only: Pure frontend implementation possible
API Reference
PaymentsClient
The main client class - requires Connection and Tributary instances.
import { Connection } from "@solana/web3.js";
import { Tributary } from "@tributary-so/sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const tributary = new Tributary(connection, wallet);
const stripe = new PaymentsClient(connection, tributary);checkout.sessions.create()
Create a checkout session with encoded URL.
const session = await stripe.checkout.sessions.create({
payment_method_types: ["tributary"], // Only "tributary" supported
line_items: [
{
description: "Product Name",
unitPrice: 20.0, // Amount in dollars
quantity: 1,
},
],
paymentFrequency: "monthly", // "daily" | "weekly" | "monthly" | "annually"
mode: "subscription",
success_url: "https://yourapp.com/success",
cancel_url: "https://yourapp.com/cancel",
tributaryConfig: {
gateway: "gateway-public-key", // Gateway public key
recipient: "recipient-public-key", // Recipient public key
trackingId: "unique-tracking-id", // Your unique identifier
autoRenew: true,
memo: "Optional memo for payments",
},
});Returns:
{
id: string; // Session ID
object: "checkout.session";
url: string; // Encoded checkout URL
payment_status: "unpaid";
status: "open";
amount_total: number;
currency: "usd";
// ... other Stripe-compatible fields
}checkout.sessions.retrieve()
Retrieve a session with real-time subscription status.
// Retrieve by session ID
const session = await stripe.checkout.sessions.retrieve("cs_1234567890");
// Or retrieve by encoded URL (auto-detected)
const session = await stripe.checkout.sessions.retrieve(
"https://checkout.tributary.so/subscribe/eyJ0bSI6IkVQakFWZGRBdWZxU1NxZTJxTjF6eWJhcEM4RzR3RUdHa3p3eVREdjF2Iiwi..."
);
// Enhanced response with subscription details
console.log(session.subscription);
// {
// id: "sub_1234567890",
// object: "subscription",
// status: "active",
// paymentCount: 3,
// current_period_end: 1640995200,
// nextPaymentDue: 1641081600,
// metadata: { ... }
// }Returns:
{
id: string;
object: "checkout.session";
url: string;
payment_status: "unpaid";
status: "open";
amount_total: number;
currency: "usd";
subscription?: SubscriptionStatus; // Real-time status if available
// ... other Stripe-compatible fields
}subscriptions.checkStatus()
Check subscription status using dual lookup strategy.
// User-based lookup
const status = await stripe.subscriptions.checkStatus({
trackingId: "user_123_monthly_premium",
userPublicKey: "USER_PUBLIC_KEY_HERE",
tokenMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
});
// Gateway-based lookup
const status = await stripe.subscriptions.checkStatus({
trackingId: "user_123_monthly_premium",
gatewayPublicKey: "GATEWAY_PUBLIC_KEY_HERE",
});PolicyLookupOptions:
interface PolicyLookupOptions {
trackingId: string;
userPublicKey?: string; // Either userPublicKey OR gatewayPublicKey
gatewayPublicKey?: string;
tokenMint?: string; // Defaults to USDC
}Returns:
{
id: string;
object: "subscription";
status: "pending" | "created" | "active" | "failed";
paymentCount: number;
current_period_start?: number;
current_period_end?: number;
nextPaymentDue?: number;
metadata: {
trackingId: string;
userPublicKey?: string;
gatewayPublicKey?: string;
recipient: string;
tokenMint: string;
amount: number;
frequency: number;
};
}subscriptions.isActive()
Quick check if subscription is active (created + initial payment).
const isActive = await stripe.subscriptions.isActive({
trackingId: "user_123_monthly_premium",
userPublicKey: "USER_PUBLIC_KEY_HERE",
});
// Returns: booleansubscriptions.getDetails()
Get detailed subscription information.
const details = await stripe.subscriptions.getDetails({
trackingId: "user_123_monthly_premium",
gatewayPublicKey: "GATEWAY_PUBLIC_KEY_HERE",
});
// Returns: SubscriptionStatus | nullThe tributaryConfig object contains Tributary-specific settings:
gateway: Your Tributary gateway public keyrecipient: The recipient public key (where payments go)trackingId: Your unique identifier for tracking paymentsautoRenew: Enable automatic subscription renewal (default: false)memo: Optional memo to attach to each payment transaction
Tributary Configuration
Traditional Approach (Expensive)
// Expensive: Scan transaction history
const transactions = await connection.getSignaturesForAddress(recipient);
const paymentTxs = await Promise.all(
transactions.map((sig) => connection.getParsedTransaction(sig.signature))
);
const hasPayment = paymentTxs.some((tx) =>
tx.memo?.includes(`tributary:payment:${trackingId}`)
);PaymentPolicy Approach (Efficient)
// Fast: Direct on-chain state lookup
const paymentPolicy = await tributary.getPaymentPolicy(policyPda);
const isActive = paymentPolicy?.paymentCount > 0;Performance Benefits
Traditional Approach (Expensive)
// Expensive: Scan transaction history
const transactions = await connection.getSignaturesForAddress(recipient);
const paymentTxs = await Promise.all(
transactions.map((sig) => connection.getParsedTransaction(sig.signature))
);
const hasPayment = paymentTxs.some((tx) =>
tx.memo?.includes(`tributary:payment:${trackingId}`)
);PaymentPolicy Approach (Efficient)
// Fast: Direct on-chain state lookup
const paymentPolicy = await tributary.getPaymentPolicy(policyPda);
const isActive = paymentPolicy?.paymentCount > 0;Dual Lookup Strategy (Optimized)
Our SDK provides two optimized lookup methods:
User-based lookup:
// O(1) lookup via UserPayment PDA
const userPaymentPda = getUserPaymentPda(userPublicKey, tokenMint);
const policies = await getPaymentPoliciesByUserPayment(userPaymentPda);
const policy = policies.find((p) => p.trackingId === trackingId);Gateway-based lookup:
// O(1) lookup via gateway public key
const policies = await getPaymentPoliciesByGateway(gatewayPublicKey);
const policy = policies.find((p) => p.trackingId === trackingId);Benefits:
- O(1) vs O(n): Direct lookup vs transaction scanning
- Low RPC cost: Single query vs hundreds of transactions
- Instant response: No need to scan entire payment history
- Flexible: User-facing apps OR gateway management
- Reliable: Uses on-chain state, not external indexes
- Real-time: Live status from PaymentPolicy
paymentCount
Error Handling
The SDK throws standard JavaScript errors for invalid inputs:
try {
const session = await stripe.checkout.sessions.create(params);
} catch (error) {
if (error.message.includes("Invalid gateway public key")) {
// Handle invalid gateway key
} else if (error.message.includes("Invalid trackingId format")) {
// Handle invalid tracking ID
} else if (
error.message.includes("Either userPublicKey or gatewayPublicKey required")
) {
// Handle missing lookup parameters
}
// ... other error handling
}Development
# Install dependencies
npm install
# Build the package
npm run build
# Run tests
npm run test
# Lint the code
npm run lintLicense
MIT
