Developers

Avocadough makes an AT Protocol identity a Bitcoin payment address.
One lexicon, no server, no signups — pay @alice.bsky.social over Lightning.

Overview

An AT Protocol identity is a handle (like alice.bsky.social) anchored to a DID, with a signed data repository the user controls. Avocadough defines one small record type — xyz.avocadough.payment.endpoint — that lets any AT Protocol identity publish how to pay it. Once published, anyone can resolve the handle to a Lightning payment, with nothing but public HTTP requests:

@alice.bsky.social
        |  handle resolution (well-known / DNS)
        v
did:plc:abc123…
        |  DID document (plc.directory)
        v
PDS (alice's data server)
        |  com.atproto.repo.getRecord (public, no auth)
        v
xyz.avocadough.payment.endpoint
        |  preferred method, e.g. lnAddress
        v
[email protected]
        |  LNURL-pay (LUD-16)
        v
BOLT11 invoice  →  pay with any Lightning wallet

The record is settlement-agnostic: it advertises an ordered list of payment methods. Today that's a Lightning address; BOLT12 offers, raw LNURL-pay endpoints, and Cashu mints have reserved types and can be published with zero schema changes. There is no Avocadough server in the loop — resolution reads the user's own PDS, and payment goes peer-to-endpoint over Lightning.

Key concepts

  • Handle & DID — the human name and the stable identifier behind it. Handles can move hosts or change; the DID and its repo persist.
  • PDS — the user's Personal Data Server, which stores their signed records and serves them publicly over XRPC.
  • Lexicon — an AT Protocol schema. Avocadough's lexicons live under the xyz.avocadough.* namespace.
  • Lightning address (lud16)user@domain, resolvable to a BOLT11 invoice via LNURL-pay.

The Lexicon: xyz.avocadough.payment.endpoint

One canonical record per identity, stored at record key self in the user's repo. Machine-readable schema: xyz.avocadough.payment.endpoint.json.

Record

FieldTypeRequiredDescription
methodsarray of methodyesWays to pay this identity, resolved in priority order.
updatedAtstring (datetime)yesWhen the record was last updated.

Method

FieldTypeRequiredDescription
typestringyesKnown values: lnAddress, bolt12, lnurlp, cashuMint. Clients must ignore unknown types.
valuestringyesThe endpoint itself: a lud16 address, BOLT12 offer, bech32 LNURL, or mint at-identifier, depending on type.
priorityintegernoResolution order across methods; lower is preferred. Methods without a priority sort last, in published order.

Example record

{
  "$type": "xyz.avocadough.payment.endpoint",
  "methods": [
    { "type": "lnAddress", "value": "[email protected]", "priority": 0 }
  ],
  "updatedAt": "2026-06-10T12:00:00.000Z"
}

Semantics

  • Record key is the literal self — an identity has exactly one canonical payment endpoint record.
  • Resolvers walk methods in priority order and use the first type they support.
  • Unknown type values are skipped, never an error — new rails can be added without breaking old clients.
  • The record is public, like the rest of an AT Protocol repo. Publish only an address you're happy to associate with your identity.

Resolving a payment endpoint

Resolution is unauthenticated end to end. Given a handle:

  1. Handle → DID. GET https://{handle}/.well-known/atproto-did, falling back to the DNS TXT record _atproto.{handle}.
  2. DID → DID document. https://plc.directory/{did} for did:plc, or https://{domain}/.well-known/did.json for did:web. Verify the document's alsoKnownAs claims the handle.
  3. DID document → PDS. Take the service endpoint with id #atproto_pds.
  4. PDS → record. Fetch the payment endpoint record (no auth required).
  5. Method → payment. For lnAddress: LNURL-pay turns it into a BOLT11 invoice for your amount, payable by any Lightning wallet.

Fetch the record with curl

curl "https://{pds}/xrpc/com.atproto.repo.getRecord?repo={did}&collection=xyz.avocadough.payment.endpoint&rkey=self"

Then turn the Lightning address into an invoice

# [email protected]  →  LNURL-pay discovery
curl "https://getalby.com/.well-known/lnurlp/alice"

# → { "callback": "...", "minSendable": 1000, "maxSendable": ... }

# request a 21 sat (21000 msat) invoice
curl "{callback}?amount=21000&comment=hello"

# → { "pr": "lnbc210n1..." }   ← pay this with any Lightning wallet

Publishing your endpoint

Publishing is a standard authenticated AT Protocol record write to your own PDS — the same operation any Bluesky post is:

POST {pds}/xrpc/com.atproto.repo.putRecord
{
  "repo": "{your-did}",
  "collection": "xyz.avocadough.payment.endpoint",
  "rkey": "self",
  "record": {
    "$type": "xyz.avocadough.payment.endpoint",
    "methods": [
      { "type": "lnAddress", "value": "[email protected]", "priority": 0 }
    ],
    "updatedAt": "2026-06-10T12:00:00.000Z"
  }
}

Or use the SparrowTek atproto CLI, which wraps the whole round-trip:

atproto login your-handle.bsky.social        # app password
atproto payment publish --ln-address [email protected]
atproto payment resolve @your-handle.bsky.social --amount-sats 21

That last command is the entire pipeline in one line: handle → DID → PDS → record → LNURL-pay → BOLT11 invoice.

AvocadoughKit (Swift)

AvocadoughKit is the open-source Swift package that implements this layer — it's what the Avocadough app uses to send to handles, and what Effem uses to pay podcast boost recipients. It sits on top of SparrowTek's CoreATProtocol and provides:

  • PaymentTargetResolver — classifies and resolves anything a user pastes: @handle, DID, Lightning address, BOLT11 invoice, or LNURL.
  • PaymentEndpointRecord — typed lexicon model with priority-ordered methods.
  • PaymentEndpointPublisher — publish, read back, and remove your record.
  • LNURLPayService — Lightning address → BOLT11 (LUD-06/12/16/21).
import AvocadoughKit

// Pay an identity — no auth, no server
let target = try await PaymentTargetResolver().resolve("@alice.bsky.social")
if let lud16 = target.preferredLightningAddress,
   let address = LightningAddress(lud16) {
    let invoice = try await LNURLPayService().invoice(for: address, amountMsat: 21_000)
    // pay invoice.pr with your Lightning stack (e.g. Nostr Wallet Connect)
}

// Become payable
try await PaymentEndpointPublisher().publish(
    lightningAddress: "[email protected]",
    repo: yourDID
)

Resources

Building on the payment identity layer? Contact us — we'd love to hear about it.