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:
- Completed the Installation Guide
- A Solana wallet with some SOL for transaction fees
- Basic understanding of Core Concepts
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:
- A stealth address is derived for the recipient
- An escrow is created holding your tokens
- An announcement is posted on-chain
- A payment link is generated containing the claim information
Alternative: Create Payment Without Link
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:
- Check the Troubleshooting Guide
- Review transaction details on Solana Explorer
- Join our Discord Community
- Open an issue on GitHub
Next Steps
Congratulations! You've successfully sent and received your first private payment. Here's what to explore next:
- Advanced Features - Multi-signature payments, conditional escrows, and batch operations
- API Reference - Complete documentation for all SDK functions
- Integration Guides - Add Priv Protocol to existing applications
- Security Best Practices - Keep your users' funds and privacy safe
Production Considerations
Before deploying to production:
- Use mainnet: Switch from devnet to mainnet-beta
- Secure key storage: Use hardware wallets or secure enclaves
- Error handling: Implement robust error handling and retry logic
- Rate limiting: Respect RPC provider rate limits
- Monitoring: Set up alerts for failed transactions
- Testing: Thoroughly test all payment flows
- Compliance: Ensure compliance with local regulations
You're now ready to build private payment applications with Priv Protocol!