Podcast Subscription
A podcast offers a $10/month premium subscription. Supporters pay through a broker, which creates a recurring payment record with an entitlement that grants access to premium content. Any app, tile, or appview can verify the subscription by checking for an active payment with the matching entitlement.
How it works
The podcast creator publishes their content on ATProtocol and uses a broker to handle subscriptions. When a listener subscribes, three things happen:
- The listener’s client initiates a payment through the broker via
network.attested.payment.initiate - The broker processes the $10/month payment and writes a
network.attested.payment.recurringrecord to the listener’s repository, including anentitlementsreference pointing to the podcast’s product record - The broker and creator each write
payment.proofattestation records to their own repositories, linked via the payment’ssignaturesarray
From that point on, any app can verify the subscription by calling network.attested.payment.lookup with the entitlement AT-URI. No centralized subscription database needed.
sequenceDiagram
participant L as Listener's Client
participant B as Broker
participant LR as Listener's Repo
participant CR as Creator's Repo
participant BR as Broker's Repo
participant App as Podcast App
L->>B: network.attested.payment.initiate
B-->>L: { token, url }
L->>B: Complete payment ($10/month)
B->>LR: Write payment.recurring record
Note right of LR: Includes entitlements[]
pointing to product record
par Attestations
B->>BR: Write payment.proof
B->>CR: Notify creator
CR->>CR: Write payment.proof
end
App->>B: network.attested.payment.lookup
?payer=...&recipient=...
&paymentType=...recurring
&entitlements=at://...product/...
B-->>App: { payments: [recurring record] }
App->>App: Subscription verified, grant access
The payment record
The recurring payment record lives in the listener’s repository. It declares the $10/month commitment, references the podcast’s product record as an entitlement, and carries attestation signatures from both the creator and broker.
{ "$type": "network.attested.payment.recurring", "subject": "did:plc:podcast-creator", "amount": 1000, "currency": "USD", "unit": "monthly", "frequency": 1, "txnid": "01J6M4R5XQHV8WNBCM3G9RFBT", "createdAt": "2026-03-01T00:00:00.000Z", "entitlements": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:podcast-creator/com.example.podcast.subscription/premium", "cid": "bafyreig7xxgb5tcoqd3ne6gqkvrulzpfnwjmcc5fsgqjdx4huswnhzbekcc" } ], "signatures": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:podcast-creator/network.attested.payment.proof/3la8rxz3vdc4t", "cid": "bafyreigyh7s6lqf5n3xke4jt6r2x3mqkzf4wpgicbqhqg5k3vdjn7aomfe" }, { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:broker-payments/network.attested.payment.proof/3la8ry4ldsc4u", "cid": "bafyreih7wwfa3tcoqd2ne5gqkvrulzpfnwjmcc5fsgqjdx4huswnhzaehqu" } ] }
Verifying access
When a listener opens the podcast in any ATProtocol app, the app checks for an active subscription by querying the broker’s lookup endpoint. The key is filtering by both the recurring payment type and the specific entitlement.
// Does this listener have an active premium subscription? GET /xrpc/network.attested.payment.lookup ?payer=did:plc:listener123 &recipient=did:plc:podcast-creator &paymentType=network.attested.payment.recurring &entitlements=at://did:plc:podcast-creator/com.example.podcast.subscription/premium
If the response contains a payment record, the listener has an active subscription and the app grants access to premium episodes. If the payments array is empty, the listener either hasn’t subscribed or their subscription has lapsed.
App-agnostic verification. Because the payment record and its entitlements live on the protocol, any podcast app, tile, or appview can independently verify the subscription. The listener isn’t locked into a single client—their subscription follows them across the ecosystem.
Entitlements are flexible. The com.example.podcast.subscription/premium record is defined by the podcast creator, not by this spec. It could contain tier details, feature flags, or access rules—whatever makes sense for the product. The payment record simply references it as a strongRef, linking proof of payment to what was purchased.
One-Time Purchase
A user makes a one-time payment to unlock something tied to a specific recipient—a profile badge in a social app, a tip that comes with an award, downloadable content, or any other digital good. The payment is linked to an entitlement record that the app checks to gate access or display.
How it works
A user wants to support a creator and get something in return—maybe a supporter badge that appears on their profile, or access to bonus content. The app presents the option, the user pays through a broker, and the resulting payment record carries an entitlement that any app can verify.
- The user selects a purchase option in the app (e.g. “Buy Supporter Badge” or “Unlock Bonus Pack”). The app initiates the payment via
network.attested.payment.initiateon the creator’s broker - The broker processes the one-time payment and writes a
network.attested.payment.oneTimerecord to the payer’s repository. The record includes anentitlementsreference pointing to the specific product—a badge, an award, DLC content, or whatever the creator has defined - The broker and creator each write
payment.proofattestation records to their own repositories, completing the cryptographic chain
When any app wants to check whether the user has purchased that item, it calls network.attested.payment.lookup filtered by the entitlement AT-URI. If a matching one-time payment exists, the user has paid and the app renders the badge, unlocks the content, or grants whatever the entitlement represents.
sequenceDiagram
participant U as User's Client
participant B as Broker
participant UR as User's Repo
participant CR as Creator's Repo
participant BR as Broker's Repo
participant App as Social App
U->>B: network.attested.payment.initiate
B-->>U: { token, url }
U->>B: Complete payment (one-time)
B->>UR: Write payment.oneTime record
Note right of UR: Includes entitlements[]
pointing to badge/product record
par Attestations
B->>BR: Write payment.proof
B->>CR: Notify creator
CR->>CR: Write payment.proof
end
App->>B: network.attested.payment.lookup
?payer=...&recipient=...
&entitlements=at://...badge/...
B-->>App: { payments: [oneTime record] }
App->>App: Show badge on profile
The payment record
The one-time payment record lives in the payer’s repository. In this example, a user pays $5 to get a supporter badge on a creator’s profile. The entitlement references the creator’s badge record.
{ "$type": "network.attested.payment.oneTime", "subject": "did:plc:creator-xyz", "amount": 500, "currency": "USD", "txnid": "01J7N5S6YRHW0XPBDN4H1UHEV", "memo": "Supporter badge", "createdAt": "2026-03-20T14:30:00.000Z", "entitlements": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:creator-xyz/com.example.app.badge/supporter", "cid": "bafyreif8xxgb6tcoqd4ne7gqkvrulzpfnwjmcc6fsgqjdx5huswnhzbekdd" } ], "signatures": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:creator-xyz/network.attested.payment.proof/3ld4txz5xfe5v", "cid": "bafyreigyh8s7lqf6n4xke5jt7r3x4mqkzf5wpgicbqhqg6k4vdjn8bomge" }, { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:broker-payments/network.attested.payment.proof/3ld4ty5mdtc5w", "cid": "bafyreih8wwfa4tcoqd3ne6gqkvrulzpfnwjmcc6fsgqjdx5huswnhzafiqu" } ] }
Verifying the purchase
When an app renders a user’s profile, it checks whether the user has purchased the supporter badge for that creator by querying the lookup endpoint with the entitlement AT-URI.
// Does this user have the supporter badge for this creator? GET /xrpc/network.attested.payment.lookup ?payer=did:plc:user456 &recipient=did:plc:creator-xyz &paymentType=network.attested.payment.oneTime &entitlements=at://did:plc:creator-xyz/com.example.app.badge/supporter
If the payments array contains a matching record, the app displays the badge on the user’s profile. Because this is a one-time payment, it persists indefinitely—there’s no renewal to check.
Many forms, same pattern. This scenario covers any one-time digital purchase: supporter badges, tip awards, DLC content packs, custom emoji sets, premium filters, or exclusive stickers. The entitlement record is defined by the app or creator—the payment spec just links proof of payment to whatever was purchased. Different apps can define different products, all verified the same way.
Entitlement Transfer
A user purchases a concert ticket, can’t make it, and transfers the entitlement to someone else through the broker. The original payment is marked as transferred, and a new payment record is created in the recipient’s repository referencing the transfer transaction.
Step 1: The original purchase
Nick buys a ticket to TheGreatestBand’s concert on May 1st through TicketsForReal. This is a standard one-time payment with the concert ticket as an entitlement.
{ "$type": "network.attested.payment.oneTime", "subject": "did:plc:thegreatestband", "amount": 7500, "currency": "USD", "txnid": "01J8P6T7ZSHX1YQCEO5I2VIFV", "memo": "Concert ticket - May 1st 2026", "createdAt": "2026-03-10T09:00:00.000Z", "entitlements": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:thegreatestband/com.example.tickets.event/may1-2026", "cid": "bafyreig9xxgb7tcoqd5ne8gqkvrulzpfnwjmcc7fsgqjdx6huswnhzbekee" } ], "signatures": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:thegreatestband/network.attested.payment.proof/3le5uxz6yge6w", "cid": "bafyreigyh9s8lqf7n5xke6jt8r4x5mqkzf6wpgicbqhqg7k5vdjn9comhe" }, { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:ticketsforreal/network.attested.payment.proof/3le5uy6neuc6x", "cid": "bafyreih9wwfa5tcoqd4ne7gqkvrulzpfnwjmcc7fsgqjdx6huswnhzbgjru" } ] }
At this point, Nick’s repository has the payment record, TheGreatestBand’s repository has a proof record, and TicketsForReal’s repository has a proof record. Standard one-time purchase flow.
Step 2: Initiating the transfer
Nick can’t make the concert and decides to transfer the ticket to Ted. He initiates the transfer through the TicketsForReal website. The broker updates the status field on its proof record to mark the original payment as transferred.
{ "$type": "network.attested.payment.proof", "cid": "bafyreih9wwfa5tcoqd4ne7gqkvrulzpfnwjmcc7fsgqjdx6huswnhzbgjru", "status": "transferred" }
Any app that verifies Nick’s ticket will now see the broker’s proof status is transferred, indicating the entitlement is no longer valid for Nick. The original payment record in Nick’s repository remains untouched—it still serves as a receipt proving the purchase happened.
Step 3: Claiming the transfer
Ted logs in to the TicketsForReal website to claim the transferred ticket. The broker creates a new payment.oneTime record in Ted’s repository. This record references the same concert entitlement but with a new transaction ID for the transfer, and carries its own proof records from both TheGreatestBand and TicketsForReal.
{ "$type": "network.attested.payment.oneTime", "subject": "did:plc:thegreatestband", "amount": 0, "currency": "USD", "txnid": "01J9Q7U8ATHX2ZRDFO6J3WJGW", "memo": "Transfer from did:plc:nick", "createdAt": "2026-04-15T16:45:00.000Z", "entitlements": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:thegreatestband/com.example.tickets.event/may1-2026", "cid": "bafyreig9xxgb7tcoqd5ne8gqkvrulzpfnwjmcc7fsgqjdx6huswnhzbekee" } ], "signatures": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:thegreatestband/network.attested.payment.proof/3lf6vyz7zhe7x", "cid": "bafyreigyh0s9lqf8n6xke7jt9r5x6mqkzf7wpgicbqhqg8k6vdjn0domie" }, { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:ticketsforreal/network.attested.payment.proof/3lf6vz7ofvc7y", "cid": "bafyreih0wwfa6tcoqd5ne8gqkvrulzpfnwjmcc8fsgqjdx7huswnhzbhksu" } ] }
The full flow
sequenceDiagram
participant Nick as Nick's Client
participant B as TicketsForReal
participant NR as Nick's Repo
participant TR as Ted's Repo
participant CR as TheGreatestBand's Repo
participant BR as TicketsForReal's Repo
participant Ted as Ted's Client
Note over Nick,BR: Step 1: Original Purchase
Nick->>B: network.attested.payment.initiate
B-->>Nick: { token, url }
Nick->>B: Complete payment ($75)
B->>NR: Write payment.oneTime
Note right of NR: entitlements[] → concert ticket
par Attestations
B->>BR: Write payment.proof
B->>CR: Notify TheGreatestBand
CR->>CR: Write payment.proof
end
Note over Nick,BR: Step 2: Transfer
Nick->>B: Initiate transfer to Ted
B->>BR: Update proof status → "transferred"
Note over Nick,BR: Step 3: Ted Claims
Ted->>B: Claim transferred ticket
B->>TR: Write payment.oneTime
Note right of TR: Same entitlement,
new txnid for transfer
par Attestations
B->>BR: Write new payment.proof
B->>CR: Notify TheGreatestBand
CR->>CR: Write new payment.proof
end
Ted->>Ted: Ticket verified ✓
Transfer verification. When an app checks who holds the ticket, it queries network.attested.payment.lookup with the concert entitlement AT-URI. Nick’s original payment will no longer match—the broker’s proof has a transferred status. Ted’s payment record, with fresh proofs from both TheGreatestBand and TicketsForReal, is the current valid holder.
Broker-managed process. Transfers are facilitated entirely by the broker. The broker controls the transfer policy—whether transfers are allowed, whether fees apply, how many times a ticket can change hands. The spec provides the primitives (proof status, new payment records) but the business logic is up to the broker.
Consumable Entitlements
A “punch card” model where a one-time purchase grants a fixed number of uses. Each use is tracked by the seller updating their proof record, while the broker’s attestation remains unchanged throughout.
Step 1: Purchasing the punch card
Nick visits spellcheck.me and buys a punch card for 5 uses of their editing service for $20. This is a standard one-time purchase, but the seller’s proof record includes a uses field initialized to 0.
{ "$type": "network.attested.payment.oneTime", "subject": "did:plc:spellcheckme", "amount": 2000, "currency": "USD", "txnid": "01JA0R8V9BIY3ASEGP7K4XKHX", "memo": "Punch card - 5 edits", "createdAt": "2026-04-01T10:00:00.000Z", "entitlements": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:spellcheckme/com.example.spellcheck.punchcard/5pack", "cid": "bafyreig0xxgb8tcoqd6ne9gqkvrulzpfnwjmcc8fsgqjdx7huswnhzbekff" } ], "signatures": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:spellcheckme/network.attested.payment.proof/3lg7wyz8aif8y", "cid": "bafyreia1xxhc9udrr7of0hu0s9x7nrlzpgnxjndd8gtrkex8m7wekp1epjf" }, { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:broker-edits/network.attested.payment.proof/3lg7wz8pgvc8z", "cid": "bafyreih1wwfa7tcoqd6ne9gqkvrulzpfnwjmcc9fsgqjdx8huswnhzbiltu" } ] }
The seller’s proof record starts with a uses counter at 0, indicating none of the 5 credits have been consumed yet:
{ "$type": "network.attested.payment.proof", "cid": "bafyreia1xxhc9udrr7of0hu0s9x7nrlzpgnxjndd8gtrkex8m7wekp1epjf", "uses": 0 }
The broker’s proof is a standard attestation with no usage tracking:
{ "$type": "network.attested.payment.proof", "cid": "bafyreih1wwfa7tcoqd6ne9gqkvrulzpfnwjmcc9fsgqjdx8huswnhzbiltu" }
Step 2: Using a credit
Nick writes a Leaflet post and wants it reviewed, so he visits spellcheck.me and submits his article. When the service processes the request, two things happen atomically:
-
The seller updates their proof record. The
usesfield increments from0to1. Because the proof record’s content changed, itscidis recomputed to reflect the new data. -
The payment record in Nick’s PDS updates. The
signaturesarray’sstrongReffor the seller’s proof is updated to point to the new CID, keeping the reference in sync with the changed proof.
{ "$type": "network.attested.payment.proof", "cid": "bafyreia2yyid0vesr8pg1iv1t0y8oslzqhoyknee9huskfgy9n8xflq2fqg", "uses": 1 }
{ "$type": "network.attested.payment.oneTime", "subject": "did:plc:spellcheckme", "amount": 2000, "currency": "USD", "txnid": "01JA0R8V9BIY3ASEGP7K4XKHX", "memo": "Punch card - 5 edits", "createdAt": "2026-04-01T10:00:00.000Z", "entitlements": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:spellcheckme/com.example.spellcheck.punchcard/5pack", "cid": "bafyreig0xxgb8tcoqd6ne9gqkvrulzpfnwjmcc8fsgqjdx7huswnhzbekff" } ], "signatures": [ { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:spellcheckme/network.attested.payment.proof/3lg7wyz8aif8y", "cid": "bafyreia2yyid0vesr8pg1iv1t0y8oslzqhoyknee9huskfgy9n8xflq2fqg" }, { "$type": "com.atproto.repo.strongRef", "uri": "at://did:plc:broker-edits/network.attested.payment.proof/3lg7wz8pgvc8z", "cid": "bafyreih1wwfa7tcoqd6ne9gqkvrulzpfnwjmcc9fsgqjdx8huswnhzbiltu" } ] }
Notice the seller’s signature cid changed to match the updated proof record, but the broker’s signature remains identical.
The full flow
sequenceDiagram
participant Nick as Nick's Client
participant B as Broker
participant NR as Nick's Repo
participant SR as spellcheck.me's Repo
participant BR as Broker's Repo
Note over Nick,BR: Purchase
Nick->>B: network.attested.payment.initiate
B-->>Nick: { token, url }
Nick->>B: Complete payment ($20)
B->>NR: Write payment.oneTime
Note right of NR: entitlements[] → punch card
par Attestations
B->>BR: Write payment.proof
SR->>SR: Write payment.proof (uses: 0)
end
Note over Nick,BR: Use a credit
Nick->>SR: Submit article for review
SR->>SR: Update proof (uses: 0 → 1, new CID)
SR->>NR: Update seller signature strongRef
Note right of BR: Broker proof unchanged
Note over Nick,BR: Use another credit
Nick->>SR: Submit another article
SR->>SR: Update proof (uses: 1 → 2, new CID)
SR->>NR: Update seller signature strongRef
Why the broker proof is unaffected. The attestation CID in the broker’s proof is computed from the payment record without the signatures field. When the seller updates their proof and the payment record’s signatures array changes to point to the new seller CID, the broker’s attestation CID remains valid—it was never based on the signatures in the first place. This is what makes metered usage possible without re-attesting with the broker on every use.
Seller-managed consumption. The seller controls the usage counter in their own proof record. When uses reaches the purchased limit (5 in this case), the seller simply stops accepting new requests. The spec doesn’t enforce the limit—the seller’s application logic does. Any app can read the seller’s proof to see how many credits remain.