> DOCUMENTATION
> Getting Started
Welcome to the BIP47 documentation. Here you'll find everything you need to understand and implement reusable payment codes, Auth47 authentication, and PayNym identity services.
What is BIP47?
BIP47 defines a protocol for reusable payment codes in Bitcoin. Unlike traditional addresses that should only be used once, payment codes allow you to share a single identifier that others can use to generate unlimited unique payment addresses for you.
What is Auth47?
Auth47 is an authentication protocol built on top of BIP47. It uses the notification address of a payment code to verify ownership, enabling passwordless authentication using Bitcoin wallets.
What is PayNym?
PayNym is a social layer built on BIP47 payment codes. It provides human-readable
identifiers (like +littlevoice) and a social graph of followers/following,
making it easier to discover and pay other Bitcoin users.
Quick Start: New to BIP47? Start with the BIP47 LAB for an interactive walkthrough of how payment codes work.
> BIP47 Protocol
BIP47 enables reusable payment codes that generate unique addresses for each transaction, preserving privacy while maintaining the convenience of a single identifier.
Payment Codes
A BIP47 payment code is a Base58Check-encoded string that starts with "PM8T". It contains a version byte, features byte, sign byte, x-coordinate of the public key, and chain code.
PM8TJJwnXi1t3jv52qM2MMZFWa8wJhj8eyZYcC5cjzEfzENMrxJM9fbnQANqmUSptJdiQmoScyf3Y41SGTPHWpf9PLDVvSSq2UEa8WympaepqxETMgPW
Notification Address
Each payment code has a notification address derived from the public key at index 0. This address is used to signal the creation of a payment channel between two parties.
Creating a Payment Channel
- Alice obtains Bob's payment code
- Alice creates a notification transaction containing her encrypted payment code
- Bob monitors his notification address and detects Alice's transaction
- Both parties can now derive unique payment addresses for each other
Address Derivation
Payment addresses are derived using ECDH (Elliptic Curve Diffie-Hellman) to create a shared secret, which is then used to modify the public key derivation:
// Alice derives payment address for Bob
S = a × B // Shared secret (Alice's private key × Bob's public key)
s = SHA256(S_x) // x-coordinate of shared secret
K = B + s×G // Payment address public key
// Bob derives private key to spend
k = b + s // Bob's private key + shared secret scalar
Key Features
- Reusable: Share one code, receive unlimited payments
- Private: Each payment uses a unique address
- Non-interactive: Sender doesn't need receiver's cooperation
- Deterministic: Same addresses derived by both parties
Important: The notification transaction requires a small amount of Bitcoin (typically 546 sats) to be sent to the notification address. This is a one-time cost per payment channel.
> Auth47 Specification
Auth47 provides a challenge-response authentication protocol using BIP47 payment codes. It enables passwordless login by proving ownership of a payment code's notification address.
Protocol Overview
Auth47 uses the notification address of a BIP47 payment code as the identity. Authentication is performed by signing a challenge with the private key corresponding to the notification address.
Authentication Flow
- Challenge Generation: Server generates a unique nonce and creates an Auth47 URI
- QR Display: User scans the QR code with a compatible wallet
- Signature: Wallet signs the challenge using the notification address private key
- Verification: Server verifies the signature against the claimed payment code, and that the challenge it was signed over names this server in
r - Authentication: Upon successful verification, user is authenticated
Auth47 URI Format
auth47://?c=[&r=][&e=]
Parameters
| Parameter | Description |
|---|---|
nonce |
Unique challenge identifier (16 bytes hex, in the URI hostname position) |
c |
Callback URL where the authentication response is POSTed |
r |
Resource URL. The site the user is authenticating to, and what their wallet displays. If omitted, the callback URL is used. The server must compare this against its own URL when verifying a proof; see Security Considerations. |
e |
Unix timestamp when challenge expires |
Challenge String Format: The challenge that gets signed always uses the r=
parameter format, even when the QR code only contains c=. If no explicit r=
parameter is present, the callback URL is used as the resource value. This ensures compatibility
with all wallet implementations (Samourai, Ashigaru, Sparrow, BlueWallet).
Verification Request
The wallet POSTs the following JSON to the callback URL:
{
"auth47_response": "1.0",
"challenge": "auth47://?r=&e=",
"nym": "PM8T...",
"signature": ""
}
Note: The challenge field always contains the r= format,
where the value is either the explicit resource URL from the QR code, or the callback URL if no
resource was specified. The c= parameter is never included in the signed challenge.
Signature Verification
The server verifies the signature using the notification address derived from the claimed payment code. The signature must be a valid Bitcoin message signature of the challenge URI.
Security Considerations
-
The server MUST verify that the challenge's
r(resource) value is its own URL. The library checks thatris a well-formed URL but cannot know which URL is yours. Without this comparison, an attacker can take a live nonce from your server, present a victim with the same challenge naming the attacker's site inr, and relay the signed proof back to you: the signature is genuine and a session is created in the victim's name. Compare the two as parsed URLs, so a trailing slash is not treated as a different site while a different origin or path is. - Nonces expire after 5 minutes to prevent replay attacks
- Each nonce can only be used once
- Signatures must use the Bitcoin Message Signing format
- Servers should validate the expiry timestamp
Why the first item matters most: the other four all concern whether a proof is fresh and genuine. Only the resource check answers a different question — whether it was made for you. A signature is evidence of what it was signed over, nothing more. Thanks to The Dojo Bay for reporting this gap.
Try it: Test Auth47 authentication on the Auth47 Login page.
> PayNym API
The PayNym API provides endpoints for creating and managing PayNym identities, following other PayNyms, and retrieving payment code information.
Base URL
https://paynym.rs/api/v1/
Public Endpoints
These endpoints do not require authentication and can be used to look up PayNym information.
Create PayNym
Create a new PayNym entry in the database.
| Parameter | Type | Description |
|---|---|---|
code |
string | A valid BIP47 payment code |
Get Token
Update the verification token for authenticated requests. Tokens are valid for 24 hours.
| Parameter | Type | Description |
|---|---|---|
code |
string | A valid BIP47 payment code |
Lookup Nym
Retrieve all known information about a PayNym account.
| Parameter | Type | Description |
|---|---|---|
nym |
string | Payment code, nymID, or nymName |
{
"codes": [
{
"claimed": true,
"segwit": true,
"code": "PM8T..."
}
],
"followers": [{ "nymId": "5iEpU..." }],
"following": [],
"nymID": "wXGgdC...",
"nymName": "littlevoice"
}
Authenticated Endpoints
These endpoints require authentication via the notification address signature.
Authentication Headers
| Header | Description |
|---|---|
auth-token |
The token obtained from /token endpoint |
Claim Payment Code
Claim ownership of a payment code by signing the token with the notification address.
| Parameter | Type | Description |
|---|---|---|
signature |
string | Token signed by BIP47 notification address |
Follow/Unfollow
Follow another PayNym account.
| Parameter | Type | Description |
|---|---|---|
target |
string | The nymID to follow |
signature |
string | Token signed by notification address |
Unfollow a PayNym account. Same parameters as /follow.
Add Payment Code
Add a new payment code to an existing Nym.
| Parameter | Type | Description |
|---|---|---|
nym |
string | Existing nymID, code, or nymName |
code |
string | New payment code to add |
signature |
string | Token signed by primary payment code |
Tip: Use the PayNym Explorer to look up PayNym details and see the API in action.
> Code Examples
Ready-to-use code snippets for common BIP47 and Auth47 operations.
Generate Payment Code
import ecc from '@bitcoinerlab/secp256k1';
import { BIP32Factory } from 'bip32';
import { BIP47Factory } from '@dojo-tools/bip47';
const bip32 = BIP32Factory(ecc);
const bip47 = BIP47Factory(ecc);
// Generate from mnemonic
const seed = bip32.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed);
const account = root.derivePath("m/47'/0'/0'");
const paymentCode = bip47.fromBip32Node(account);
console.log(paymentCode.toBase58());
// PM8TJJwnXi1t3jv52qM2MMZFWa8wJhj8eyZYcC5cjzEfzENMrxJM9fbnQANqmUSptJdiQmoScyf3Y41SGTPHWpf9PLDVvSSq2UEa8WympaepqxETMgPW
Derive Payment Address
// Alice derives address to pay Bob
const alice = bip47.fromBip32Node(aliceAccount);
const bob = bip47.fromBase58(bobPaymentCode);
// Get payment address at index 0
const paymentAddress = alice.getPaymentAddress(bob, 0, 'p2wpkh');
console.log(paymentAddress);
// tb1q8gpft5rpju8lcshfa6at44pev5y0q7kzfwqpg
Package note: these libraries were published as
@samouraiwallet/bip47 and @samouraiwallet/auth47.
They now live in the
dojo-tools
monorepo as @dojo-tools/bip47 and
@dojo-tools/auth47, which is what this site runs and what the
examples below use. The public API is unchanged, but the
@dojo-tools releases require Node 24 or later.
Verify Auth47 Signature
import { Auth47Verifier } from '@dojo-tools/auth47';
// Constructor takes (ecc, callbackUrl) - ECC library first, then the URL.
const verifier = new Auth47Verifier(ecc, callbackUrl);
const proof = {
auth47_response: '1.0',
challenge: challengeUri,
nym: paymentCode,
signature: base64Signature
};
const result = verifier.verifyProof(proof, 'bitcoin');
if (result.result !== 'ok') {
throw new Error(result.error);
}
// REQUIRED: the signature proves what it was made over, not who it was made
// for. verifyProof cannot know which resource is yours, so compare it yourself.
const sameSite = (a, b) => {
try {
const norm = (u) => {
const x = new URL(u);
return x.origin.toLowerCase() + x.pathname.replace(/\/+$/, '') + x.search;
};
return norm(a) === norm(b);
} catch { return false; }
};
const resource = new URL(proof.challenge).searchParams.get('r');
if (!resource || !sameSite(resource, MY_SITE_URL)) {
throw new Error('proof was signed for a different site');
}
// Only now is the payment code in proof.nym authenticated to THIS site.
console.log('Authentication successful!');
Lookup PayNym
// Lookup by nymID, nymName, or payment code
const response = await fetch('https://paynym.rs/api/v1/nym/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nym: '+littlevoice' })
});
const data = await response.json();
console.log(data.nymName); // "littlevoice"
console.log(data.nymID); // "wXGgdC..."
console.log(data.codes[0].code); // Payment code
Get PayNym Avatar
// Preferred: our proxy. Served from this origin and cached,
// so a visitor's browser never contacts a third party.
GET /api/paynym/avatar/{payment_code}
// Example
/api/paynym/avatar/PM8TJJwnXi1t3jv52qM2MMZFWa8wJhj8eyZYcC5cjzEfzENMrxJM9fbnQANqmUSptJdiQmoScyf3Y41SGTPHWpf9PLDVvSSq2UEa8WympaepqxETMgPW
// Upstream origin (contacted by our server, not by the browser)
https://paynym.rs/{payment_code}/avatar
Verify BIP47 Message
Verify that a message was signed by the owner of a BIP47 payment code's notification address. This is useful for proving ownership of a payment code without revealing private keys.
import ecc from '@bitcoinerlab/secp256k1';
import { BIP47Factory } from '@dojo-tools/bip47';
import { networks } from '@dojo-tools/bip47/utils';
import { bitcoinMessageFactory } from '@dojo-tools/bitcoinjs-message';
const bip47 = BIP47Factory(ecc);
const bitcoinjsMessage = bitcoinMessageFactory(ecc);
// Parse the payment code
const paymentCode = 'PM8TJYp8zHvhimVNRjUcEuULfmvmUML6YTbTSnU69MYy...';
const paynym = bip47.fromBase58(paymentCode);
// Get the notification address
const notificationAddress = paynym.getNotificationAddress();
// Verify the signature
const message = 'Hello, BIP47!';
const signature = 'H9...base64-signature...';
const isValid = bitcoinjsMessage.verify(
message,
notificationAddress,
signature,
networks.bitcoin.messagePrefix
);
console.log(isValid ? '✅ Signature verified!' : '❌ Invalid signature');
Try it: Test message verification on the BIP47 Lab page using the Message Verifier tool.
API Endpoint
Verify a message signed with a BIP47 notification address.
| Parameter | Type | Description |
|---|---|---|
paymentCode |
string | BIP47 payment code (Base58 format) |
message |
string | The original message that was signed |
signature |
string | Base64-encoded signature |
{
"valid": true,
"notificationAddress": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"nymName": "example",
"avatarUrl": "/api/paynym/avatar/PM8T..."
}
> Resources
Official specifications, libraries, and community resources for BIP47 and PayNym.
Official Specifications
- BIP47 Specification - Official Bitcoin Improvement Proposal
- BIP47 Github Repository - Authentication protocol specification
- Auth47 Github Repository - Authentication protocol specification
Libraries
- @dojo-tools/bip47 - BIP47 implementation for JavaScript/TypeScript
- @dojo-tools/auth47 - Auth47 verification library
- @bitcoinerlab/secp256k1 - secp256k1 cryptography library
Wallets Supporting BIP47
- Samourai Wallet - Android wallet with full BIP47/PayNym support
- Sparrow Wallet - Desktop wallet with full BIP47/PayNym support
- BlueWallet - Android/iOS wallet with Non-Segwit BIP47 support
- Stack Wallet - Android/iOS/Desktop wallet with BIP47/PayNym integration
- Ashigaru - Android wallet with full BIP47/PayNym support
Community
- PayNym.rs - PayNym directory and API
- Support Bill & Keonne - Sign the petition, donate and get the word out
- TSB Signal Chat - A group chat for BIP47/PayNyms enjoyers. Contact Jordan to join.
Contribute: Found an error or want to improve this documentation? Submit a pull request to the GitHub repository .