Strawman: payment attestation by posting the same record to the same rkey

Excited about a cross-application payment spec on atproto, happy to see that moving forward! I have one piece of recurring confusion with the CID-without-signature scheme – I guess this is badge.blue – so I figured I’d take the chance to write out a strawman proposal for a very simple payment record. Please poke holes in this until I realize what I’m missing!

Scenario: I’m (did:plc:iameli) buying a goose sticker from @goose.art (did:plc:goose) over Stripe (did:web:stripe.com) for $5.00. In this scenario we’re imagining Stripe is all-in on atproto and facilitating atproto-native payments, I guess.

My simplified spec involves all three participants in the exchange: broker, recipient, and payer, writing the same record to the same rkey. So the following will be a com.iameli.money.payment record. I’ve copied as much as possible from network.attested.payment.oneTime to try and make the comparison as clear as possible. The following record (with cid=bafyreiamlxptjneem5b7tuwnrjt36urnlcdpcdogjippqptuqwkklmchkq) is written to these three locations:

  • at://did:plc:iameli/com.iameli.money.payment/01J7N5S6YRHW0XPBDN4H1UHEV
  • at://did:plc:goose/com.iameli.money.payment/01J7N5S6YRHW0XPBDN4H1UHEV
  • at://did:web:stripe.com/com.iameli.money.payment/01J7N5S6YRHW0XPBDN4H1UHEV
{
  "$type": "com.iameli.money.payment",
  "payer": "did:plc:iameli",
  "recipient": "did:plc:goose",
  "broker": "did:web:stripe.com",
  "amount": 500,
  "currency": "USD",
  "txnid": "01J7N5S6YRHW0XPBDN4H1UHEV",
  "memo": "Goose sticker",
  "createdAt": "2026-03-20T14:30:00.000Z",
  "entitlements": [
    {
      "$type": "com.atproto.repo.strongRef",
      "uri": "at://did:plc:goose/com.example.sticker/funny-goose-sticker",
      "cid": "bafyreif8xxgb6tcoqd4ne7gqkvrulzpfnwjmcc6fsgqjdx5huswnhzbekdd"
    }
  ]
}

So now all three of us have the same record at the same rkey. Verifying that we all agree is trivial: in the happy case, all three repos referenced in payer, recipient, and broker all have precisely the same CID; just check those three and you know that everyone agrees. If there’s disagreement that’s similarly easy to detect via CID mismatch and the disagreements can be resolved in the same manner. Records can’t be shifted between repos or anything like that: publishing this record to at://did:plc:malicious-attacker/com.iameli.money.payment/01J7N5S6YRHW0XPBDN4H1UHEV isn’t valid to anybody; that DID isn’t listed in the body of the JSON.

What did I miss?

2 Likes

a few questions:

  1. what do you mean by “disagreements can be resolved in the same manner”? I don’t see a dispute/disagreement resolution mechanism in here.

  2. this seems to rely on all parties’ repos staying accessible. what happens if one party doesn’t renew a domain name for their did:web? or just decides to delete those records?

generally I think signed records would solve a lot of this, but I know that’s more complicated

Sure yeah, both important failure modes that will need to be addressed in any payment system. If attested.network has answers here that won’t work with my data model, then I’ve gotten my answer.

But just briefly: you’ve got a recipient, a payer, and a broker.

  • If the broker defects (or goes down), presumably the payer and the recipient can shrug at each other and use a different broker going forward.
  • If the payer defects, nobody really cares, that’s just you disclaiming the thing; I guess you might be asking for a refund.
  • And if the recipient defects, you’ve still got the payer and broker records in place; applications can still adversarially treat the payment as having occurred.

This is what I’m pushing back against a bit; attested.network has lots of inline signatures. But they’re signed records already, right? Here, have a Merkle proof that at://did:plc:2zmxikig2sj7gqaezl5gntae/com.iameli.money.payment/01J7N5S6YRHW0XPBDN4H1UHEV is present in my repo: https://iameli.com/xrpc/com.atproto.sync.getRecord?did=did:plc:2zmxikig2sj7gqaezl5gntae&collection=com.iameli.money.payment&rkey=01J7N5S6YRHW0XPBDN4H1UHE

If you like, you can serialize that XRPC response alongside my entire did:plc chain and you can prove that that record was, at one point, present in my repo. Ditto for the broker and recipient. Adding more signatures proves what, to whom, under what circumstances?

Thanks for writing this up!

Your proposal has real appeal: three repos holding identical records, and verification is a CID comparison across them. If the goal were purely “everyone agrees on one fact,” I don’t think you’re missing anything. It works, and it’s cheaper than fetching separate proof records.

The gaps I see are about the kinds of changes the system needs to accommodate over a record’s life, and the kinds of metadata each party legitimately wants to carry that the others don’t.

All parties have to be known at record creation. Probably fine in the happy path, but it constrains discovery and extension. Additional attestors can’t chime in later without the original record changing, and a verifier has to already know which DIDs to check before it can verify.

The strong reference is implicit. You’re depending on the tuple (DID, collection, rkey, CID) matching across three repos, but nothing in the record actually says that. com.atproto.repo.strongRef exists to make “this record in this repo at this CID” explicit. If that’s the real dependency, the record should say so. This proposal in this form can’t have that.

Any change requires unanimous consent. This is the biggest one for me. A few cases where it bites:

  • A broker marks a consumable entitlement as used, or increments a punch-card counter. Now the creator and payer have to co-sign a mutation they don’t care about.
  • A refund or chargeback is processed. The creator has no reason to be involved in broker-internal state, but any divergence breaks the CID match.
  • An entitlement is transferred to another identity. In the three-party-same-record model, that’s everyone rewriting their copy in lockstep.

In attested.network the payment record lives only in the payer’s repo; creator and broker proofs live independently in their own repos. Proof CID computation strips the signatures field, so a broker can update its own proof (status, consumption counter, etc.) without invalidating anything else.

No party-specific metadata. A broker might carry internal txnids, fee breakdowns, or escrow state. A creator might attach fulfillment details or product-version info. One shared record means those fields either collide, bloat every copy, or aren’t expressible. Separate proof records let each attestor carry whatever they need without leaking into anyone else’s repo.

TL;DR: for a static “we all agreed this happened” artifact, same-record-same-rkey works. For a payment that evolves (consumption, cancellation, transfer, disputes) and carries party-specific context, the three-repo attestation model buys you independence at the cost of a few extra fetches.