Did a bit of parallel development - First & Second impressions

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.
  • 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.

2 Likes

Lexicon: Alternative suggestion

Idea proposal - hybrid lexicon: more complete payments interface, separate entitlements concept. Simplifies my lexicon to bring it more in line with attested.network, while still keeping the auth-scoped calls. Using a theoretical atpaym.net domain to seperate payment concern from entitlement concern.

Currently, everything should run off of the recipient app view (eg a storefront), as they will be the entity with payer oauth, recipient metadata, and knowledge of broker compatibility. The protocol is not at the point where payer, recipient, and broker could operate independently over sync.

Payments

A compliant broker must implement everything in this section.

Records

NSID Type Description
net.atpaym.profile record Per-user payment settings; brokers and accepts-flags.
net.atpaym.cart record Shopping cart of items / subscriptions, payable in one checkout
net.atpaym.item record A one-time payment from sender to recipient
net.atpaym.subscription record A recurring payment from sender to recipient

Sender endpoints (net.atpaym.payment.*)

NSID Type Description
payment.cart.create procedure Create a cart.
payment.cart.get query Hydrate a cart by uri.
payment.item.create procedure Create a one-time payment. Shortcuts cart if not provided.
payment.item.get query Hydrate an item by uri.
payment.subscription.create procedure Create a recurring payment. Shortcuts cart if not provided.
payment.subscription.cancel procedure Cancel a recurring payment.
payment.subscription.get query Hydrate a subscription by uri.
payment.verify query Verify payment status; return a synthetic entitlement record.

Recipient endpoints (net.atpaym.recipient.*)

NSID Type Description
recipient.payment.cart.get query Hydrate an incoming cart by uri.
recipient.payment.item.get query Hydrate an incoming item by uri.
recipient.payment.subscription.get query Hydrate an incoming subscription by uri.
recipient.payment.verify query Recipient-side mirror of payment.verify.

Repo endpoints (net.atpaym.repo.*)

NSID Type Description
repo.profile.get query Public profile lookup by DID. payment readiness.

Entitlements

This would eventually be stored in a private space. Could be stored in public space, depending on data. Initial would rely on the record returned & signed by broker.

NSID Type Description
network.attested.entitlements record Simplified version of network.attested.payment.*: keeps subject, entitlements[], signatures[]; adds broker did:web

Optional surface

A compliant broker MAY omit anything in this section. Capabilities could be published in did.json while clients would need to programmatically check for compatibility for graceful degredation or refusal.

Sender endpoints

NSID Type Description
payment.cart.put procedure Update a cart record.
payment.cart.list procedure Filter cart records owned by the authed user.
payment.item.put procedure Update an item record.
payment.item.list query Filter items sent by the authed user
payment.subscription.put procedure Update a subscription record.
payment.subscription.list query Filter subscriptions sent by the authed user.

Recipient endpoints

NSID Type Description
recipient.payment.cart.list query Filter incoming carts.
recipient.payment.item.list query Filter incoming items.
recipient.payment.subscription.list query Filter incoming subscriptions.
recipient.profile.get query ref#self w/ payment secrets. No provisioning? Not necessary.
recipient.profile.put procedure Update authed user’s profile. Brokers that don’t handle server-side provisioning can skip this or treat as no-op. Generally will be used by the broker’s own management interface.

Repo endpoints

NSID Type Description
repo.item.count query Filter & count items for a record. Default completed.
repo.subscription.count query Filter & count active subscriptions to a subject.

Notes

  • Privacy. cart, item, subscription records are private by default, stripping identity (in my terms, subject/record. In your terms, subject/entitlement) from records written to PDS.
  • Signatures. cart, item, subscription carry a signatures array per the badge.blue scheme; the recipient’s client (storefront) signs each canonical record before storage in PDS. This allows the broker to verify the record’s integrity and origin (no editing in a discount).
  • Additional Entitlements Scope. network.attested.entitlements scope would cover how one would transfer and revoke an entitlement.
  • Entitlements Privacy. network.attested.entitlements could be written publicly, but should be default private, synthesized from broker verify. This will limit discoverability, but keeps the private-first mindset. Migrate to 3-party signing within private spaces when widely available.

Out of Scope

  • Inter-service communication. This would probably be in scope at some point as polling for status is super inefficient and it would be better to look into something like webhooks or service-auth + direct websockets. If we’re going to have generic broker relationships, then we’d need a generic inverted relationship as well.
  • What happens after payment is verified: logistics, receipt, shipping. There are a few discovery endpoints in the broker optional calls that would aid in this, but otherwise it’s up to the recipient AppView.