Pay-as-you-go Payments¶
Usage-based billing where providers claim funds incrementally within predefined limits — for AI agents, APIs, cloud services, and any consumption-based billing.
Overview¶
Pay-as-you-go policies let service providers pull payments on-demand, up to a maximum chunk amount per claim, within a capped total per billing period. When the period ends, counters reset automatically and a fresh cycle begins.
User creates policy --> Sets period limit + chunk limit
|
+------- Period 1 -------+ +------- Period 2 -------+
| | | | | | |
Claim 1 Claim 2 Claim 3 Claim 4 Claim 5 ...
$3.50 $7.20 $1.80 $5.00 $2.30
(chunk: max $10) (chunk: max $10)
(period: max $100/mo) (period resets: fresh $100)
When to Use¶
| Good For | Not Ideal For |
|---|---|
| AI/LLM providers (token-based billing) | Predictable fixed-cost services |
| API services (pay-per-call) | One-time purchases |
| Cloud resources (compute, storage) | Simple monthly subscriptions |
| SaaS with variable consumption | Project-based deliverables |
| Any service with unpredictable usage | Fixed-price contracts |
| Micro-payments with rate limits |
On-Chain Specification¶
PolicyType::PayAsYouGo {
max_amount_per_period: u64, // Total budget per billing period (lamports)
max_chunk_amount: u64, // Max per individual claim (lamports)
period_length_seconds: u64, // Duration of each period (seconds)
current_period_start: i64, // When current period started (unix timestamp)
current_period_total: u64, // Amount claimed so far in current period
padding: [u8; 88], // 128-byte alignment
}
Key Fields¶
| Field | Description |
|---|---|
max_amount_per_period |
Ceiling for total claims within one period. Resets when period rolls over. |
max_chunk_amount |
Maximum the provider can claim in a single execute_payment call. Prevents large unexpected pulls. |
period_length_seconds |
Billing cycle duration. Any value in seconds — hourly, daily, weekly, monthly, etc. |
current_period_start |
Timestamp when the current period began. Used to calculate period expiry. |
current_period_total |
Running total of claims in the current period. Checked against max_amount_per_period on each claim. |
Account Size¶
Each PayAsYouGo variant is exactly 128 bytes, consistent with all other policy types.
Creating a Pay-as-you-go Policy¶
Basic Example — AI API Billing¶
import { Tributary } from "@tributary-so/sdk";
import { BN } from "@coral-xyz/anchor";
import { createMemoBuffer } from "@tributary-so/sdk";
import { PublicKey, Transaction } from "@solana/web3.js";
const sdk = new Tributary(connection, wallet);
const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const provider = new PublicKey("BxKpT3mZQ5HgeRZFMfWVBpDCmCN8eYwGmCjL7m9mVq");
const gateway = new PublicKey("6ntm5rWqDFefET8RFyZV73FcdqxPMbc7Tso3pCMWk4w4");
const instructions = await sdk.createPayAsYouGo(
USDC_MINT,
provider, // recipient (the service provider)
gateway,
new BN(100_000_000), // max $100 per month
new BN(10_000_000), // max $10 per claim
new BN(86400 * 30), // 30-day period
createMemoBuffer("openai_api_user123", 64)
);
const tx = new Transaction().add(...instructions);
const signature = await sendAndConfirm(connection, tx, [wallet.payer]);
Conservative vs Aggressive Limits¶
// Conservative -- low risk, small frequent payments
const conservative = {
maxAmountPerPeriod: new BN(10_000_000), // $10/month cap
maxChunkAmount: new BN(1_000_000), // $1 max per claim
periodLength: new BN(86400 * 30), // monthly
};
// Aggressive -- higher limits, larger claims
const aggressive = {
maxAmountPerPeriod: new BN(100_000_000), // $100/month cap
maxChunkAmount: new BN(25_000_000), // $25 max per claim
periodLength: new BN(86400 * 7), // weekly
};
Period Length Options¶
const periods = {
hourly: new BN(3600), // 1 hour
daily: new BN(86400), // 1 day
weekly: new BN(86400 * 7), // 7 days
monthly: new BN(86400 * 30), // 30 days
quarterly: new BN(86400 * 90), // 90 days
custom: new BN(whatever_you_want),
};
How It Works¶
Provider Claims¶
The service provider (or their automated system) calls execute_payment when usage thresholds are met:
const instructions = await sdk.executePayment(
policyPda,
provider, // recipient
USDC_MINT,
gateway,
new BN(7_500_000) // claiming $7.50 for recent usage
);
const tx = new Transaction().add(...instructions);
await sendAndConfirm(connection, tx, [gatewaySigner]);
Validation & Execution¶
The protocol validates before transferring:
- Chunk limit:
payment_amount <= max_chunk_amount - Period limit:
current_period_total + payment_amount <= max_amount_per_period - Period expiry: if
now >= current_period_start + period_length_seconds, reset counters first
Automatic Period Reset¶
When the current time exceeds current_period_start + period_length_seconds:
current_period_totalresets to0current_period_startupdates to the current timestamp- A fresh billing cycle begins with full limits
Period 1: claimed $45 of $100
|--- period_length expires ---|
Period 2: fresh $100 limit, claimed $0
Token Delegation¶
The approval amount is calculated to cover a reasonable number of periods:
// SDK calculates: maxAmountPerPeriod * periods_to_cover
// Default: covers enough for the initial period + buffer
// Or provide explicitly:
const instructions = await sdk.createPayAsYouGo(
USDC_MINT,
provider,
gateway,
new BN(100_000_000), // $100/period
new BN(10_000_000), // $10/chunk
new BN(86400 * 30), // 30 days
createMemoBuffer("api_billing", 64),
new BN(300_000_000) // approvalAmount: $300 = 3 months buffer
);
Managing Pay-as-you-go Policies¶
Query Status¶
const policy = await sdk.getPaymentPolicy(policyPda);
const payg = policy.policyType.payAsYouGo;
console.log("Max per period:", payg.maxAmountPerPeriod.toString());
console.log("Max per chunk:", payg.maxChunkAmount.toString());
console.log("Period length:", payg.periodLengthSeconds.toString(), "seconds");
console.log(
"Period started:",
new Date(payg.currentPeriodStart.toNumber() * 1000)
);
console.log("Period used:", payg.currentPeriodTotal.toString());
const remaining =
payg.maxAmountPerPeriod.toNumber() - payg.currentPeriodTotal.toNumber();
const periodEnd = new Date(
(payg.currentPeriodStart.toNumber() + payg.periodLengthSeconds.toNumber()) *
1000
);
console.log(`Remaining this period: $${remaining / 1e6}`);
console.log(`Period ends: ${periodEnd.toLocaleString()}`);
Provider-side Claim Check¶
async function canClaim(
sdk: Tributary,
policyPda: PublicKey,
amount: number
): Promise<boolean> {
const policy = await sdk.getPaymentPolicy(policyPda);
const payg = policy.policyType.payAsYouGo;
if (amount > payg.maxChunkAmount.toNumber()) return false;
const remaining =
payg.maxAmountPerPeriod.toNumber() - payg.currentPeriodTotal.toNumber();
return amount <= remaining;
}
// Before claiming:
if (await canClaim(sdk, policyPda, 7_500_000)) {
const instructions = await sdk.executePayment(
policyPda,
provider,
USDC_MINT,
gateway,
new BN(7_500_000)
);
// ... submit transaction
}
Pause / Resume / Cancel¶
// Pause -- stops all claims
await sdk.changePaymentPolicyStatus(tokenMint, policyId, { paused: {} });
// Resume -- reactivates
await sdk.changePaymentPolicyStatus(tokenMint, policyId, { active: {} });
// Cancel -- deletes policy, revokes delegation
await sdk.deletePaymentPolicy(tokenMint, policyId);
Use Case Examples¶
AI Agent Token Billing¶
// LLM provider charges per token batch
await sdk.createPayAsYouGo(
USDC_MINT,
llmProvider,
gateway,
new BN(50_000_000), // $50/month max
new BN(5_000_000), // $5 max per batch
new BN(86400 * 30), // monthly
createMemoBuffer("llm_api_user_42", 64)
);
// Provider claims after each batch:
await sdk.executePayment(
policyPda,
llmProvider,
USDC_MINT,
gateway,
new BN(2_340_000) // $2.34 for 234K tokens
);
REST API Pay-per-call¶
// API gateway charges per 1000 requests
await sdk.createPayAsYouGo(
USDC_MINT,
apiProvider,
gateway,
new BN(25_000_000), // $25/month max
new BN(500_000), // $0.50 max per claim
new BN(86400 * 30), // monthly
createMemoBuffer("weather_api_pro", 64)
);
Cloud Compute¶
// Compute provider bills hourly usage
await sdk.createPayAsYouGo(
USDC_MINT,
cloudProvider,
gateway,
new BN(200_000_000), // $200/month max
new BN(20_000_000), // $20 max per claim
new BN(86400 * 30), // monthly
createMemoBuffer("gpu_cluster_proj_x", 64)
);
Best Practices¶
For Users (Payment Creators)¶
- Start conservative — set low limits first, increase as trust builds
- Monitor claims — track provider behavior and adjust limits accordingly
- Match period to budget — align billing periods with your financial cycles
- Use pause liberally — if something looks off, pause first, investigate later
For Providers (Service Providers)¶
- Claim reasonably — don't max out chunks unnecessarily; build user trust
- Transparent conversion — clearly show how usage maps to payment amounts
- Document pricing — publish your rate card so users can estimate costs
- Notify on large claims — give users a heads-up before pulling significant amounts
Security¶
- Rate limiting — implement reasonable delays between claims
- Usage proofs — consider attaching usage receipts to claim memos
- Emergency pause — users can pause instantly if they detect abuse
- Audit trail — all claims are on-chain with timestamps and amounts
Troubleshooting¶
| Error | Cause | Fix |
|---|---|---|
InvalidAmount |
Claim exceeds max_chunk_amount |
Reduce claim amount, or increase chunk limit |
PeriodLimitExceeded |
Period total would exceed max_amount_per_period |
Wait for period reset, or increase period limit |
InvalidDelegation |
Delegated amount insufficient | Re-approve with higher amount |
PaymentNotDue |
Likely wrong policy type being called | Pay-as-you-go doesn't use due dates |
Comparison with Other Policy Types¶
| Subscription | Milestone | Pay-as-you-go | |
|---|---|---|---|
| Amount | Fixed per period | Variable per milestone | Variable per claim |
| Timing | Fixed schedule | Event/timestamp based | On-demand |
| Flexibility | Low | Medium | High |
| Predictability | High | Medium | Low |
| Best For | Recurring services | Project deliverables | Variable usage |
| User Control | Set up once | Approve per milestone | Period limits |
| Provider Control | None (automatic) | Claim after approval | Claim within limits |