Skip to main content

Welcome to identiPay

identiPay is a privacy-preserving payment protocol that combines zero-knowledge proofs, stealth addresses, and blockchain technology to enable anonymous commerce with verifiable identity. This guide will walk you through setting up your wallet and making your first transaction.
Prerequisites: You’ll need an Android device with NFC capability and a biometric passport (ePassport) to complete identity registration.

Installation

1

Download the Wallet App

Install the identiPay Wallet from the Google Play Store or build from source:
git clone https://github.com/identipay/identipay.git
cd identipay/android/wallet-app
./gradlew assembleDebug
The wallet app is built with Kotlin and Jetpack Compose, with native cryptography for stealth addresses and zero-knowledge proofs.
2

Create Your Identity

On first launch, you’ll be guided through the identity registration process:
  1. Scan your passport - Place your biometric passport on the device’s NFC reader
  2. Enter your PIN - This is combined with your passport data to derive a deterministic seed
  3. Choose your username - Register a unique @username.idpay meta-address
Your identity commitment is computed as Poseidon(issuerCertHash, docNumberHash, dobHash, userSalt) and registered on-chain via a zero-knowledge proof. The proof verifies you have valid credentials without revealing any personal information.
The wallet derives your keypairs from your passport + PIN:
// From IdentityRepository.kt
seedManager.deriveFromPassportAndPin(
    personalNumber = credentialData.rawPersonalNumber,
    dateOfBirth = credentialData.rawDateOfBirth,
    nationality = credentialData.rawNationality,
    issuerCertHash = issuerCertHash,
    pin = pin,
)
Two keypairs are generated:
  • Spend key - Controls your funds
  • View key - Scans for incoming payments
3

Get Testnet USDC

identiPay currently operates on Sui testnet. You’ll need testnet USDC to make payments:
  1. Copy your receive address from the wallet home screen
  2. Visit the Sui testnet faucet
  3. Request testnet SUI
  4. Swap for testnet USDC on a testnet DEX
Each time you receive funds, a fresh stealth address is derived deterministically using your view key and a counter. This ensures payments are unlinkable on-chain.

Your First Transaction

1

Find a Merchant

Try the demo checkout at https://checkout.idpay.dev or use the POS app:
cd identipay/android/identipayPOS
./gradlew assembleDebug
The merchant creates a payment proposal via the backend API:
// POST /api/identipay/v1/proposals
{
  "amount": "5.00",
  "currency": "USDC",
  "description": "Coffee",
  "constraints": {
    "minAge": 18  // Optional age gate
  }
}
You’ll receive a QR code containing the transaction ID.
2

Scan the QR Code

Open the identiPay wallet and tap Scan to Pay. The wallet:
  1. Fetches the proposal from the intent endpoint
  2. Verifies the merchant’s signature
  3. Shows you the transaction details
// From PaymentRepository.kt
val proposal = backendApi.getProposal(transactionId)

// Verify merchant signature
intent.verifyMerchantSignature(
    merchantPubkey = proposal.merchant.publicKey
)
3

Review and Confirm

The wallet displays:
  • Merchant name and DID
  • Amount in USDC
  • Age gate requirements (if any)
  • Encrypted receipt preview
If the transaction requires age verification, the wallet generates a zero-knowledge proof:
// Age check proof verifies: currentYear - birthYear >= minAge
val ageProof = proofGenerator.generateAgeProof(
    birthYear = userPreferences.getBirthYear(),
    birthMonth = userPreferences.getBirthMonth(),
    birthDay = userPreferences.getBirthDay(),
    minAge = proposal.constraints.minAge
)
The proof reveals ONLY that you meet the age requirement - your actual birthdate remains private.
4

Execute Settlement

When you tap Confirm, the wallet:
  1. Derives a fresh stealth address for receiving the receipt
  2. Signs the intent hash with your spend key
  3. Builds a Programmable Transaction Block (PTB)
  4. Submits to the Sui blockchain
// The settlement PTB atomically:
// 1. Verifies ZK proof (if required)
// 2. Verifies intent signature
// 3. Transfers USDC to merchant
// 4. Mints encrypted receipt to your stealth address

