NixProtocol Documentation
Add UTXO-based private payments to your application. NixProtocol provides a privacy pool protocol for EVM chains using Noir ZK circuits and Grumpkin curve cryptography. Testnet live on Avalanche Fuji & Base Sepolia.
Architecture Overview
NixProtocol provides privacy through a layered architecture combining encryption, zero-knowledge proofs, and smart contracts.
Data Flow
- › Derive Grumpkin keypair via Poseidon2
- › Encrypt notes with ECIES
- › Generate UltraHonk proofs locally
- › Authorize via ZK proofs (no signatures)
- › Verify ZK proofs
- › Store note commitments in Merkle tree
- › Verify nullifiers & update roots
- › Emit privacy-preserving events
- › Selective disclosure keys
- › View-only access grants
- › Compliance reporting
- › Audit trail generation
Key Components
Embedded elliptic curve for ZK-friendly operations. Used for keypairs, ECIES encryption, and NixAddress encoding. Native to Noir circuits via Barretenberg.
Compiled from Noir circuits via Barretenberg. No trusted setup required. 3 circuits: deposit, registration, and transact.
ZK-optimized hash function used for note commitments, nullifiers, and Merkle trees. Poseidon2 variant for improved performance in Noir circuits.
Quickstart
Get up and running in 5 minutes. Generate keys, encrypt data, and create proofs.
Install the SDK
npm install @nixprotocol/nix-wallet viemGenerate Keypair
import { deriveNixKeyFromSignature } from '@nixprotocol/nix-wallet';
// Deterministic from wallet signature (recommended)
// Signs "NixProtocol key derivation v3", then Poseidon2-hashes the signature
const keyPair = await deriveNixKeyFromSignature(walletClient);
console.log('Public Key:', keyPair.publicKey); // { x: bigint, y: bigint }
console.log('Commitment:', keyPair.commitment); // Poseidon2(pk.x, pk.y)
console.log('Private Key:', keyPair.privateKey); // Grumpkin scalarEncrypt & Create Notes
import { ecies, poseidon2Hash } from '@nixprotocol/nix-wallet';
// Encrypt a note value to recipient's Grumpkin public key
const randomness = BigInt('0x' + crypto.randomUUID().replace(/-/g, ''));
const ciphertext = ecies.encrypt(1000n, randomness, recipientPublicKey);
// Decrypt (only recipient can do this)
const decrypted = ecies.decrypt(privateKey, ciphertext.authKeyX, ciphertext.authKeyY, ciphertext.encrypted);
// Create note commitment
const commitment = poseidon2Hash([value, ownerPkX, ownerPkY, salt]);Installation
NixProtocol is available as a set of NPM packages for different integration needs.
@nixprotocol/nix-walletBUSL-1.1Core wallet library - key derivation, encryption, proofs, NixAddress, and UTXO management
@nixprotocol/contractsBUSL-1.1NixPool Solidity contracts and TypeScript bindings
@nixprotocol/relayerBUSL-1.1Gas abstraction with ZK proof authorization
Encryption
NixProtocol uses ECIES encryption on the Grumpkin curve for encrypting UTXO note data and auditor-accessible transaction records.
Key Properties
- •Asymmetric Encryption: Encrypt data to any Grumpkin public key
- •Probabilistic: Same plaintext encrypts differently each time
- •ZK-Friendly: Grumpkin curve is native to Noir circuits via Barretenberg
Mathematical Foundation
Embedded elliptic curve native to Barretenberg/Noir:
Short Weierstrass curve: y² = x³ - 17 (embedded curve of BN254)For message m, recipient public key P = sG:
Note commitments use Poseidon2 hash: commitment = Poseidon2(value, ownerPubKey, salt). Nullifiers prevent double-spending: nullifier = Poseidon2(commitment, privateKey).
Security Parameters
~128-bit security level (Grumpkin curve, embedded in BN254)
Fresh random ephemeral key per encryption
Commitment (1 field element) + ECIES-encrypted data
20 levels supporting ~1M notes
Zero-Knowledge Proofs
NixProtocol uses UltraHonk proofs compiled from Noir circuits via Barretenberg. No trusted setup required.
Registration Circuit
Proves knowledge of Grumpkin private key corresponding to registered public key
Deposit Circuit
Creates a new UTXO note commitment from an ERC-20 deposit with ECIES-encrypted data
Transact Circuit
Consumes 2 input notes, creates 2 output notes. Proves Merkle membership, correct nullifiers, and value conservation
NixAddress & UTXO Notes
NixAddress aliases your real EVM address with a compact Grumpkin public key. Share it to receive UTXO notes in the privacy pool without ever exposing your on-chain identity.
import { deriveNixKeyFromSignature, nixAddressFromKeyPair,
encodeNixAddress, decodeNixAddress } from '@nixprotocol/nix-wallet';
// Derive keypair from wallet signature
const keyPair = await deriveNixKeyFromSignature(walletClient);
const nixAddress = nixAddressFromKeyPair(keyPair);
console.log('NixAddress:', nixAddress);
// nix:<commitment>:<pk.x>:<pk.y>
// Decode to get commitment + public key
const { commitment, publicKey } = decodeNixAddress(nixAddress);Compliance & Auditor Access
Privacy with accountability. Each NixPool has a designated auditor who holds a single Grumpkin private key, set at deployment time. This key can decrypt ECIES-encrypted note data for compliance reporting.
How Auditor Access Works
A single Grumpkin private key is designated as the auditor key when the NixPool is deployed. The corresponding public key is stored on-chain.
Transaction data is ECIES-encrypted to the auditor's Grumpkin public key during each deposit and transfer.
The auditor uses their Grumpkin private key to decrypt note data, revealing amounts and ownership for compliance reporting.
Implementation
import { NixAuditorClient } from '@nixprotocol/nix-wallet';
// Initialize auditor client with the designated Grumpkin private key
const auditorClient = new NixAuditorClient({
auditorPrivateKey: process.env.AUDITOR_GRUMPKIN_KEY,
poolAddress: '0x94e5f37c557dA9B2C0F0636BbCDB3743B30Ba6B1', // NixPool on Fuji
});
// Decrypt ECIES-encrypted note data from on-chain events
const decryptedNotes = await auditorClient.decryptPoolNotes({
fromBlock: 1000000,
toBlock: 'latest',
});
// Generate compliance report from decrypted data
const report = await auditorClient.generateComplianceReport({
notes: decryptedNotes,
fromDate: '2025-01-01',
toDate: '2025-12-31',
});What the Auditor Can See
Decrypt UTXO note values from ECIES-encrypted on-chain data
Identify note owners via decrypted Grumpkin public keys
Trace deposit, transfer, and withdrawal activity across the pool
Regulatory Compatible: Designed for institutions requiring audit trails, tax reporting, and regulatory compliance while maintaining user privacy from public view.
SDK Overview
The @nixprotocol/nix-wallet package is the main entry point for most integrations.
Exports
deriveNixKeyFromSignature(walletClient) - Deterministic keypair from wallet signature via Poseidon2generateNixKeyPair() - Random Grumpkin keypairecies.encrypt(data, pubKey) - ECIES encrypt to Grumpkin keyecies.decrypt(ciphertext, privKey) - Decrypt ECIES ciphertextposeidon2.hash(inputs) - Poseidon2 hash functioncreateNoteCommitment(value, pubKey, salt) - Create UTXO note commitmentKey Generation
Generate Grumpkin keypairs for encryption and proof generation. Keys are derived deterministically from EVM wallet signatures using Poseidon2 hashing, ensuring the same wallet always produces the same NixProtocol private key.
Important: Use deterministic key derivation from wallet signatures in production. Random keypairs require secure private key storage.
ECIES Encryption
ECIES encryption on Grumpkin curve for encrypting note data and auditor records.
import { ecies } from '@nixprotocol/nix-wallet';
// ECIES ciphertext structure
interface ECIESCiphertext {
authKeyX: bigint; // Ephemeral Grumpkin point x
authKeyY: bigint; // Ephemeral Grumpkin point y
encrypted: bigint; // Encrypted value
}
// Encrypt note data to recipient's Grumpkin public key
const randomness = BigInt('0x' + crypto.randomUUID().replace(/-/g, ''));
const encrypted = ecies.encrypt(value, randomness, recipientPublicKey);
// Decrypt (only recipient can do this)
const decrypted = ecies.decrypt(privateKey, encrypted.authKeyX, encrypted.authKeyY, encrypted.encrypted);
// Auditor can also decrypt with auditor key
const auditValue = ecies.decrypt(auditorPrivateKey, auditCt.authKeyX, auditCt.authKeyY, auditCt.encrypted);Nix ID
Nix ID is a shareable payment identifier that encodes your Grumpkin public key commitment and coordinates for receiving UTXO notes in the privacy pool.
NixAddress format:
nix:<commitment>:<pk.x>:<pk.y>Commitment is Poseidon2(pk.x, pk.y), ensuring address integrity is self-verifiable.
Smart Contracts Overview
NixProtocol smart contracts handle on-chain privacy operations including UTXO note management and ZK proof verification.
NixPool
Main privacy pool contract: deposits, transfers, withdrawals with UTXO notes
Merkle Trees
Poseidon2-based Merkle trees (depth 20) for note commitment storage
UltraHonk Verifier
On-chain proof verification for all 3 circuits
NixPool Contract
Main privacy pool contract for UTXO-based private payments.
// Register Grumpkin public key with ZK proof
function register(
bytes calldata proof,
bytes32 publicKeyHash
) external;
// Deposit ERC-20 tokens, creating a UTXO note
function deposit(
address token,
uint256 amount,
bytes calldata proof,
bytes32 noteCommitment
) external;
// Transact: consume 2 input notes, create 2 output notes
function transact(
bytes calldata proof,
bytes32[2] calldata nullifiers,
bytes32[2] calldata newCommitments,
bytes calldata encryptedNotes
) external;NixPool
Merkle tree storage for UTXO note commitments.
// Merkle tree stores note commitments
function getMerkleRoot() external view returns (bytes32);
// Check root history (last 100 roots valid)
function isKnownRoot(bytes32 root) external view returns (bool);
// Check if nullifier has been used
function isSpent(bytes32 nullifier) external view returns (bool);
// Get current note index
function nextIndex() external view returns (uint256);Deployed Addresses
Contract deployment coming soon.
Addresses TBD
Addresses TBD
Reference implementation. Security audit in progress. Do not deploy with significant value until audit complete.
Private ERC-20 Guide
Add UTXO-based private payments to any ERC-20 token in 5 steps.
- 1Deploy NixPool contract for your ERC-20 token
- 2Users register with Grumpkin public key
- 3Deposit converts public tokens to UTXO notes
- 4Transact consumes input notes, creates output notes
- 5Withdraw burns notes and releases public tokens
Complete Integration Example
import { createWalletClient, custom } from 'viem';
import {
deriveNixKeyFromSignature,
generateRegistrationProof,
generateDepositProof,
generateTransactProof,
ecies,
poseidon2,
createNoteCommitment
} from '@nixprotocol/nix-wallet';
import { NixPool__factory } from '@nixprotocol/contracts';
// Setup
const walletClient = createWalletClient({
transport: custom(window.ethereum),
account: userAddress,
});
// Step 1: Derive keypair from wallet signature (deterministic via Poseidon2)
// Signs "NixProtocol key derivation v3", splits sig into 3 chunks,
// then Poseidon2-hashes them to produce a valid Grumpkin scalar
const keyPair = await deriveNixKeyFromSignature(walletClient);
// Step 2: Register (one-time per address)
const isRegistered = await nixPool.isRegistered(userAddress);
if (!isRegistered) {
const regProof = await generateRegistrationProof(keyPair);
const regTx = await nixPool.register(regProof, keyPair.commitment);
await regTx.wait();
}
// Step 3: Deposit - convert public tokens to UTXO note
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const amount = 100_000000n; // 100 USDC (6 decimals)
// First approve
await usdc.approve(NIX_POOL_ADDRESS, amount);
// Create note commitment and deposit proof
const salt = crypto.getRandomValues(new Uint8Array(32));
const noteCommitment = createNoteCommitment(amount, keyPair.publicKey, salt);
const depositProof = await generateDepositProof({ keyPair, amount, salt });
const depositTx = await nixPool.deposit(USDC_ADDRESS, amount, depositProof, noteCommitment);
await depositTx.wait();
// Step 4: Transact - consume input notes, create output notes
const transactProof = await generateTransactProof({
inputNotes: [myNote1, myNote2],
outputNotes: [recipientNote, changeNote],
merkleRoot: await nixPool.getMerkleRoot(),
senderKeyPair: keyPair,
});
const encryptedNotes = ecies.encrypt(outputNoteData, recipientPublicKey);
const transactTx = await nixPool.transact(
transactProof.proof,
transactProof.nullifiers,
transactProof.newCommitments,
encryptedNotes
);
await transactTx.wait();
console.log('Private transaction complete!');Relayer Transactions
Customize how gas is handled for your users. Users generate ZK proofs, the relayer submits transactions on-chain with configurable gas fees. No signatures needed for authorization. Supports ERC-2612 Permit for gasless token approvals.
How It Works
Basic Usage
import { NixRelayer } from '@nixprotocol/relayer';
const relayer = new NixRelayer({
url: 'https://relayer.nixprotocol.com',
chainId: 43113, // Avalanche Fuji testnet
});
// User signs, relayer pays gas
const result = await relayer.sendPrivateTransfer({
signer: userSigner, // User's wallet (MetaMask, etc.)
token: USDC_ADDRESS,
recipient: recipientAddress,
proof: transferProof,
feeToken: USDC_ADDRESS, // Pay fee in USDC
maxFee: ethers.parseUnits('1', 6), // Max 1 USDC fee
});
console.log('Tx hash:', result.txHash);Self-Hosted Relayer
// server.ts - Your own relayer backend
import { NixRelayerServer } from '@nixprotocol/relayer/server';
const relayer = new NixRelayerServer({
privateKey: process.env.RELAYER_PRIVATE_KEY,
rpcUrl: process.env.RPC_URL,
supportedTokens: [USDC, USDT, DAI],
feePercentage: 0.1, // 0.1% fee
});
// Validate and execute meta-transactions
app.post('/relay', async (req, res) => {
const { signedRequest } = req.body;
// Verify signature and fee payment
const isValid = await relayer.verifyRequest(signedRequest);
if (!isValid) return res.status(400).json({ error: 'Invalid request' });
// Execute on-chain
const result = await relayer.execute(signedRequest);
res.json({ txHash: result.hash });
});Fee Structure
| Operation | Gas Cost | ~Fee (L2) |
|---|---|---|
| Private Transfer | ~3M gas | ~$0.15-1.50 |
| Withdraw | ~3M gas | ~$0.15-1.50 |
| Register | ~2.8M gas | ~$0.14-1.40 |
* Estimated fees on L2 chains (Avalanche, Base, Arbitrum). Currently deployed on Avalanche Fuji and Base Sepolia testnets.
Integration Paths
Choose the right approach based on your privacy requirements.
Add Privacy to ERC-20
Deposit any ERC-20 into the UTXO privacy pool. Amounts hidden with unlinkable transfers.
Relayer UX
Users generate proofs, relayer submits with customizable gas fees. Better UX with flexible gas management.
Full Privacy Payments
Complete anonymity with unlinkable withdrawals using NixPool dark pool pattern.
Error Handling
Common errors and how to handle them in your integration.
| Error Code | Cause | Solution |
|---|---|---|
INVALID_PROOF | ZK proof verification failed | Regenerate proof with correct inputs |
INSUFFICIENT_BALANCE | UTXO note value less than transfer amount | Check decrypted balance before transfer |
NOT_REGISTERED | User hasn't registered their Grumpkin public key | Call register() with valid proof first |
NULLIFIER_USED | Attempting to spend already-spent note | Use fresh commitment for new transaction |
INVALID_MERKLE_ROOT | Merkle root doesn't match on-chain state | Fetch latest root before proof generation |
DECRYPTION_FAILED | Wrong private key or corrupted ciphertext | Verify keypair matches registered public key |
Handling Errors
import { NixError, NixErrorCode } from '@nixprotocol/nix-wallet';
try {
const tx = await nixPool.privateTransfer(recipient, proof);
await tx.wait();
} catch (error) {
if (error instanceof NixError) {
switch (error.code) {
case NixErrorCode.INVALID_PROOF:
// Regenerate proof with fresh randomness
const newProof = await generateTransferProof(/*...*/);
break;
case NixErrorCode.INSUFFICIENT_BALANCE:
// Show user their actual balance
const balance = await getDecryptedBalance();
throw new Error(`Insufficient balance: ${balance}`);
default:
console.error('Nix error:', error.message);
}
}
throw error;
}Debugging Tips
Set NIX_DEBUG=true to log proof inputs and circuit constraints.
Most errors stem from keypair mismatch. Always verify public key matches on-chain registration.
UltraHonk proof verification is gas-heavy (~3M gas). Ensure transactions have sufficient gas limit.
Security Best Practices
Critical security considerations for production deployments.
Key Management
Use deterministic key derivation from wallet signatures. Keys should be derived on-demand, not persisted.
Use deriveNixKeyFromSignature(walletClient) to deterministically derive a Grumpkin keypair from a wallet signature via Poseidon2 hashing.
Proof Generation
Every proof must use cryptographically random values. Reusing randomness leaks private information.
Generate proofs in browser/client. Never send private keys or amounts to backend servers.
Always verify proofs locally before submitting to chain to save gas on invalid proofs.
Add random delays between operations to prevent transaction graph analysis.
Smart Contract Security
- •Verify Circuit Hash: Ensure deployed UltraHonk verifier matches compiled Noir circuit
- •Check Nullifier Storage: Nullifiers must be stored permanently to prevent double-spend
- •Merkle Tree Depth: Use sufficient depth (20+) to support expected user base
- •Reentrancy Guards: All state-changing functions must be protected
Reference implementation is undergoing security audit. Do not deploy with significant value until audit is complete. Contact us for enterprise deployment security review.
Full API Reference
Complete function signatures and type definitions.
@nixprotocol/nix-wallet
// Key Generation
function deriveNixKeyFromSignature(walletClient: WalletClient): Promise<NixKeyPair>;
function generateNixKeyPair(): NixKeyPair;
function importNixKey(privateKeyHex: string): NixKeyPair;
function exportNixKey(keyPair: NixKeyPair): string;
interface Point {
x: bigint;
y: bigint;
}
interface NixKeyPair {
privateKey: bigint; // Grumpkin scalar
publicKey: Point; // Grumpkin curve point
commitment: bigint; // Poseidon2(pk.x, pk.y)
}
// ECIES Encryption
namespace ecies {
function encrypt(value: bigint, randomness: bigint, receiverPk: Point): ECIESCiphertext;
function decrypt(sk: bigint, authKeyX: bigint, authKeyY: bigint, encrypted: bigint): bigint;
}
interface ECIESCiphertext {
authKeyX: bigint;
authKeyY: bigint;
encrypted: bigint;
}
// Poseidon2 Hash
function poseidon2Hash(inputs: bigint[]): bigint;
function computeCommitment(pkx: bigint, pky: bigint): bigint;
// Note Commitments
function createNoteCommitment(value: bigint, pubKey: Point, salt: bigint): bigint;
// Proof Generation
function generateRegistrationProof(keyPair: NixKeyPair): Promise<ProofResult>;
function generateDepositProof(params: DepositParams): Promise<ProofResult>;
function generateTransactProof(params: TransactParams): Promise<ProofResult>;
interface ProofResult {
proof: `0x${string}`; // UltraHonk proof bytes
publicInputs: `0x${string}`[];
}NixAddress & NixWallet
// NixAddress encoding/decoding
function encodeNixAddress(data: NixAddressData): string;
function decodeNixAddress(nixAddress: string): NixAddressData;
function nixAddressFromKeyPair(keyPair: NixKeyPair): string;
interface NixAddressData {
commitment: bigint; // Poseidon2(pk.x, pk.y)
publicKey: Point; // Grumpkin curve point
}
// Format: "nix:<commitment>:<pk.x>:<pk.y>"
// NixWallet class - high-level wallet interface
class NixWallet {
static create(config: NixWalletConfig): Promise<NixWallet>;
register(): Promise<void>;
deposit(token: Address, amount: bigint): Promise<void>;
transfer(recipientNixAddress: string, amount: bigint): Promise<void>;
withdraw(token: Address, amount: bigint, to: Address): Promise<void>;
syncBalances(): Promise<void>;
exportPrivateKey(): string;
}Contract ABIs
interface INixPool {
function register(bytes calldata proof, bytes32 publicKeyHash) external;
function deposit(
address token,
uint256 amount,
bytes calldata proof,
bytes32 noteCommitment
) external;
function transact(
bytes calldata proof,
bytes32[2] calldata nullifiers,
bytes32[2] calldata newCommitments,
bytes calldata encryptedNotes
) external;
function withdraw(
bytes calldata proof,
bytes32 nullifier,
address token,
uint256 amount,
address recipient
) external;
function getMerkleRoot() external view returns (bytes32);
function isKnownRoot(bytes32 root) external view returns (bool);
function isSpent(bytes32 nullifier) external view returns (bool);
function isRegistered(address user) external view returns (bool);
function nextIndex() external view returns (uint256);
}poseidon2-go
A pure Go implementation of the Poseidon2 hash function over the BN254 scalar field. Designed for off-chain computation that must match on-chain or in-circuit Poseidon2 outputs.
Installation
go get github.com/nixprotocol/poseidon2-goParameters
| Parameter | Value |
|---|---|
| Field | BN254 scalar field (Fr) |
| State width | 4 |
| Rate | 3 |
| Capacity | 1 |
| Full rounds | 8 |
| Partial rounds | 56 |
| S-box | x⁵ |
Exported Functions
func Hash(inputs []fr.Element) fr.Element
func Hash2(a, b fr.Element) fr.Element
func HashToBytes(inputs []fr.Element) [32]byte
func Permute(s *[4]fr.Element)HashVariable-length hash. Absorbs inputs in rate-3 chunks via the sponge, returns one field element.
Hash2Two-input hash. Convenience wrapper equivalent to Hash([]fr.Element{a, b}}).
HashToBytesSame as Hash but returns the result as a 32-byte big-endian array.
PermuteIn-place Poseidon2 permutation on a 4-element state. Used internally by Hash; exposed for advanced sponge constructions.
Sponge Construction
Hash and HashToBytes use a sponge that absorbs inputs into state slots [1..3] (rate = 3), leaving slot 0 as capacity. After each rate-sized chunk the full permutation runs. The output is state[0] after final absorption and one more permutation.
Usage
package main
import (
"fmt"
poseidon2 "github.com/nixprotocol/poseidon2-go"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
)
func main() {
var a, b fr.Element
a.SetUint64(1)
b.SetUint64(2)
h := poseidon2.Hash2(a, b)
fmt.Println("hash:", h.String())
raw := poseidon2.HashToBytes([]fr.Element{a, b})
fmt.Printf("bytes: %x\n", raw)
}The round constants and MDS matrix match the Noir and Solidity implementations used in NixProtocol, so hashes computed with this library will produce identical outputs for the same inputs.
MCP Server
Model Context Protocol server that wraps the NixProtocol SDK for AI agent integration. Plug it into Claude, or any MCP-compatible agent, to enable private payments through natural language.
Setup
npm install @nixprotocol/mcp-server
# Add to your MCP config (e.g. claude_desktop_config.json)
{
"mcpServers": {
"nixprotocol": {
"command": "npx",
"args": ["@nixprotocol/mcp-server"],
"env": {
"NIX_RPC_URL": "https://api.avax-test.network/ext/bc/C/rpc",
"NIX_CONTRACT_ADDRESS": "0x94e5...",
"NIX_CHAIN_ID": "43113",
"NIX_CIRCUIT_PATH": "./circuits",
"NIX_RELAYER_URL": "https://relayer.nixprotocol.com"
}
}
}
}Available Tools
nix_init_wallet - Initialize prover, clients, and optionally import a private keynix_get_address - Return the NixAddress for the current keypairnix_get_balance - Scan notes and return token balancenix_get_history - List all notes with spent/unspent statusnix_register - One-time identity registration with the poolnix_deposit - Deposit ERC-20 tokens into the privacy poolnix_transfer - Private transfer to a NixAddress recipientnix_withdraw - Withdraw from the pool to an EVM addressnix_get_fee - Query the relayer for current fee infonix_decode_address - Parse and validate a NixAddressEnvironment Variables
| Variable | Description |
|---|---|
| NIX_RPC_URL | RPC endpoint for the target chain |
| NIX_CONTRACT_ADDRESS | Deployed NixPool address |
| NIX_CHAIN_ID | Target chain ID |
| NIX_EVM_PRIVATE_KEY | EVM key for gas (deposits only) |
| NIX_PRIVATE_KEY | Nix private key (optional, can derive from wallet) |
| NIX_CIRCUIT_PATH | Path to compiled circuit artifacts |
| NIX_RELAYER_URL | Relayer endpoint for gasless transactions |
Relayer API Reference
The relayer submits transactions on behalf of users so they never need to hold native gas tokens. It calculates fees dynamically using Chainlink price feeds and deducts them from the privacy pool itself.
Endpoints
GET /:chainId/relay/fee-info?token=<address>Returns the current fee quote for a given token, including gas cost estimate, relayer margin, and the relayer's public key for encrypting notes.
// Response
{
"feeAmount": "500000",
"feeToken": "0x5425...",
"gasEstimate": "350000",
"relayerPublicKey": { "x": "0x...", "y": "0x..." }
}POST /:chainId/relay/transactSubmit a transact proof (transfer or withdrawal). The relayer verifies the proof, checks the fee output covers gas costs, and submits on-chain.
// Request body
{
"proof": "0x...",
"publicInputs": ["0x...", ...],
"encryptedNotes": "0x..."
}POST /:chainId/relay/registerSubmit a registration proof. The relayer submits the identity commitment to the registration Merkle tree.
Fee Calculation
Fees are calculated as: (estimated gas * gas price * token price) + margin. Default margin is 30% with a $0.50 minimum. Token prices are fetched from Chainlink oracles.
Rate Limiting
The relayer enforces rate limiting per IP address. Default: 20 requests per minute.
Self-Hosting
# Clone and run your own relayer
npm install @nixprotocol/relayer
# Configure environment
export PRIVATE_KEY="0x..."
export RPC_URL="https://api.avax-test.network/ext/bc/C/rpc"
export PORT=3001
# Start
npx nix-relayerAuditor Tools
The auditor holds a single Grumpkin private key set at NixPool deployment. With this key, the auditor can decrypt all transaction data for compliance reporting without affecting user privacy from public view.
What the Auditor Can Decrypt
SDK Functions
decryptAuditorNote(ciphertext, auditorPrivKey) - Decrypt a note's amount and blinding factordecryptRecipientCommitment(ciphertext, auditorPrivKey) - Decrypt who received a noteparseDepositCalldata(txData) - Extract deposit proof public inputs from calldataparseTransactCalldata(txData) - Extract transact proof public inputs from calldataimport { decryptAuditorNote, decryptRecipientCommitment } from '@nixprotocol/sdk';
// Auditor decrypts a deposit note
const note = decryptAuditorNote(
encryptedCiphertext,
auditorPrivateKey
);
console.log('Amount:', note.amount);
console.log('Blinding:', note.blinding);
// Auditor decrypts recipient identity
const recipient = decryptRecipientCommitment(
recipientCiphertext,
auditorPrivateKey
);NixPay Auditor Page
NixPay includes a built-in auditor interface at /auditor where the auditor can paste their private key to decrypt and inspect all pool transactions.
Pool Client
PoolClient wraps the NixPool smart contract for deposits, transfers, withdrawals, and on-chain queries. It handles ABI encoding, Merkle root lookups, and event fetching.
Key Methods
deposit(proof, noteHash, ...) - Submit a deposit transactiontransact(proof, nullifiers, ...) - Submit a transfer or withdrawalregister(proof, commitment) - Register identity commitmentgetNotes(fromBlock?) - Fetch NoteCreated events from the poolgetMerkleRoot() - Get current Merkle rootisSpent(nullifier) - Check if a nullifier has been spentUsage
import { PoolClient } from '@nixprotocol/sdk';
import { createPublicClient, http } from 'viem';
import { avalancheFuji } from 'viem/chains';
const publicClient = createPublicClient({
chain: avalancheFuji,
transport: http(),
});
const pool = new PoolClient({
contractAddress: '0x94e5...',
publicClient,
});
// Fetch all notes from the pool
const notes = await pool.getNotes();
// Check if a nullifier has been spent
const spent = await pool.isSpent(nullifierHash);Note Manager
NoteManager handles off-chain tracking of UTXO notes. It scans NoteCreated events, trial-decrypts each note with the user's private key, and maintains a local cache of owned notes in IndexedDB (browser) or in-memory (Node.js).
Key Methods
scan() - Scan for new notes and update local cachegetUnspentNotes() - Return all unspent notes owned by this keygetBalance() - Sum of all unspent note amountsmarkSpent(nullifier) - Mark a note as spent after a successful transactionCaching
NoteManager caches events in IndexedDB (key: nixpay-v2-events) to avoid re-scanning the full chain on every load. In Node.js environments, it falls back to in-memory storage.
import { NoteManager } from '@nixprotocol/sdk';
const noteManager = new NoteManager({
poolClient,
privateKey: nixPrivateKey,
});
// Scan for new notes (uses IndexedDB cache)
await noteManager.scan();
// Get spendable balance
const balance = noteManager.getBalance();
console.log('Balance:', balance.toString());
// Get unspent notes for building transactions
const notes = noteManager.getUnspentNotes();Need Help Integrating?
We offer custom development for complex privacy requirements.