Priv Protocol
Getting Started

Your First Private Payment

Complete step-by-step tutorial for sending and receiving private payments

Your First Private Payment

This comprehensive tutorial walks you through every step of sending and receiving a private payment using Priv Protocol. You'll learn to generate meta-addresses, register them on-chain, create payments, scan for incoming funds, and claim them.

Prerequisites

Before starting, make sure you have:

Step 1: Generate a Meta-Address

A meta-address is your private payment identity. Let's create one:

import { generateMetaAddress, encodeMetaAddress } from '@priv/sdk';
import { Connection } from '@solana/web3.js';

// Generate your meta-address keypairs
const { spendKeypair, viewKeypair } = generateMetaAddress();

console.log('Spend Public Key:', spendKeypair.publicKey.toString());
console.log('View Public Key:', viewKeypair.publicKey.toString());

// Encode as a shareable meta-address
const metaAddress = encodeMetaAddress(
  spendKeypair.publicKey,
  viewKeypair.publicKey
);

console.log('Your Meta-Address:', metaAddress);
// Output: priv:ma:a1b2c3d4e5f6789...f9e8d7c6b5a4321...

Important: Store these keypairs securely! The spend keypair controls access to your funds.

// Save keypairs securely (example using localStorage for demo)
const keypairData = {
  spendSecretKey: Array.from(spendKeypair.secretKey),
  viewSecretKey: Array.from(viewKeypair.secretKey),
  metaAddress
};

localStorage.setItem('privKeypairs', JSON.stringify(keypairData));

Step 2: Register Your Meta-Address On-Chain

Register your meta-address so others can send you payments:

import { PrivClient } from '@priv/sdk';

// Set up connection and wallet
const connection = new Connection('https://api.devnet.solana.com');

// Your wallet adapter (replace with your actual wallet)
const walletAdapter = {
  publicKey: yourWallet.publicKey,
  signTransaction: async (tx) => await yourWallet.signTransaction(tx)
};

// Initialize Priv client
const client = new PrivClient(connection, walletAdapter);

// Register your meta-address
try {
  const signature = await client.registerMetaAddress({
    spendPubkey: spendKeypair.publicKey,
    viewPubkey: viewKeypair.publicKey,
    label: 'My Priv Wallet' // Optional label for identification
  });

  console.log('Registration successful!');
  console.log('Transaction signature:', signature);
  console.log('View on explorer:', `https://explorer.solana.com/tx/${signature}?cluster=devnet`);
} catch (error) {
  console.error('Registration failed:', error);
}

What happens: This creates an on-chain record linking your meta-address to your wallet. Others can now look up your meta-address and send you private payments.

Step 3: Create a Payment

Now let's send a private payment to another meta-address:

import { USDC_MINT } from '@priv/common';

// Recipient's meta-address (replace with actual recipient)
const recipientMetaAddress = 'priv:ma:recipient_spend_pubkey_hex...recipient_view_pubkey_hex...';

// Create a private payment
try {
  const { paymentUrl, escrowAddress, claimSecret } = await client.createPaymentLink({
    recipientMetaAddress,
    amount: 1000000n, // 1 USDC (6 decimal places)
    tokenMint: USDC_MINT,
    message: 'Coffee payment', // Optional message
    expiryDays: 7 // Payment expires in 7 days
  });

  console.log('Payment created successfully!');
  console.log('Payment URL:', paymentUrl);
  console.log('Escrow Address:', escrowAddress.toString());
  console.log('Claim Secret:', Buffer.from(claimSecret).toString('hex'));

  // Share the payment URL with the recipient
  // They can claim it by visiting the URL
} catch (error) {
  console.error('Payment creation failed:', error);
}

What happens:

  1. A stealth address is derived for the recipient
  2. An escrow is created holding your tokens
  3. An announcement is posted on-chain
  4. A payment link is generated containing the claim information

If you prefer to handle the claim process manually:

import { generateClaimSecret, hashClaimSecret } from '@priv/sdk';

// Generate claim secret manually
const claimSecret = generateClaimSecret();
const claimHash = hashClaimSecret(claimSecret);

// Create escrow directly
const escrowAddress = await client.createEscrow({
  recipientMetaAddress,
  amount: 1000000n,
  tokenMint: USDC_MINT,
  claimHash,
  expirySlot: (await connection.getSlot()) + 432000 // ~30 days
});

console.log('Escrow created:', escrowAddress.toString());
console.log('Share this claim secret with recipient:', Buffer.from(claimSecret).toString('hex'));

Step 4: Scan for Payments

Recipients need to scan the blockchain to discover payments sent to them:

// Load your keypairs (from Step 1)
const savedData = JSON.parse(localStorage.getItem('privKeypairs')!);
const spendKeypair = Keypair.fromSecretKey(new Uint8Array(savedData.spendSecretKey));
const viewKeypair = Keypair.fromSecretKey(new Uint8Array(savedData.viewSecretKey));

// Scan for payments sent to your meta-address
try {
  const payments = await client.scanForPayments({
    viewPrivkey: viewKeypair.secretKey,
    spendPubkey: spendKeypair.publicKey,
    limit: 100 // Scan last 100 announcements
  });

  console.log(`Found ${payments.length} payments:`);
  
  payments.forEach((payment, index) => {
    console.log(`Payment ${index + 1}:`);
    console.log(`  Amount: ${payment.amount}`);
    console.log(`  Token: ${payment.tokenMint.toString()}`);
    console.log(`  Escrow: ${payment.escrowAddress.toString()}`);
    console.log(`  Stealth Address: ${payment.stealthAddress.toString()}`);
    console.log(`  Timestamp: ${new Date(payment.timestamp * 1000).toISOString()}`);
  });
} catch (error) {
  console.error('Scanning failed:', error);
}

What happens: Your view key is used to check each announcement on-chain. When a match is found, the payment details are decrypted and returned.

Continuous Scanning

For real applications, implement continuous scanning:

// Scan periodically for new payments
const startScanning = () => {
  setInterval(async () => {
    try {
      const newPayments = await client.scanForPayments({
        viewPrivkey: viewKeypair.secretKey,
        spendPubkey: spendKeypair.publicKey,
        since: lastScanSlot // Only scan new announcements
      });

      if (newPayments.length > 0) {
        console.log(`Found ${newPayments.length} new payments!`);
        // Handle new payments (show notification, update UI, etc.)
      }
    } catch (error) {
      console.error('Scan error:', error);
    }
  }, 30000); // Scan every 30 seconds
};

startScanning();

Step 5: Claim a Payment

Once you've found a payment, claim it to receive the tokens:

Method 1: Claim from Payment URL

If you received a payment link:

// Parse the payment URL
const paymentUrl = 'https://app.priv.dev/claim?e=ABC123...&s=DEF456...';
const url = new URL(paymentUrl);

const escrowAddress = new PublicKey(url.searchParams.get('e')!);
const claimSecret = Buffer.from(url.searchParams.get('s')!, 'hex');

// Claim the payment
try {
  const signature = await client.claimPayment({
    escrowAddress,
    claimSecret,
    recipientAddress: walletAdapter.publicKey // Where to receive tokens
  });

  console.log('Payment claimed successfully!');
  console.log('Transaction signature:', signature);
  console.log('View on explorer:', `https://explorer.solana.com/tx/${signature}?cluster=devnet`);
} catch (error) {
  console.error('Claim failed:', error);
}

Method 2: Claim from Scanned Payment

If you found the payment through scanning:

// Use payment found in Step 4
const payment = payments[0]; // First payment from scan results

try {
  const signature = await client.claimPayment({
    escrowAddress: payment.escrowAddress,
    claimSecret: payment.claimSecret, // Extracted during scanning
    recipientAddress: walletAdapter.publicKey
  });

  console.log('Payment claimed successfully!');
  console.log('Received:', payment.amount, 'tokens');
} catch (error) {
  console.error('Claim failed:', error);
}

Gasless Claiming with Relayer

For truly gasless claiming (recipient doesn't need SOL):

// Build unsigned claim transaction
const claimTx = await client.buildClaimTransaction({
  escrowAddress,
  claimSecret,
  recipientAddress: walletAdapter.publicKey
});

// Submit to relayer for gas payment
const response = await fetch('https://relayer.priv.dev/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    transaction: claimTx.serialize({ requireAllSignatures: false }),
    escrowAddress: escrowAddress.toString()
  })
});

if (response.ok) {
  const { signature } = await response.json();
  console.log('Gasless claim successful:', signature);
} else {
  console.error('Relayer submission failed:', await response.text());
}

Step 6: Verify on Explorer

Check your transactions on Solana Explorer:

// View escrow creation
console.log(`Escrow: https://explorer.solana.com/address/${escrowAddress}?cluster=devnet`);

// View claim transaction
console.log(`Claim: https://explorer.solana.com/tx/${claimSignature}?cluster=devnet`);

// Check your token balance
const tokenAccount = await connection.getTokenAccountsByOwner(
  walletAdapter.publicKey,
  { mint: USDC_MINT }
);

