> DOCUMENTATION

BIP47 protocol, Auth47 specification, and API references

> 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.

Example
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

  1. Alice obtains Bob's payment code
  2. Alice creates a notification transaction containing her encrypted payment code
  3. Bob monitors his notification address and detects Alice's transaction
  4. 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:

Formula
// 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

  1. Challenge Generation: Server generates a unique nonce and creates an Auth47 URI
  2. QR Display: User scans the QR code with a compatible wallet
  3. Signature: Wallet signs the challenge using the notification address private key
  4. Verification: Server verifies the signature against the claimed payment code, and that the challenge it was signed over names this server in r
  5. Authentication: Upon successful verification, user is authenticated

Auth47 URI Format

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:

JSON
{
  "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 that r is 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 in r, 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

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

POST /create

Create a new PayNym entry in the database.

Parameter Type Description
code string A valid BIP47 payment code

Get Token

POST /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

POST /nym

Retrieve all known information about a PayNym account.

Parameter Type Description
nym string Payment code, nymID, or nymName
Response
{
  "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

POST /claim

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

POST /follow

Follow another PayNym account.

Parameter Type Description
target string The nymID to follow
signature string Token signed by notification address
POST /unfollow

Unfollow a PayNym account. Same parameters as /follow.

Add Payment Code

POST /nym/add

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

JavaScript
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

JavaScript
// 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

JavaScript
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

JavaScript
// 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

URL Pattern
// 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.

JavaScript
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

POST /api/bip47/verify-message

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
Response
{
  "valid": true,
  "notificationAddress": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
  "nymName": "example",
  "avatarUrl": "/api/paynym/avatar/PM8T..."
}

> Resources

Official specifications, libraries, and community resources for BIP47 and PayNym.

Official Specifications

Libraries

Wallets Supporting BIP47

Community

Contribute: Found an error or want to improve this documentation? Submit a pull request to the GitHub repository .