WIP - not for production.
Paykit helps apps discover where someone can receive a payment through their Pubky identity. As a meta payment protocol, it also provides a layer for payment-related metadata such as Payment Requests, Payment Proofs, receipts, Receipt Access, and Allowances.
A payee can publish public payment details under their Pubky public key, or share a Private Payment List with another person over an Encrypted Link. Paykit handles the common Pubky storage layout, encrypted message formats, Payment Request messages, receipts, and language bindings needed for that exchange.
Wallets, payment processors, and apps keep control of payment execution, business rules, payment selection, local storage, key rotation, recurring Payment Request scheduling, and timeouts. The SDK derives Paykit Payment Request lifecycle state from the shared private event stream and outbound queue. Paykit is the discovery and exchange layer those systems can integrate.
For canonical protocol vocabulary, see THESAURUS.md.
Paykit Protocol defines the domain rules, data model, and flows for payment discovery and exchange through Pubky Routing.
Paykit uses Pubky as its only network and storage backend. Public data is stored
on Pubky homeservers under paths owned by a Pubky public key, and private Paykit
messages are exchanged through pubky-noise.
The identity-wide Paykit App Registry is stored at:
/pub/paykit/v0/app-registry.json
Public Payment Endpoint Payloads are owned by a registered Paykit App and stored as separate files under:
/pub/paykit/v0/apps/{app_id}/endpoints/{payment_endpoint_identifier}
Public reads use pubky::PublicStorage; authenticated writes use
pubky::PubkySession. Missing files or directories are treated as absent data
rather than protocol errors.
Private Application Messages use pubky-noise. Paykit derives per-counterparty
private folders during the Encrypted Link Handshake, while pubky-noise owns
encryption, file naming, counters, and storage slots.
- Payment Method: the broad human/domain concept for how value can move, such as Bitcoin, Lightning, SEPA, ACH, or a card rail.
- Payment Endpoint Identifier: the machine-readable identifier for a
Payment Endpoint type, such as
btc-lightning-bolt12. - Payment Endpoint Payload: the receiving handle/details for that identifier, such as an address, invoice, offer, IBAN, tag, or JSON descriptor.
- Payment Endpoint: the pairing of a Payment Endpoint Identifier and a Payment Endpoint Payload.
- Payment List: a collection of Payment Endpoints published by a payee or shared privately with a counterparty.
- Private Payment List: a versioned Private Application Message carrying a complete Payment List over an Encrypted Link.
- Payment Request: a private protocol object where a payee asks a payer for one-time or recurring payment. Its lifecycle messages use Event Message semantics.
- Allowance: shared, scoped permission from an Allower to an Allowee that the Allower's wallet may use to handle qualifying Payment Requests automatically. It is not a balance, payment, or wallet-local setting.
- Allower: the party granting an Allowance and controlling the funds.
- Allowee: the authenticated Payment Request sender whose qualifying requests may use the Allowance.
- Payment Amount: decimal
valuetext plus anasset, used by Payment Requests and optional Receipt details. - Payment Proof: method-specific evidence for one concrete payment execution.
- Receipt Access: an Event Message that lets a counterparty retrieve and decrypt an Encrypted Receipt.
Paykit can describe many kinds of payment details as long as payer and payee understand the same Payment Endpoint Identifiers. The recommended identifier convention is documented in specs/payment-endpoint-identifier.md. The convention is recommended for interoperability, but the library only enforces structural path-safety validation.
Public Payment Lists are discoverable by anyone who knows the payee's Pubky public key.
- The payee creates one or more Payment Endpoints.
- The payee registers the publishing Paykit App in the identity-wide App Registry.
- The payee writes each Payment Endpoint Payload under that App ID and its Payment Endpoint Identifier.
- The payee shares their Pubky public key.
- A payer reads the App Registry, then calls
get_payment_listorget_payment_endpointfor the relevant App ID through the Paykit Library or a Language Binding.
Public Payment Lists are observable by anyone with the payee public key. Apps should avoid publishing reusable or correlation-sensitive Payment Endpoint Payloads unless that matches the payee's privacy model.
Private Payment Lists are shared only with a counterparty over an established Encrypted Link.
- The counterparties create an Encrypted Link with
initiate_encrypted_link/accept_encrypted_linkand advance it withadvance_handshake. - The payee builds a complete Private Payment List containing the counterparty-specific Payment List.
- The payee sends it with
set_private_payment_list. - The payer receives the raw Private Application Message stream with
EncryptedLink::receive_private_application_messagesand parses Private Payment List messages withparse_private_payment_list_json.
Private Payment Lists use Latest-State Message semantics per Paykit App: a
newer list supersedes older queued lists from the same app without replacing
lists from other apps. Only valid list messages participate in latest-state
selection; malformed newer messages do not supersede the latest valid state.
The caller is responsible for maintaining the complete payment_endpoints map
and sending the full desired Payment List for its app on each update.
Paykit helps wallets and processors discover candidate Payment Endpoints. It does not execute payments or choose the final endpoint. The caller decides which Payment Endpoint to use according to its own Payment Selection Policy.
When an Encrypted Link exists, callers can prefer the latest Private Payment List. If no Encrypted Link or Private Payment List is available, callers can fall back to the payee's public Payment List when that is acceptable for the payment's privacy model.
If payment execution fails because an endpoint was consumed, expired, or changed, callers should re-fetch the relevant Payment Endpoint or Payment List and apply their own retry policy.
Payment Endpoint Payloads can represent static or interactive payment flows. Paykit transports the Payment Endpoint Payload; the payment-specific protocol is still implemented by the wallet or processor.
A Payment Endpoint Payload may point to a server, offer, API, or protocol flow that requires the payer to interact before a payment can be executed.
A Payment Endpoint Payload may also contain a static receiving detail such as an on-chain address, reusable offer, bank account detail, payment tag, or similar handle.
Allowances add a consent lifecycle to ordinary Payment Requests without adding an Allowance-specific request or payment message. Either party may propose immutable terms on an exact Encrypted Link, the recipient may accept or reject, and a proposal sender may withdraw while either party may end accepted authority. The SDK/runtime is responsible for durably deriving these shared lifecycle views across restart, backup restore, Event ID replay, and Encrypted Link recovery.
A one-time or Recurring Payment Request remains unchanged: it carries no Allowance ID and follows the normal acceptance, cancellation, Payment Proof, endpoint-resolution, and scheduling rules. At the first automatic-handling decision, a wallet may select exactly one matching accepted Allowance using local priority or explicit user choice. A payment cannot pool multiple Allowances. Without a selection, the ordinary manual path remains available.
Automatic payment remains a wallet decision. The protocol requires a monetary ceiling or expiry in every Allowance. Temporary failures may defer handling; explicit manual-only decisions remain sticky. Recurring requests retain their selected Allowance unless the user authorizes a durable reassociation of future unpaid Billing Periods. Existing payment and reservation history survives that change, preventing duplicate payment across old and replacement Allowances. Payment Proofs may optionally identify the Allowance actually used for an execution. This is informational attribution; proofs do not update usage accounting. The optional field requires peers that support the coordinated pre-release wire extension.
The Library provides stateless matching and limit calculations. The SDK/runtime uses them for candidate evaluation, persists the wallet's selection, and coordinates payment admission, occurrence exclusion, usage reservations, outcome accounting, and recovery. Wallets retain priority rules, consent, scheduling, payment-method validation, signing, execution, settlement detection, and reconciliation against their external execution records. See the SDK integration guide for the implemented flow and the Allowances specification for the eligibility, durability, and recovery rules.
Paykit receipts are encrypted before storage. The plaintext Receipt is created and read locally; the payee stores only the Encrypted Receipt at the canonical homeserver path derived from the Receipt ID, then sends Receipt Access to the counterparty over the Encrypted Link.
Receipt Access uses Event Message semantics: every valid Receipt Access message matters. Apps that process multiple Private Message Kinds should consume the full Encrypted Link Private Application Message stream, persist handled/unhandled state, and only then persist the advanced Encrypted Link snapshot; the snapshot is the local read checkpoint.
Receipt Decryption Keys are sensitive. Callers must not log raw key material and should store it only in platform secure storage.
The Paykit Library is a stateless Rust library for interacting with Paykit Protocol data on Pubky. It is intended to be used inside wallets, payment processors, and apps that already own their payment execution logic.
For release history and upgrade notes, see CHANGELOG.md.
Wallets can use Paykit to publish their own receiving details, discover payment details for contacts or counterparties, exchange Private Payment Lists over an Encrypted Link, and receive Encrypted Receipts.
Payment processors can use Paykit to expose the Payment Endpoints they support, retrieve Payment Lists for payees, and apply their own Payment Selection Policy before executing a payment through their existing infrastructure.
paykit-lib and platform bindings do not:
- execute payments
- choose the final Payment Endpoint for a payer
- manage recurring execution, recurring Payment Request scheduling, or Payment Request lifecycle state
- maintain a stateful background service/runtime
- fetch profiles or contacts
- manage Pubky session creation, authorization scope, key rotation, or account recovery
paykit-sdk is the Rust runtime layer for identity-wide durable
state such as endpoint sync, Encrypted Link snapshots, private stream intake,
Private Payment Lists, Paykit Profiles, Paykit Blob helpers, read-only Pubky
app profile/follows helpers, Contact Records, contact payment resolution,
Payment Requests, Allowance lifecycle views, candidate evaluation, persisted
selection, payment admission, usage accounting, and recovery. Wallet consent,
priority rules, scheduling, signing, payment execution, settlement detection,
external execution reconciliation, product UI, and platform session storage
remain with the integrating application and its adapters.
Since 0.1.0-rc55, PubkySessionBootstrap::republish_identity(public_key)
(republishIdentity(publicKey) in Swift and Kotlin) can rebroadcast an existing
signed Pubky identity record unchanged, without a secret key or restored session.
Applications own scheduling, throttling, retries, and timeouts. See the
Pubky Session Bootstrap API
for return values and limitations.
paykit-libis the canonical Rust Paykit Library. It consumes concrete Pubky SDK handles and keeps no global application state.paykit-sdkis the Rust SDK runtime for stateful Paykit workflows.paykit-ffiexposes UniFFI bindings for Swift and Kotlin.
get_payment_list fetches all public Payment Endpoints published by one Paykit
App under a payee identity. The result is empty when that app has not published
any endpoints.
get_payment_endpoint fetches one Payment Endpoint Payload for a payee, Paykit
App ID, and Payment Endpoint Identifier. Missing files are returned as None.
set_payment_endpoint publishes or updates one Payment Endpoint Payload under
the caller's Paykit App ID and authenticated Pubky session.
remove_payment_endpoint removes a previously published Payment Endpoint.
initiate_encrypted_link, accept_encrypted_link, and advance_handshake
perform the Encrypted Link Handshake. Handshakes and established Encrypted Links
can be serialized and restored by callers that need restart recovery.
Serialized Encrypted Link snapshots include sensitive key material. Store them encrypted at rest and never log them or expose them in telemetry.
set_private_payment_list sends a complete Private Payment List over an
Encrypted Link. The serialized message must fit within one pubky-noise
message.
EncryptedLink::receive_private_application_messages returns the available
Private Application Message batch in send order. SDK/runtime code should persist
and route that raw stream, then use stateless parsers such as
parse_private_payment_list_json, parse_payment_request_event_message,
and parse_receipt_access_event_message. The raw payload is preserved even
when parsed version/kind header fields are missing or malformed.
Payment Request protocol messages are Event Messages. Use
EncryptedLink::receive_private_application_messages when deriving durable
state across multiple Private Message Kinds. Payment Request events can
then be parsed with parse_payment_request_event_message, which keeps the
canonical kind, raw payload, and parse result so malformed recognized messages
can be persisted before the app persists an advanced Encrypted Link snapshot or
treats them as handled.
For outbound idempotency, SDK/runtime code can call
serialize_payment_request_event and persist the exact JSON payload before
sending. A retried send should reuse the same event_id and payload.
Apps issue receipts in retryable steps: prepare_receipt creates the plaintext
Receipt locally, encrypts it into an Encrypted Receipt, and returns the matching
Receipt Access descriptor. store_prepared_receipt stores the Encrypted
Receipt, and send_receipt_access sends access to the counterparty. Receivers
get Receipt Access messages from the raw Private Application Message stream and parse them with
parse_receipt_access_event_message, or parse_receipt_access_json when they
already have a known Receipt Access JSON payload. decrypt_receipt decrypts an
Encrypted Receipt fetched by the app from its Receipt Location into the local
plaintext Receipt.
Receipt Location is a path on the issuer's homeserver; SDK/runtime code pairs
it with the Receipt Access sender/issuer context when retrieving the Encrypted
Receipt.
Private Application Messages share one ordered encrypted stream. The raw stream
API returns every received Private Application Message plaintext payload in
send order, including malformed JSON payloads. Callers that trigger side
effects from Event Messages must persist and reconcile their
own handled/unhandled event state before persisting a snapshot whose read
counter has advanced past those messages. If event state is persisted but the
snapshot is not, replay is expected; Event Messages should be deduped by
event_id, while Receipt Access can also be reconciled by Receipt ID and caller
receipt state.
Paykit v0.2 private wire messages are closed-world JSON objects:
unknown fields are rejected unless a field is explicitly defined as an open JSON
object, such as Payment Request metadata, Payment Proof proof, or Receipt
Metadata.
set_payment_endpoint(session, app_id, identifier, payload): publish or update one app-owned public Payment Endpoint.remove_payment_endpoint(session, app_id, identifier): remove one app-owned public Payment Endpoint.get_payment_list(storage, payee, app_id): fetch one app's public Payment List under the payee identity.get_payment_endpoint(storage, payee, app_id, identifier): fetch one app-owned public Payment Endpoint Payload.
initiate_encrypted_link(...)/accept_encrypted_link(...): start the Encrypted Link Handshake.advance_handshake(...): progress the handshake until it returns a completedEncryptedLink.EncryptedLink::serialize()/restore_encrypted_link(...): snapshot and restore an established Encrypted Link.EncryptedLink::receive_private_application_messages(): receive the full Private Application Message stream batch in send order for SDK/runtime routing.EncryptedLinkHandshake::serialize()/restore_encrypted_link_handshake(...): snapshot and restore an in-progress handshake.
set_private_payment_list(link, list): send a complete Private Payment List over the Encrypted Link.parse_private_payment_list_json(json): parse a Private Payment List from a raw Private Application Message.
send_payment_request(link, app_id, request): send a payee-initiated Payment Request.send_payment_request_acceptance(link, app_id, acceptance): send payer acceptance for a Payment Request.send_payment_request_rejection(link, app_id, rejection): send payer rejection for a Payment Request.send_payment_request_cancellation(link, app_id, cancellation): send payer or payee cancellation for a Payment Request.send_payment_proof(link, app_id, proof): send payer-submitted Payment Proof after payment.parse_payment_request_event_message(message): parse a raw Private Application Message as a Payment Request event when applicable.send_payment_conversion_quote(link, app_id, quote): send immutable recurring rates.serialize_payment_request_event(app_id, event): serialize an app-attributed Payment Request event so SDK/runtime code can persist the outbound payload before sending.PaymentProof::validate_for_request(request): validate stateless proof and request correlation fields.PaymentProof::validate_conversion_quote(request, quote): also validate the selected quote's request, Billing Period and accepted payment asset.
Payment Requests can carry exact conversion rates and payment deadlines. Recurring requests can opt into payee-issued quotes for individual Billing Periods. See Payment conversion and deadlines and the ERC-20 proof profile. These are communication terms; wallets remain responsible for payment execution and verification.
AllowanceTerms::builder(asset): construct validated immutable Allowance Terms.AllowanceProposal,AllowanceAcceptance,AllowanceRejection, andAllowanceEnd: typed lifecycle messages carried byAllowanceEvent.parse_allowance_event_message(message): parse a raw Private Application Message when its kind is recognized as an Allowance event. Malformed recognized messages retain their raw payload and validation result.serialize_allowance_event(app_id, event): serialize an Allowance event for durable outbound storage before sending.send_allowance_proposal(link, app_id, proposal)/send_allowance_acceptance(link, app_id, acceptance)/send_allowance_rejection(link, app_id, rejection)/send_allowance_end(link, app_id, end): send typed lifecycle events over the exact authenticated Encrypted Link.
These paykit-lib helpers are stateless. Use the SDK for durable lifecycle
views and commands; the wallet owns automatic payment decisions and execution.
prepare_receipt(link, draft): build the plaintext Receipt, Encrypted Receipt, and Receipt Access descriptor without storing or sending. Receipt drafts may include optionalpayment_request_idandbilling_periodfields for Payment Request correlation.store_prepared_receipt(session, prepared): store a prepared Encrypted Receipt at its Receipt Location.send_receipt_access(link, app_id, access): send an app-attributed Receipt Access descriptor over the Encrypted Link.parse_receipt_access_event_message(message): parse a raw Private Application Message as a Receipt Access event when applicable.parse_receipt_access_json(json): parse Receipt Access from a raw private JSON payload.decrypt_receipt(encrypted_json, key, location): decrypt a Receipt fetched by the app from its Receipt Location.
Example identifiers:
btc-bitcoin-p2tr
btc-lightning-bolt11
btc-lightning-bolt12
eur-sepa-iban
Example Payment Endpoint Payload:
{
"value": "lnurl1...",
"label": "primary lightning endpoint"
}cargo fmt
cargo clippy --all-targets --all-features
cargo test
cargo doc --no-depsPlatform bindings must be built for every target:
cd paykit-ffi
./build.sh allThe mobile build scripts regenerate ignored Android bindgen/JNI outputs and the
iOS release XCFramework under paykit-ffi/dist/ios. See
paykit-ffi/README.md for platform-specific build and
release notes.
- First draft implementation of paykit library: https://github.com/pubky/paykit-pdk
- Product overview: https://docs.google.com/document/d/1Z1HHdxpkOtelOXJRgPldso4_-lchzs3NL_JqDxCdiu8/edit?pli=1&tab=t.0