val txDigest = executeSendTransaction(
    senderAddress = sourceStealthAddress,
    senderPrivKey = stealthPrivKey,
    recipientStealthAddress = merchantStealthAddress,
    amountMicros = amountMicros,
    ephemeralPubkey = ephemeralPubkey,
    viewTag = viewTag,
)
The settlement contract ensures atomicity - either everything succeeds or everything fails:
// From settlement.move:77-167
entry fun execute_commerce<T>(
    state: &mut SettlementState,
    payment: &mut Coin<T>,
    amount: u64,
    // ... ZK proof parameters
    // ... encrypted receipt parameters
) {
    // 1. Verify ZK proof
    zk_verifier::assert_proof_valid(zk_vk, &zk_proof, &zk_public_inputs);
    
    // 2. Verify intent signature
    intent::verify_intent_signature(&intent_sig, &intent_hash, &buyer_pubkey);
    
    // 3. Transfer payment
    let exact = coin::split(payment, amount, ctx);
    transfer::public_transfer(exact, merchant);
    
    // 4. Mint receipt to stealth address
    let receipt = receipt::mint_receipt(/* ... */);
    transfer::public_transfer(receipt, buyer_stealth_addr);
    
    // 5. Emit settlement event
    event::emit(SettlementEvent { intent_hash, /* ... */ });
}
5

Receive Your Receipt

The encrypted receipt is delivered to your stealth address. The wallet:
  1. Scans for new stealth announcements
  2. Derives the stealth private key
  3. Decrypts the receipt using ECDH
  4. Stores it locally in your transaction history
// From AnnouncementRepository.kt
val announcements = backendApi.getAnnouncements(since = lastSync)

for (announcement in announcements) {
    // Check if this stealth address belongs to us
    val scanResult = stealthAddress.scan(
        viewPrivateKey = viewPriv,
        spendPrivateKey = spendPriv,
        ephemeralPubkey = announcement.ephemeralPubkey,
        announcedViewTag = announcement.viewTag,
        announcedStealthAddress = announcement.stealthAddress
    )
    
    if (scanResult != null) {
        // This is ours! Decrypt the receipt
        val receipt = decryptReceipt(scanResult.stealthPrivateKey)
        storeTransaction(receipt)
    }
}

Advanced: Shielded Pool for Privacy

If you need to merge coins from multiple stealth addresses, the wallet automatically uses the shielded pool:
1

Automatic Pool-on-Merge

When your largest stealth address has insufficient balance:
// From PaymentRepository.kt:67-108
if (source == null && totalBalance >= amountMicros) {
    // 1. Deposit all sources into the shielded pool
    for (src in sources) {
        poolRepository.deposit(src.balanceUsdc, src.stealthAddress, privKey)
    }
    
    // 2. Derive a fresh stealth address
    val mergeAddr = deriveReceiveAddress(counter)
    
    // 3. Withdraw to fresh address via ZK proof
    for (note in unspentNotes) {
        poolRepository.withdraw(
            noteId = note.id,
            recipientAddress = mergeAddr.stealthAddress,
            amount = note.amount
        )
    }
}
This breaks the on-chain link between your old addresses and the new merged output.

Backend API Reference

The identiPay backend exposes these key endpoints:

Announcements

GET /api/identipay/v1/announcements - Query stealth address announcements with optional view tag filtering

Intents

GET /api/identipay/v1/intents/{txId} - Resolve a payment proposal by transaction ID

Names

GET /api/identipay/v1/names/{name} - Resolve a meta-address to public keys

Merchants

POST /api/identipay/v1/merchants/register - Register a new merchant DID

WebSocket Status Updates

Subscribe to real-time settlement status:
const ws = new WebSocket('wss://api.idpay.dev/ws/transactions/{txId}');

ws.onmessage = (event) => {
  const { status, txDigest } = JSON.parse(event.data);
  // status: "pending" | "settled" | "failed"
};
The backend polls for SettlementEvent emissions and pushes updates to connected clients.

Next Steps

Architecture Overview

Understand the full system design - contracts, circuits, and mobile apps

Smart Contracts

Dive into the Sui Move contracts powering settlements

ZK Circuits

Learn how zero-knowledge proofs preserve privacy

Backend API

Full API reference for integrating with identiPay
Testnet Only: identiPay is currently deployed on Sui testnet. Do not use real funds or real identity documents. All transactions use testnet tokens with no real-world value.

Build docs developers (and LLMs) love