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
| Field | Type | Required | Description |
|---|---|---|---|
methods | array of method | yes | Ways to pay this identity, resolved in priority order. |
updatedAt | string (datetime) | yes | When the record was last updated. |
Method
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Known values: lnAddress, bolt12, lnurlp, cashuMint. Clients must ignore unknown types. |
value | string | yes | The endpoint itself: a lud16 address, BOLT12 offer, bech32 LNURL, or mint at-identifier, depending on type. |
priority | integer | no | Resolution 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
methodsin priority order and use the first type they support. - Unknown
typevalues 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:
- Handle → DID.
GET https://{handle}/.well-known/atproto-did, falling back to the DNS TXT record_atproto.{handle}. - DID → DID document.
https://plc.directory/{did}fordid:plc, orhttps://{domain}/.well-known/did.jsonfordid:web. Verify the document'salsoKnownAsclaims the handle. - DID document → PDS. Take the service endpoint with id
#atproto_pds. - PDS → record. Fetch the payment endpoint record (no auth required).
- 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
- Lexicon schema (JSON)
- AT Protocol documentation
- LNURL specifications (LUDs) — LUD-06 pay flow, LUD-16 Lightning addresses
- Nostr Wallet Connect — how the Avocadough app talks to your Lightning wallet
- SparrowTek on Tangled
Building on the payment identity layer? Contact us — we'd love to hear about it.