Why Verify Payments?
Payment verification unlocks a range of features for ATProtocol applications, from supporter recognition to premium content delivery.
Gate Content
Restrict access to posts, media, or feeds based on whether a viewer has an active payment relationship with the creator.
Supporter Badges
Display visual indicators next to users who financially support a creator, building community recognition and social proof.
Premium Features
Unlock advanced functionality for paying supporters, such as extended upload limits, priority replies, or custom themes.
Payment History
Show a verifiable timeline of payments between accounts, useful for transparency dashboards and financial reporting.
Sponsorship Proof
Prove sponsorship relationships between accounts on-protocol, enabling verified sponsor listings and partnership displays.
Access Control
Combine payment status with other signals to build nuanced access policies for communities, groups, or collaborative spaces.
Checking Payment Status
The quickest way to check whether a payment exists between two accounts is to call network.attested.payment.lookup on a broker's XRPC endpoint.
Supply the payer and recipient DIDs as query parameters. You can optionally filter by paymentType using the full NSID of the payment collection (e.g. network.attested.payment.recurring). You can also pass one or more brokers DIDs to ensure results have at least one validating signature from a broker you trust, or pass entitlements AT-URIs to filter for payments that grant specific entitlements.
// GET request to broker's XRPC endpoint GET /xrpc/network.attested.payment.lookup ?payer=did:plc:abc123supporter &recipient=did:plc:xyz789creator &paymentType=network.attested.payment.recurring &brokers=did:plc:broker456
{ "payments": [ { "$type": "network.attested.payment.recurring", "uri": "at://did:plc:abc123supporter/network.attested.payment.recurring/3abc", "cid": "bafyreie...", "recipient": "did:plc:xyz789creator", "amount": 500, "currency": "USD", "interval": "monthly", "createdAt": "2026-03-15T10:30:00Z", "entitlements": [ { "uri": "at://did:plc:xyz789creator/com.example.product/3jkl", "cid": "bafyreik..." } ], "signatures": [ { "uri": "at://did:plc:xyz789creator/network.attested.payment.proof/3def", "cid": "bafyreig..." }, { "uri": "at://did:plc:broker456/network.attested.payment.proof/3ghi", "cid": "bafyreih..." } ] } ] }
Broker discovery. It is up to the recipient to contextually hint or broadcast which payment servicers (brokers) they use. The recipient’s list may include more than one broker DID—each with a #AttestedNetwork service endpoint. Your application should resolve these DIDs and help the payer select the best option at that time, whether by preference order, availability, supported payment methods, or other criteria.
Verification Walkthrough
When you need to cryptographically verify a payment record rather than trusting a broker's lookup response, follow these five steps.
-
Fetch the payment record from the supporter's repository using
com.atproto.repo.getRecord. The record lives under thenetwork.attested.paymentcollection in the payer's repo. -
Strip the
signaturesarray from the record. The signatures are not part of the content-addressed payload and must be removed before hashing. -
Prepare attestation metadata. Take each entry from the signatures array, add the repository DID as the signer identity, and strip the
cidandsignaturefields. Insert the resulting object as the$sigfield on the record. -
Serialize and hash. Encode the prepared record as DAG-CBOR, hash the bytes with SHA-256, and wrap the digest as a CIDv1 with the DAG-CBOR codec (
0x71). -
Fetch and compare proof records. Using the
strongRefURIs from the signatures array, retrieve the corresponding proof records from the creator's and broker's repositories. Confirm that the CID you computed matches the CID referenced in each proof record.
Why verify locally? Broker lookup is convenient but requires trusting the broker's response. Local verification lets your app independently confirm that the payment record is authentic and has been attested by the expected parties, without relying on any single intermediary.
Trust Models
Your app can adopt different trust strategies depending on security requirements and the nature of the payment-gated feature.
- Strict Require attestation proofs from both the creator (recipient) and a specific trusted broker. This is the most secure model and is recommended for high-value access control, financial dashboards, or any context where payment fraud would have significant consequences.
- Creator-Trusted Accept any payment record that the creator has attested, regardless of which broker facilitated it. Suitable for social features like supporter badges or shout-outs, where the creator's endorsement is the primary signal.
- Federated Maintain a set of trusted broker DIDs and accept attestations from any of them. This balances security with flexibility, allowing your app to work with multiple brokers while still filtering out unknown or untrusted attestors. Good for platforms that aggregate content from many creators.
Initiating Payments
Your app can trigger a payment flow on behalf of the user, guiding them through the process without leaving your interface until checkout.
- Resolve the recipient's DID document to discover their available payment servicers. Use the identity resolution layer of ATProtocol to fetch the DID doc.
-
Find
#AttestedNetworkservice endpoints in the DID document. Each entry represents a broker or servicer that handles payments for this recipient. - Present available servicers to the user. If multiple endpoints exist, let the user choose which payment provider they prefer.
-
Call
network.attested.payment.initiateon the chosen servicer's XRPC endpoint, passing the product identifier and the payer's DID. - Redirect the payer to the returned URL. The servicer responds with a checkout URL and a polling token. Open the URL in the user's browser to complete the payment.
-
Poll
network.attested.payment.statuswith the token to track the payment outcome. On success, the response includes astrongRefpointing to the newly created payment record.
sequenceDiagram
participant App
participant DIDDoc as Recipient DID Doc
participant Servicer as Payment Servicer
participant Browser as Payer Browser
App->>DIDDoc: Resolve recipient DID
DIDDoc-->>App: Service endpoints (#AttestedNetwork)
App->>Servicer: network.attested.payment.initiate
Servicer-->>App: checkoutUrl + pollingToken
App->>Browser: Redirect to checkoutUrl
Browser->>Servicer: Complete payment
Servicer-->>Browser: Payment confirmation
loop Poll for result
App->>Servicer: network.attested.payment.status(token)
end
Servicer-->>App: strongRef (success) or error (failed)
{ "recipient": "did:plc:xyz789creator", "payer": "did:plc:abc123supporter", "product": "monthly-subscription" }
{ "checkoutUrl": "https://broker.example.com/pay/sess_abc123", "token": "poll_tok_abc123" }
{ "status": "completed", "paymentRef": { "uri": "at://did:plc:abc123supporter/network.attested.payment/3abc", "cid": "bafyreie..." } }
Private Payments
Payment records stored in Permissioned Data Spaces follow the same verification mechanics described above.
The only difference is access: records in a permissioned space are not publicly readable. Your application must hold valid space credentials to fetch the payment record and its associated proof records. Once retrieved, the verification steps (stripping signatures, computing the CID, and comparing against proof records) are identical to public payments.
Credential requirements. Contact the space operator to obtain read credentials. Your app will need to present these credentials when calling com.atproto.repo.getRecord for records stored in the permissioned space. The broker lookup endpoint may also require credentials if the broker enforces access control on private payment data.