TLDR:Documentation Index
Fetch the complete documentation index at: https://docs.chainstack.com/llms.txt
Use this file to discover all available pages before exploring further.
- The Solana Subscriptions & Allowances program emits its events (subscription created, cancelled, resumed, and the three transfer types) as Anchor-compatible self-CPI inner instructions — not log lines — tagged with the 8-byte
e445a52e51cb9a1dprefix plus a 1-byte event type. - Decoding those events gives you off-chain billing state — revenue per plan, active vs. cancelled subscribers, every pull and its amount — without scanning account state.
- You can pull events from any transaction over standard RPC, or stream them in real time and low latency through a Chainstack Yellowstone gRPC (Geyser) node.
- This guide gives you a complete Python decoder for all six event types, plus a live Yellowstone gRPC listener, validated against live mainnet activity.
Why index the events
If you build anything on the Subscriptions & Allowances program — a SaaS billing backend, a creator-payouts dashboard, a payment gateway — you need an off-chain view of what happened: who subscribed, who cancelled, which pulls succeeded, and how much revenue each plan produced. The program makes this clean because it emits a structured event on every state change. You don’t reconstruct billing history by diffing account snapshots; you read the events. The question is only how you get them — by fetching a transaction over RPC, or by streaming them live through Geyser. We’ll do both, on top of one shared decoder.How the program emits events
Most Solana programs that emit events either write base64 blobs into program logs or, like Anchor, emit them through a self-CPI: the program calls its own no-op instruction, and the event payload rides along as that inner instruction’s data. The Subscriptions program uses the self-CPI approach, which is more reliable than log parsing (logs get truncated; inner instructions don’t). Each event’s inner-instruction data is laid out as:| Type | Byte | Payload fields |
|---|---|---|
SubscriptionCreated | 0 | plan, subscriber, mint, created_ts |
SubscriptionCancelled | 1 | plan, subscriber, expires_at_ts |
SubscriptionTransfer | 2 | subscription, plan, delegator, mint, amount, period_start_ts, period_end_ts, amount_pulled_in_period, receiver |
FixedTransfer | 3 | delegation, delegator, delegatee, mint, amount, remaining_amount, receiver |
RecurringTransfer | 4 | delegation, delegator, delegatee, mint, amount, period_start_ts, period_end_ts, amount_pulled_in_period, receiver |
SubscriptionResumed | 5 | plan, subscriber, resumed_ts |
u64 for amounts, i64 for timestamps). To find these events in a transaction, you look at its inner instructions, keep the ones whose program is the subscriptions program, and check whether the data starts with the tag.
SubscriptionTransfer (model: subscription plan), FixedTransfer (allowance), and RecurringTransfer (recurring delegation) are the three “money moved” events — the ones you sum for revenue. Created, Cancelled, and Resumed track the subscription lifecycle for churn.Prerequisites
- Python 3.10+
pip install solders base58 requestsfor the decoder and RPC pathpip install grpcio grpcio-toolsfor the Geyser streaming path- A Chainstack Solana node — any node for the RPC path; a Trader or dedicated node with a Yellowstone gRPC endpoint for streaming
Run Solana mainnet and devnet nodes on Chainstack
Start for free and get your app to production levels immediately. No credit card required. You can sign up with your GitHub, X, Google, or Microsoft account.The decoder
This is the shared core — it turns a raw inner-instruction byte string into a typed event dict. Both the RPC and the Geyser paths feed bytes intodecode_event.
Option A: pull events from a transaction over RPC
For backfills, webhooks, or a quick look at one transaction, fetch it withgetTransaction and walk its inner instructions. In jsonParsed encoding, an inner instruction for a program the parser doesn’t recognize comes back with a base58 data field and a programId.
period_end_ts - period_start_ts is exactly 86400 here — a 24-hour billing period — and amount is the tokens pulled this cycle. That’s a billing row, straight off the chain.
RPC polling is fine for backfills, but for a live dashboard you don’t want to poll getSignaturesForAddress in a loop. That’s what Geyser is for.
Option B: stream events live with Yellowstone gRPC
Chainstack’s Yellowstone gRPC (Geyser) endpoint pushes transactions to you as they’re confirmed. You subscribe once to transactions that mention the program, and decode events from each one with the samedecode_event.
Generate the Geyser client stubs
Fetch the Yellowstone proto files and generate Python stubs.This writes
geyser_pb2.py, geyser_pb2_grpc.py, and the solana_storage_pb2.py it depends on into your working directory.Connect, subscribe, and decode
Chainstack Yellowstone endpoints use TLS and an Point it at a mainnet Yellowstone node and you see live subscription activity stream in:The reconnect loop matters: long-lived gRPC streams drop occasionally, and you want to resume rather than miss events. For production you’d also persist the last processed slot and replay from it on reconnect using the request’s
x-token passed as gRPC metadata. Get both the gRPC endpoint and the token from your node’s details in the Chainstack console.The one subtlety on the streaming side: an inner instruction references its program by index into the transaction’s account keys, and for versioned transactions that list is message.account_keys plus the addresses loaded from lookup tables. Resolve the full list before checking the program.from_slot field.What you can build
Once events are flowing into your own store, the billing views write themselves:- Revenue per plan — sum
amountonSubscriptionTransfergrouped byplan. The mint tells you the currency; on mainnet you’ll see USDC and wrapped SOL. - Monthly recurring revenue and active subscriber counts — count
SubscriptionCreatedminusSubscriptionCancelled(net ofSubscriptionResumed) per plan. - Churn —
SubscriptionCancelledevents with theirexpires_at_ts, so you know when each subscriber actually lapses. - Failed or missed collections — expected a pull this period and saw no
SubscriptionTransfer? That subscriber’s payment didn’t go through; flag it for retry or dunning. - Fee splits — a single collect can emit multiple
SubscriptionTransferevents to different receivers (for example a 99% / 1% split between a merchant and a platform fee account), and each one decodes cleanly.