if (tokenAccount.value.length > 0) {
  const balance = await connection.getTokenAccountBalance(tokenAccount.value[0].pubkey);
  console.log(`USDC Balance: ${balance.value.uiAmount}`);
}

Complete Example: End-to-End Payment

Here's a complete example that demonstrates the entire flow:

import { 
  Connection, 
  Keypair, 
  PublicKey 
} from '@solana/web3.js';
import { 
  PrivClient, 
  generateMetaAddress, 
  encodeMetaAddress 
} from '@priv/sdk';
import { USDC_MINT } from '@priv/common';

async function completePaymentFlow() {
  // Setup
  const connection = new Connection('https://api.devnet.solana.com');
  const wallet = Keypair.generate(); // Replace with your actual wallet
  
  const walletAdapter = {
    publicKey: wallet.publicKey,
    signTransaction: async (tx) => {
      tx.sign(wallet);
      return tx;
    }
  };

  const client = new PrivClient(connection, walletAdapter);

  try {
    // 1. Generate meta-addresses for sender and recipient
    const senderKeys = generateMetaAddress();
    const recipientKeys = generateMetaAddress();
    
    const senderMetaAddress = encodeMetaAddress(
      senderKeys.spendKeypair.publicKey,
      senderKeys.viewKeypair.publicKey
    );
    
    const recipientMetaAddress = encodeMetaAddress(
      recipientKeys.spendKeypair.publicKey,
      recipientKeys.viewKeypair.publicKey
    );

    console.log('Sender meta-address:', senderMetaAddress);
    console.log('Recipient meta-address:', recipientMetaAddress);

    // 2. Register meta-addresses
    await client.registerMetaAddress({
      spendPubkey: senderKeys.spendKeypair.publicKey,
      viewPubkey: senderKeys.viewKeypair.publicKey,
      label: 'Sender Wallet'
    });

    await client.registerMetaAddress({
      spendPubkey: recipientKeys.spendKeypair.publicKey,
      viewPubkey: recipientKeys.viewKeypair.publicKey,
      label: 'Recipient Wallet'
    });

    console.log('Meta-addresses registered!');

    // 3. Create payment
    const { paymentUrl, escrowAddress } = await client.createPaymentLink({
      recipientMetaAddress,
      amount: 1000000n, // 1 USDC
      tokenMint: USDC_MINT,
      message: 'Test payment'
    });

    console.log('Payment created:', paymentUrl);

    // 4. Recipient scans for payments
    const payments = await client.scanForPayments({
      viewPrivkey: recipientKeys.viewKeypair.secretKey,
      spendPubkey: recipientKeys.spendKeypair.publicKey
    });

    console.log(`Recipient found ${payments.length} payments`);

    // 5. Claim the payment
    if (payments.length > 0) {
      const payment = payments[0];
      const claimSignature = await client.claimPayment({
        escrowAddress: payment.escrowAddress,
        claimSecret: payment.claimSecret,
        recipientAddress: wallet.publicKey
      });

      console.log('Payment claimed successfully!');
      console.log('Claim signature:', claimSignature);
    }

  } catch (error) {
    console.error('Payment flow failed:', error);
  }
}

// Run the complete flow
completePaymentFlow();

Troubleshooting

Common Issues

"Insufficient funds" error

  • Make sure your wallet has enough SOL for transaction fees
  • Ensure you have the tokens you're trying to send

"Meta-address not found" error

  • Verify the recipient's meta-address is correctly formatted
  • Check that the recipient has registered their meta-address on-chain

"Claim secret invalid" error

  • Ensure you're using the correct claim secret
  • Check that the escrow hasn't expired

Scanning returns no results

  • Make sure you're using the correct view private key
  • Check that payments were actually sent to your meta-address
  • Verify you're scanning the correct network (devnet vs mainnet)

Getting Help

If you encounter issues:

  1. Check the Troubleshooting Guide
  2. Review transaction details on Solana Explorer
  3. Join our Discord Community
  4. Open an issue on GitHub

Next Steps

Congratulations! You've successfully sent and received your first private payment. Here's what to explore next:

Production Considerations

Before deploying to production:

  1. Use mainnet: Switch from devnet to mainnet-beta
  2. Secure key storage: Use hardware wallets or secure enclaves
  3. Error handling: Implement robust error handling and retry logic
  4. Rate limiting: Respect RPC provider rate limits
  5. Monitoring: Set up alerts for failed transactions
  6. Testing: Thoroughly test all payment flows
  7. Compliance: Ensure compliance with local regulations

You're now ready to build private payment applications with Priv Protocol!

On this page