So I’ve been working on something in parallel to this working group. I’ve actually got a broker implemented (Stripe, of course, because that’s easiest) on my own lexicon and I’m happy to share some of my findings:
- First, here’s my implementation: https://atiproto.com/docs/get-started
It started as a tip/subscription service for an AppView I was working on, but quickly became its own thing. I was thinking of migrating to atpayments.social or atpaym.net as my root lexicon. - payments.oneTime/recurring/scheduled are not enough. I started with that but quickly had to switch to a carts concept to hold groups of items, each of which could be recurring or one-time. I think you have similar with a single payment record and a list of entitlements, but I found it more straightforward to set payment details on individual entitlements with the cart containing a total. This is because:
- Checkout needs a trust component as well. A user is able to write to their PDS, so between putting together and completing a checkout, a user has the chance to modify their entitlements array and amounts. I’ve had to implement this in a way that server-side hydration wins if a cart/item isn’t update via the server. Might adopt the badge.blue attestation myself pre-checkout for trust verification.
- A lot of what I’ve implemented has been around user trust, actually.
- Inter-service auth will not support writing directly to user PDS at this stage. It’s actually hard-coded in the PDS to ignore echo-back service-tokens. You need a first-class auth token to write to user PDS. This could change with spaces as you can create a 3-party space between payer, recipient, and broker, each using their own tokens to access the space.
- Right now, I’m getting around this via a multi-step write from the client using a specialized workflow implementation. Others use long-lived oauth tokens directly on the xrpc server. See: [Proposal] Empower service auth tokens to write from proxy · bluesky-social/atproto · Discussion #4877 · GitHub
- network.attested.payment.initiate may need to be more opinionated. Unless I’m misunderstanding, it’s intended to go to the broker with a product uri? That doesn’t seem like enough information for the broker to generate the checkout flow.
- To me, it would be called from the recipient’s AppView using the payer’s oauth credentials through to the broker. At that point it would be initialized with a payment record and associated entitlements. The recipient AppView would redirect the user to the broker workflow, then fetch a signed and attested record from the broker once the payment is complete.
- I could just be misreading your spec and that is your intent. It’s just not clear who the intended recipient of that procedure is, nor what values it would expect.
- I would expect payment records to contain the broker for ease of lookup and filtering.
I really like the badge.blue implementation of verification. It would cleanly remove the need for my broker-only database verification. I do agree with iame.li’s conlcusion in the strawman post that it is very complex for what it is doing and for it to be adopted, we’d need packages that abstract away most of this. I’d build it into my Agent, for example. Most developers would find it easier to validate over broker query and would trust broker response. The broker itself could manage 2 or 3 party validation, or validate on its own (for example, by looking up stripe transaction ID and status). Currently, due to technical limitations, you’d have to poll until status is complete in order to update the user PDS with attestation signatures from the broker.
I don’t agree with iame.li that multi-PDS is the way forward as you run into issues with write-and-sync permissions and managing long-lived oauth tokens. Better to wait for spaces in that case.
You also seem to have come to the same conclusion I did regarding the actual infrastructure around payment records, or perhaps you didn’t consider it in the first place: I leave it up to the client to determine what is being attached to the payment. Originally, I had set up a lot of profile information around what was accepted/not. Knocked that down to a few simple on/off switches.
The biggest difference between the two is that I swung for scoped endpoints, and a more CRUD-oriented mindset. I specifically wanted to lock down using the endpoints to look up records that you are not party to. I think this is something you should strongly consider adding to the spec as it substantially reduces the privacy scope: you can still search a user’s PDS for the records (or even just pull them out using sync), but it at least doesn’t let you search a broker for any given user’s transactions with filters and validation.
Privacy is key
As Dave put it in another post. Privacy is the hard part right now. I resolved this in my implementation by removing the recipient information from payment records when the payment is marked as private as well as scoping all endpoints to either a payer or a recipient.
I am already planning a migration to spaces, which would resolve a lot of the issues we see: privacy-by-default since there is no actual reason why transactions should be public; shared write access means the 3 parties could independently sign the attestation; it would also clean up about half the database that I’m working with.
Client access does become more difficult, so I do like your idea of storing broker in did document. Might be easier to have it be part of a profile lexicon (since editing did.json is somewhat a pain). If I was making my implementation multi-broker, I’d add it to com.atiproto.profile ref#self. So could be stored as network.attestation.profile with the recipient AppView adding brokers as they are used.
Final Thoughts:
- I don’t think this can be implemented, as written, before spaces is widespread.
- Should we split entitlement and payment as a concept? The cart → checkout → payment flow and entitlements records could be considered somewhat separate concerns, only linked by the fact that the broker is one of the parties to attestation. It would allow us to add additional payment-oriented properties to entitlements without overriding the entitlement record.
- for example, I’ve implemented a wildly simply tipping AppView: https://skytip-simple.kayakyakr.workers.dev/. Lets say I upgraded that to allow me to tip specific posts, all in a single payment. I want to see line items of amounts related to entitlement. Storing tip value on the entitlement as part of the payment would mean I have to wrap the user aturi in an AppView-specific record. That then defeats the purpose of the universal attestation as downstream AppViews need knowledge of my tipping AppView.
- Could also be resolved by switching to payment → [payment-items → entitlement]
Feel free to reach out directly as well. I’m gonna push something forward as I have an implementation that works with atproto right now as well as an idea for how it would work in the upcoming changes, so I’d love to collaborate.