privily-sdk
v1.1.0
Published
Privily's SDK (node/web compat)
Readme
Privily SDK
A TypeScript SDK for building on Privily — a multi-chain, privacy-first payment layer. Supports EVM, Solana, Starknet, Tron, and Bitcoin ecosystems. Post-quantum account support is planned (see the "Falcon512 — Coming Soon" section).
Table of Contents
- Features
- Installation
- Network Endpoints
- Quick Start
- Module 1 — Bridge / Anonymity / Swap
- Module 2 — Payment Provider (Beta)
- Recipes & Examples
- Full Setup: EVM wallet → Privily keys → deploy
- Restore existing keys from storage
- Bridge: EVM → Privily
- Bridge: Privily → EVM (withdrawal)
- Bridge: Delayed withdrawal
- Bridge: Disperse (one-to-many)
- Swap (eligible non-Privily chains)
- Anonymous transfer
- Paginate operation history
- Manage withdrawal addresses
- Referral program
- Create a payment request
- Pay a received payment request
- Pay a public (open) payment request without auth
- Contributing
- License
Features
Module 1 — Bridge / Anonymity / Swap ✅ Production
- Current Account Standard — STRK-based v0 account flow is supported today across all integrations.
- Multi-chain — EVM (Ethereum, Polygon, BSC…), Solana, Starknet, Tron, and Bitcoin (Legacy + Taproot).
- Cross-chain Bridge — Deposit, withdraw, and transfer assets across networks.
- Privacy by Design — Bridge/transfer/payment flows are private by default on Privily.
- Anonymity Flags — Optional
isAnonflags further restrict counterparty metadata visibility in specific histories. - Asset Swap — Supported on eligible non-Privily chains (depends on liquidity provider availability).
- Disperse — One-to-many transfers in a single transaction.
- Delayed Withdrawals — Schedule withdrawals for future execution.
- Payment Requests — Request funds from users (directed or open), and pay requests on-chain cross-chain.
- History & Tracking — Full operation, deposit, transfer, and withdrawal history with pagination.
- TypeScript Native — Full type safety, IntelliSense, and TSDoc throughout.
- Dual Module — Ships as both ESM and CommonJS.
Module 2 — Payment Provider (Beta)
⚠️ Not currently available. Payment-provider contracts are not deployed yet (including revenue sharing). APIs may exist in the SDK, but these features should be treated as upcoming and not usable in production or testing environments yet.
- On-chain Product Commerce — Coming soon.
- Subscription Management — Coming soon.
- Revenue Sharing — Coming soon.
Installation
npm install privily-sdk⚠️ Self-custody notice: You are fully responsible for storing and backing up your keys/seed material. Privily never has access to your private keys at any time and cannot recover lost credentials or funds.
Network Endpoints
- RPC Wrapper URL (
RPCWrapper/RPCWrapperV0):https://rpc.privily.fi- Use for keygen message, deployment, auth, and Privily RPC wrapper operations.
- Units RPC URL (
UnitsProvider/UnitsAccount):https://privily.karnot.xyz- Use for all Units/on-chain execution (provider, account calls, and transactions).
Quick Start
1. Generate v0 STRK Keys
Use an unauthenticated wrapper to fetch the public keygen message, sign it with the user wallet, then pass that signature as additional entropy during v0 key derivation:
import { RPCWrapper, getAccountV0SkFromEntropy, getStarkPub } from 'privily-sdk';
// 1) Public route: wrapper without credentials
const rpc = new RPCWrapper();
const msgToSign = await rpc.getKeygenMessage();
// 2) Sign with wallet SDK (ethers / solana wallet adapter / starknet.js / etc.)
const walletSignature = await wallet.signMessage(msgToSign);
// 3) Derive v0 stark key material using the wallet signature as extra entropy
const starkPrivateKey = getAccountV0SkFromEntropy(msgToSign, walletSignature);
const starkPublicKey = getStarkPub(starkPrivateKey);See Account Key Generation for chain-specific accountV0Keys* helpers.
2. Deploy a Privily Account
import { RPCWrapper, PRIVILY_PLATFORM_ID } from 'privily-sdk';
const rpc = new RPCWrapper();
const result = await rpc.deployEvmUserAccount({
signer: evmProvider, // EVM wallet (must implement sign())
pk: starkPublicKey, // v0 Stark public key (hex string)
platform: PRIVILY_PLATFORM_ID, // Optional, defaults to Privily
version: 0, // v0 flow
});
console.log('Deployed:', result);See Account Deployment for all chains.
3. Authenticate & Query Balances
All authenticated v0 RPC operations go through RPCWrapperV0, which manages JWT login automatically:
import { RPCWrapperV0, PRIVILY_PLATFORM_ID } from 'privily-sdk';
const rpc = new RPCWrapperV0(starkPrivateKey, PRIVILY_PLATFORM_ID);
const balances = await rpc.getBalances();
console.log('Balances:', balances);
const prices = await rpc.getAssetsPrices();
console.log('Prices:', prices);4. Send Transactions
Use UnitsAccount to execute on-chain transactions with a STRK-based account:
import { UnitsProvider, UnitsAccount, retrieveAccountV0, PRIVILY_PLATFORM_ID } from 'privily-sdk';
const provider = new UnitsProvider('https://privily.karnot.xyz');
const address = retrieveAccountV0(starkPublicKey, PRIVILY_PLATFORM_ID);
const account = new UnitsAccount(provider, address, starkPrivateKey);
const { transaction_hash } = await account.sendTransaction([
{
contractAddress: tokenAddress,
entrypoint: 'transfer',
calldata: [recipientAddress, amountLow, amountHigh],
},
]);
const receipt = await account.waitForTransaction(transaction_hash);
console.log('TX status:', receipt.execution_status);Alternative: You can also generate a STRK private key directly with starknet.js (see Alternative: Direct Starknet.js Key Generation).
Module 1 — Bridge / Anonymity / Swap
Account Key Generation
Generate v0 (STRK) keys from chain-specific signatures. Each function accepts a chain provider/signer, the platform keygen message, and optional extra entropy.
v0 STRK Keys
| Function | Chain | Returns |
|---|---|---|
| accountV0KeysEvm(evmProvider, msg, extraEntropy?) | EVM | Promise<string> (STRK private key) |
| accountV0KeysSvm(svmProvider, msg, extraEntropy?) | Solana | Promise<string> |
| accountV0KeysSnvm(signer, msg, extraEntropy?) | Starknet | Promise<string> |
| accountV0KeysBtc(btcProvider, msg, extraEntropy?) | Bitcoin | Promise<string> |
| accountV0KeysTvm(tronWebTrx, msg, extraEntropy?) | Tron | Promise<string> |
v0 key note: these STRK keys are Privily-generated from the keygen flow (
accountV0Keys*). If you already have a wallet signature from ethers/solana/starknet wallets, pass the raw signature string as the thirdextraEntropyparameter for compatibility with wallet import/recovery in the dapp.
Alternative: Direct Starknet.js Key Generation
import { stark } from 'starknet';
import { getStarkPub } from 'privily-sdk';
const starkPrivateKey = stark.randomAddress();
const starkPublicKey = getStarkPub(starkPrivateKey);This direct flow can be imported into the dapp and used, but the wallet-link between the original wallet and the Privily account is not created, so wallet-linked recovery/import experiences may be limited.
Account Deployment
Smart account deployment is performed through the RPCWrapper instance. All deploy methods accept a single params object.
import { RPCWrapper } from 'privily-sdk';
const rpc = new RPCWrapper(); // public routes + deployment helpers| Method on rpc | Chain | Params type | Returns |
|---|---|---|---|
| rpc.deployEvmUserAccount(params) | EVM | DeployEVMAccountParams | Promise<StringResponse> |
| rpc.deploySvmUserAccount(params) | Solana | DeploySVMAccountParams | Promise<StringResponse> |
| rpc.deploySnvmUserAccount(params) | Starknet | DeploySNVMAccountParams | Promise<StringResponse> |
| rpc.deployLegacyBtcAccount(params) | BTC Legacy | DeployBTCAccountParams | Promise<StringResponse> |
| rpc.deployTaprootBtcAccount(params) | BTC Taproot | DeployBTCAccountParams | Promise<StringResponse> |
| rpc.deployTvmUserAccount(params) | Tron | DeployTVMAccountParams | Promise<StringResponse> |
All params types extend DeployAccountParamsBase:
type DeployAccountParamsBase = {
pk: string | Uint8Array; // v0 Stark public key: hex string (preferred) or Uint8Array
platform?: string; // Platform ID (default: Privily platform)
nonce?: number; // Account nonce (default: 0)
version?: number; // 0 = v0/STRK (default: 0)
refCode?: string; // Optional referral code
specialCode?: string; // Optional special code (e.g. fee exemption)
};Starknet-specific note: DeploySNVMAccountParams additionally requires accountDeployCalldata — use getSnvmAccountDeployData(provider, snvmAddress) to compute it.
import { getSnvmAccountDeployData } from 'privily-sdk';
const accountDeployCalldata = await getSnvmAccountDeployData(starknetProvider, snvmAddress);
await rpc.deploySnvmUserAccount({
signer: starknetSigner,
pk: starkPublicKey,
accountDeployCalldata,
});Account Retrieval
Compute Privily account addresses deterministically from a public key without deploying:
import { retrieveAccountV0, retrieveAccount, AccountVersion } from 'privily-sdk';
// v0 STRK account address
const addrV0 = retrieveAccountV0(starkPublicKey, platformId, index);
// Version-agnostic helper
const addrAny = retrieveAccount(pk, platformId, index, AccountVersion.v0);RPC Wrapper
RPCWrapper and RPCWrapperV0 handle Privily RPC operations. JWT login is automatic — wrappers refresh tokens as needed.
import { RPCWrapper, RPCWrapperV0 } from 'privily-sdk';
// Public/deployment wrapper (first arg is undefined because no key credentials are needed for public routes)
const rpc = new RPCWrapper(undefined, platformId, index, onLogin?);
// STRK-based auth (v0 accounts)
const rpcV0 = new RPCWrapperV0(starkPrivateKey, platformId, index, onLogin?);The optional onLogin callback is invoked after every successful authentication with the new JWT token and its expiry timestamp, allowing you to persist the session.
Auth State
| Method | Description |
|---|---|
| rpc.isLoggedIn() | Returns true if the current token is valid (with 2-min expiry buffer) |
| rpc.hasAccount() | Returns true if account credentials are set |
| rpc.setCredentials(token, expiry) | Manually inject a pre-obtained token (skips login) |
| rpc.setSk(sk) | Update the Stark private key (RPCWrapperV0 only) |
Available Methods
| Method | Auth | Description |
|---|---|---|
| Public (no auth) | | |
| getKeygenMessage(platform?) | ❌ | Get the platform keygen message |
| getAssets() | ❌ | Get supported assets configuration |
| getAssetsPrices(assets?) | ❌ | Get current market prices for assets |
| getLiquidityProviderConf() | ❌ | Get liquidity provider configuration |
| getBetaData(account) | ❌ | Get beta/waitlist status for an account |
| joinWaitlist(refCode) | ❌ | Request to join the beta waitlist |
| getPublicPaymentRequest(id) | ❌ | Get a public (open) payment request |
| publicPayRequest(params) | ❌ | Pay a public request without authentication |
| getPublicOperationStatus(id) | ❌ | Get operation status for non-authenticated operations |
| Account | | |
| getBalances(assets?) | ✅ | Get account balances on Privily |
| getOnchainBalances() | ✅ | Get on-chain balances across networks |
| getGasTokenBalance() | ✅ | Get gas token balances (own + executor) |
| claimGasToken() | ✅ | Claim gas tokens for transaction fees |
| Bridge / Swap | | |
| getBridgeData(params) | ✅ | Get bridge transaction data for cross-chain transfers |
| getDisperseBridgeData(params) | ✅ | Get bridge data for multi-recipient (disperse) transfers |
| commitBridgeQuotes(quoteIds) | ✅ | Commit previously fetched bridge quotes (up to 20) |
| processTransfer(params) | ✅ | Submit a transfer transaction for execution |
| processDelayedWithdrawals(params) | ✅ | Submit one or more delayed withdrawal transactions |
| fullWithdrawal(asset, to) | ✅ | Withdraw full balance of an asset to an address |
| History & Status | | |
| getOperationStatus(id) | ✅ | Get operation status by ID |
| getOperationHistory(side?, limit?, startKey?, altStartKey?) | ✅ | Paginated all-operation history |
| getDepositHistory(limit?, startKey?) | ✅ | Paginated deposit history |
| getTransferHistory(side?, limit?, startKey?) | ✅ | Paginated transfer history |
| getWithdrawalHistory(limit?, startKey?) | ✅ | Paginated withdrawal history |
| Saved Addresses | | |
| getWithdrawalAddresses(searchBy?, search?, limit?, startKey?) | ✅ | List saved withdrawal addresses |
| addWithdrawalAddress(address, label) | ✅ | Save a withdrawal address |
| updateWithdrawalAddress(id, address, label) | ✅ | Update a saved address |
| deleteWithdrawalAddress(id) | ✅ | Delete a saved address |
| Payment Requests | | |
| requestPayment(params) | ✅ | Create a payment request |
| getPaymentRequest(id) | ✅ | Get a payment request by ID |
| getSentPaymentRequests(limit?, startKey?) | ✅ | List sent payment requests |
| getReceivedPaymentRequests(limit?, startKey?) | ✅ | List received payment requests |
| cancelPaymentRequest(id) | ✅ | Cancel a payment request |
| Referrals | | |
| initRefCode() | ✅ | Initialize a referral code |
| setReferer(referer) | ✅ | Link a referrer (by code string or { address, index? }) |
| deleteReferer() | ✅ | Remove referrer link |
| getRefData() | ✅ | Get referral data and earnings |
| getReferees(limit?, startKey?) | ✅ | List referred users |
| getTradingRebates() | ✅ | Get trading rebates per asset |
Swap support note:
isExchangeroutes depend on liquidity-provider availability and are not supported forPRIVILY -> PRIVILYpairs.
Bridge Data Params
getBridgeData and getDisperseBridgeData accept structured params objects:
// Standard bridge (one recipient)
await rpc.getBridgeData({
inChain: 'ethereum', // Source chain
outChain: 'PRIVILY', // Destination chain
inAsset: '0x...ethAsset', // Source asset address
outAsset: '0x...privilyAsset', // Destination asset address
amount: '1000000', // Amount (in smallest unit, e.g. 6-decimal USDC)
from: '0x...depositorAddress', // Source address
to: { address: '0x...recipient' }, // Recipient (omit to send to self)
refuel: false, // Gas refuel on destination
execTimestamp: undefined, // number — Unix timestamp (seconds) for delayed execution; omit for immediate
requestId: 'req_...', // Optional linked payment request
conf: { isAnon: false }, // Optional operation flags (`isAnon`, `isSend`, `isExchange`)
});
// Disperse (one-to-many)
await rpc.getDisperseBridgeData({
inChain: 'ethereum',
inAsset: '0x...ethAsset',
from: '0x...depositor',
disperseData: [
{ outChain: 'PRIVILY', outAsset: '0x...a', amount: '500000', to: { address: '0x...r1' } },
{ outChain: 'PRIVILY', outAsset: '0x...b', amount: '500000', to: { address: '0x...r2' } },
],
});getBridgeData, getDisperseBridgeData, and publicPayRequest return a NewBridge object containing all required execution payloads (bridgeTx, optional delayedBridgeTx, IDs, provider metadata). This same shape is used for deposit, withdrawal, disperse, swap, and payment-request flows.
Anonymity semantics: Privily bridge/transfer/payment operations are private by design.
conf.isAnon/requestPayment.isAnonadds extra counterparty metadata masking in specific recipient/request history views; it does not toggle core privacy on/off.
Executing bridgeTx by Chain Type
bridge.bridgeTx is chain-specific:
- EVM (
EVMBridgeTxData = EvmTx[]): execute each tx with wallet/provider usingto,data, and optionalvalue. - Privily / Starknet (
SNVMBridgeTxData = Call[]): pass calls directly toUnitsAccountor Starknet accountexecute. - Solana (
SVMBridgeTxData = { transactionBase64: string }): decode serialized transaction payload, set fresh blockhash/fee payer as required by wallet flow, sign, then send. - Tron (
TVMBridgeTxData = TvmTx[]): use returned Tron call parameters (contractAddress,functionSelector,parametes, optionalvalue) to build and broadcast via TronWeb.
// EVM payload
for (const tx of bridge.bridgeTx as EvmTx[]) {
await evmWallet.sendTransaction({
to: tx.to,
data: tx.data,
value: tx.value ?? undefined,
});
}
// Privily / Starknet payload
await unitsOrSnAccount.execute(bridge.bridgeTx as Call[]);
// Solana payload (SVMBridgeTxData)
const svmPayload = bridge.bridgeTx as { transactionBase64: string };
const txBytes = Buffer.from(svmPayload.transactionBase64, 'base64');
const tx = solanaWeb3.VersionedTransaction.deserialize(txBytes);
tx.message.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.sign([payerKeypair]);
await connection.sendTransaction(tx);
// Tron payload
for (const tx of bridge.bridgeTx as TvmTx[]) {
const res = await tronWeb.transactionBuilder.triggerSmartContract(
tx.contractAddress,
tx.functionSelector,
{},
[tx.parametes],
tronWeb.address.toHex(sender),
);
const signed = await tronWeb.trx.sign(res.transaction);
await tronWeb.trx.sendRawTransaction(signed);
}Payment Request Params
requestPayment creates a payment request and returns its ID. The person who receives the request fulfills it by passing that ID as requestId to getBridgeData.
// --- Create a directed request (ask a specific user to pay you) ---
const requestId = await rpc.requestPayment({
to: { address: '0x...payerPrivilyAddress' }, // The Privily address of the person you are asking to pay
amount: '5000000', // 5 USDC (6 decimals)
asset: TOKENS.USDC.address,
memo: 'Invoice #42', // Optional message
isAnon: false, // Set true to hide requester identity from the payer's request view
});
console.log('Request created:', requestId);
// --- Create an open (public) request — anyone can pay it ---
const openRequestId = await rpc.requestPayment({
to: undefined, // No specific payer → open request payable by anyone
amount: '1000000',
asset: TOKENS.USDC.address,
memo: 'Tip jar',
});
// --- Retrieve a request (authenticated — must be sender or payer) ---
const req = await rpc.getPaymentRequest(requestId);
console.log('Status:', PAYMENT_REQUEST_STATUS_STR[req.status]);
// --- Retrieve an open request without auth ---
const publicReq = await rpc.getPublicPaymentRequest(openRequestId);
// --- Cancel a request you sent ---
await rpc.cancelPaymentRequest(requestId);publicPayRequest pays an open request without authentication. It returns the same NewBridge shape as getBridgeData; execute bridgeTx exactly as described in Executing bridgeTx by Chain Type:
// Pay a public request from an external EVM chain (no auth required)
const bridge = await rpc.publicPayRequest({
inChain: 'ethereum', // Source chain of the payer
requestId: openRequestId, // The open payment request ID
from: '0x...payerEvmAddress', // Optional: payer's address on the source chain
});
// Execute bridge.bridgeTx on Ethereum with the payer's wallet
console.log('Payment TX:', bridge.bridgeTx);Chain Address Utilities
Convert between blockchain addresses and the Privily ChainAddress format (a { low: bigint, high: bigint } pair that packs the address and chain discriminator into a 256-bit value):
import {
hexAddressToChainAddress,
bs58AddressToChainAddress,
addressToChainAddress,
chainAddressToU256,
getAddressDiscriminator,
ChainAddressDiscriminator,
} from 'privily-sdk';
// From EVM hex address + explicit discriminator
const chainAddr = hexAddressToChainAddress('0x1234...', ChainAddressDiscriminator.EVM);
// From Solana base58 address + explicit discriminator
const svmAddr = bs58AddressToChainAddress('AaBb...', ChainAddressDiscriminator.SVM);
// Auto-detect chain from address format
const auto = addressToChainAddress('0x1234...'); // works with EVM, SNVM, SVM, BTC, TVM
// Convert to u256 for contract calls
const u256 = chainAddressToU256(chainAddr);
// Detect discriminator only
const disc = getAddressDiscriminator('0xAbCd...'); // ChainAddressDiscriminator.EVMDiscriminators:
| Discriminator | Value | Chains |
|---|---|---|
| ChainAddressDiscriminator.EVM | "0x0" | Ethereum, Polygon, BSC, … |
| ChainAddressDiscriminator.SVM | "0x1" | Solana |
| ChainAddressDiscriminator.SNVM | "0x2" | Starknet |
| ChainAddressDiscriminator.BTC | "0x3" | Bitcoin (legacy + taproot) |
| ChainAddressDiscriminator.TVM | "0x4" | Tron |
Units Network
The Units network is Privily's app-chain. All Privily account operations (transfers, contract calls, program deployments) are executed on Units via these classes.
UnitsProvider
Low-level JSON-RPC client for the Units network:
import { UnitsProvider } from 'privily-sdk';
const provider = new UnitsProvider('https://privily.karnot.xyz');
const { chain_id } = await provider.getChainId();
const { nonce } = await provider.getNonce(accountAddress);UnitsAccount
On-chain account class for sending transactions, deploying programs, and reading state. Use UnitsAccount with a STRK private key for on-chain execution today:
import { UnitsAccount, UnitsProvider } from 'privily-sdk';
// STRK-based account (current recommended approach)
const account = new UnitsAccount(provider, address, privateKey);
// Send one or multiple calls
const { transaction_hash } = await account.sendTransaction([call1, call2]);
// or equivalently:
const { transaction_hash: txHash } = await account.execute(singleCall);
// Wait for confirmation
const receipt = await account.waitForTransaction(transaction_hash);
// receipt.execution_status: { type: "SUCCEEDED" } | { type: "REVERTED", error: string }
// Read contract state
const { result } = await account.call(contractAddress, 'get_balance', [userAddress]);
// Batch read
const results = await account.multiCall([
{ contractAddress, entrypoint: 'balance_of', calldata: [addr1] },
{ contractAddress, entrypoint: 'balance_of', calldata: [addr2] },
]);
// Declare a program (class)
await account.declareProgram(programArtifact, compiledHash, 'PUBLIC');
// Deploy a program instance via UDC
const { transaction_hash: deployTx, program_address } =
await account.deployProgram(classHash, constructorArgs, salt);
// Deploy this account on-chain (for undeployed accounts created with newUndeployedAccount)
await account.deploySelf();
// SNIP-9 outside execution (meta-transactions / off-chain signing)
const outsideCall = await account.getOutsideExecutionCall(calls, callerAddress);Account Factory Helpers
import { deployBaseAccount, getUnitsAccount } from 'privily-sdk';
// Deploy a new basic (STRK) account
const { privateKey, unitsAccount } = await deployBaseAccount(provider, sk);
// Load an existing account (no deployment)
const acc = getUnitsAccount(provider, address, privateKey);Starknet Utilities
Helper functions for Starknet data types and contract interactions:
import {
getProvider, getAccount,
u256ArrayToBn, bnToU256Array,
strToCairoStr, cairoStrToStr,
genStarkPrivate, getStarkPub,
getContract, declareContract, deployAccount,
waitForTransaction,
} from 'privily-sdk';
// Create a Starknet RPC provider / account
const provider = getProvider('https://your-starknet-rpc');
const account = getAccount(provider, accountAddress, privateKey);
// Data conversions
const value = u256ArrayToBn([lowFelt, highFelt]); // BigInt from u256 [low, high]
const [low, high] = bnToU256Array(1_000_000n); // u256 [low, high] from BigInt
// Cairo string encoding
const cairoStr = strToCairoStr('hello'); // BigNumberish[]
const original = cairoStrToStr(serializedArray); // string
// Key utilities
const sk = genStarkPrivate(); // Random Stark private key
const pk = getStarkPub(sk); // Corresponding Stark public keyFalcon512 — Coming Soon
🔬 Planned — not yet available.
Privily's post-quantum roadmap includes native Falcon512 on-chain account support. The following features are planned for a future release and are not yet available:
FalconUnitsAccount— On-chain account that signs transactions directly with Falcon512 keys (no STRK key required).deployFalconUnitsAccount/getFalconUnitsAccount— Factory helpers for Falcon-based accounts.getFalconAccount— Starknet utility for loading a Falcon account.- Native Starknet Falcon account contract — A Starknet smart-account contract secured by Falcon512 signatures, enabling a fully post-quantum Starknet identity.
For on-chain transactions today, use UnitsAccount with a STRK private key. See Quick Start and Recipe 2.
Constants & Assets
import {
PRIVILY_PLATFORM_ID, // Default Privily platform identifier
PRIVILY_CHAIN_ID, // Privily app-chain ID
ANY_CALLER, // Sentinel address for SNIP-9 (any caller allowed)
TOKENS, // { ETH, STRK, USDC, USDT } — decimals, address, wrapper, l1Address, min
WRAPPERS, // token address → wrapper contract address
WRAPPERS_TO_TOKEN, // wrapper address → token symbol
OPERATION_TYPE, // { DEPOSIT: 0, WITHDRAWAL: 1, TRANSFER: 2 }
OPERATION_TYPE_STR, // { 0: "Deposit", 1: "Withdrawal", 2: "Transfer" }
OPERATION_STATUS, // { PENDING: 0, EXECUTING_APPCHAIN: 1, SUCCESS: 2, FAILED: 3 }
OPERATION_STATUS_STR, // { 0: "Pending", … }
PAYMENT_REQUEST_STATUS, // { PENDING: 0, EXECUTED: 1, FAILED: 2, CANCELLED: 3 }
PAYMENT_REQUEST_STATUS_STR,
OPERATION_FLAG, // Bitmask flags: SEND, PAY, EXCHANGE, ANON_FROM, ANON_TO, REFUEL, DISPERSE
SIDE, // { FROM: 0, TO: 1 }
} from 'privily-sdk';Module 2 — Payment Provider (Beta)
⚠️ Not available yet. These features are not deployed on-chain at this time (including revenue sharing). Treat this section as roadmap/API preview only.
⚠️ No liability disclaimer: If you attempt unsupported integrations before deployment readiness, you assume all risk. Privily is not responsible for losses or side effects from unsupported usage.
Revenue Sharing
Not implemented yet. Contracts are not deployed.
Product Commerce (Beta)
Not implemented yet. Contracts are not deployed.
Subscription Management (Beta)
Not implemented yet. Contracts are not deployed.
Recipes & Examples
The examples below cover the most common real-world integration patterns. All examples use the EVM chain for key generation, but the same patterns apply for Solana, Starknet, Bitcoin, and Tron by substituting the relevant accountV0Keys* and deploy* functions.
Recipe 1: Full Setup (EVM wallet → Privily keys → deploy)
import {
RPCWrapper, RPCWrapperV0,
getAccountV0SkFromEntropy, getStarkPub,
PRIVILY_PLATFORM_ID,
} from 'privily-sdk';
async function fullSetup(evmProvider: EvmSigner) {
const rpc = new RPCWrapper();
// 1. Fetch the platform keygen message (public, no auth required)
const msg = await rpc.getKeygenMessage();
// 2. Sign with wallet SDK and use signature as extra entropy for v0 keygen
const walletSignature = (await evmProvider.sign(msg)).signature;
const starkPrivateKey = getAccountV0SkFromEntropy(msg, walletSignature);
const starkPublicKey = getStarkPub(starkPrivateKey);
// Persist Stark private key securely
console.log('starkPrivateKey:', starkPrivateKey);
console.log('starkPublicKey:', starkPublicKey);
// 3. Deploy Privily account (only needed once per wallet)
await rpc.deployEvmUserAccount({
signer: evmProvider,
pk: starkPublicKey,
platform: PRIVILY_PLATFORM_ID,
version: 0,
});
// 4. Authenticate subsequent calls using RPCWrapperV0
const rpcV0 = new RPCWrapperV0(starkPrivateKey, PRIVILY_PLATFORM_ID);
const balances = await rpcV0.getBalances();
console.log('Balances:', balances);
return { rpc, rpcV0, starkPrivateKey, starkPublicKey };
}Recipe 2: Restore Existing Keys from Storage
If you have already deployed a Privily account and saved your keys, reconstruct the wrapper and account directly — no keygen or deployment needed.
import {
RPCWrapperV0,
UnitsProvider, UnitsAccount,
retrieveAccountV0,
getStarkPub,
PRIVILY_PLATFORM_ID,
} from 'privily-sdk';
// --- v0 (STRK) account — on-chain transactions ---
const provider = new UnitsProvider('https://privily.karnot.xyz');
const starkPrivateKey = '0x...'; // previously saved private key
const starkPublicKey = getStarkPub(starkPrivateKey); // derive public key from private key
const addrV0 = retrieveAccountV0(starkPublicKey, PRIVILY_PLATFORM_ID);
const rpcV0 = new RPCWrapperV0(starkPrivateKey, PRIVILY_PLATFORM_ID);
const accountV0 = new UnitsAccount(provider, addrV0, starkPrivateKey);Tip: If you use a session callback (
onLogin) you can also restore a previously saved JWT token to skip the login round-trip:rpcV0.setCredentials(savedToken, savedExpiry); // expiry in ms
Bridge Execution Helper (All getBridgeData-Style Responses)
The same execution pattern applies to:
getBridgeDatagetDisperseBridgeDatapublicPayRequest
type BridgePayload = EvmTx[] | Call[] | { transactionBase64: string } | TvmTx[];
async function executeBridgeTxByChain(bridgeTx: BridgePayload, sourceChain: string) {
if (sourceChain === 'PRIVILY' || sourceChain === 'STARKNET') {
// bridgeTx is Call[] → execute with UnitsAccount or Starknet Account
await unitsOrSnAccount.execute(bridgeTx);
return;
}
if (sourceChain === 'solana') {
// bridgeTx is { transactionBase64 } in SDK types
const payload = bridgeTx as { transactionBase64: string };
const tx = solanaWeb3.VersionedTransaction.deserialize(
Buffer.from(payload.transactionBase64, 'base64')
);
tx.message.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.sign([payerKeypair]);
await connection.sendTransaction(tx);
return;
}
if (sourceChain === 'tron') {
// bridgeTx is TvmTx[]
for (const tx of bridgeTx as TvmTx[]) {
const built = await tronWeb.transactionBuilder.triggerSmartContract(
tx.contractAddress,
tx.functionSelector,
{},
[tx.parametes],
tronWeb.address.toHex(senderAddress),
);
const signed = await tronWeb.trx.sign(built.transaction);
await tronWeb.trx.sendRawTransaction(signed);
}
return;
}
// EVM-like chains: bridgeTx is EvmTx[]
for (const tx of bridgeTx as EvmTx[]) {
await evmWallet.sendTransaction({
to: tx.to,
data: tx.data,
value: tx.value ?? undefined,
});
}
}Recipe 3: Bridge (EVM → Privily)
Deposit assets from an external EVM chain into your Privily balance.
import { RPCWrapper, OPERATION_STATUS, OPERATION_STATUS_STR } from 'privily-sdk';
async function bridgeIn(rpc: RPCWrapper, evmDepositorAddress: string) {
// 1. Fetch bridge transaction data
const bridge = await rpc.getBridgeData({
inChain: 'ethereum',
outChain: 'PRIVILY',
inAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
outAsset: TOKENS.USDC.address, // USDC on Privily
amount: '1000000', // 1 USDC (6 decimals)
from: evmDepositorAddress,
// 'to' is omitted → funds go to your own Privily account
});
// 2. Execute the returned source-chain payload
await executeBridgeTxByChain(bridge.bridgeTx, 'ethereum');
// 3. Poll operation status on Privily
await pollOperationStatus(rpc, bridge.id);
}
async function pollOperationStatus(rpc: RPCWrapper, operationId: string) {
for (let i = 0; i < 60; i++) {
const { status } = await rpc.getOperationStatus(operationId);
console.log('Status:', OPERATION_STATUS_STR[status]);
if (status === OPERATION_STATUS.SUCCESS) {
console.log('✅ Bridge complete');
return;
}
if (status === OPERATION_STATUS.FAILED) {
throw new Error('❌ Bridge failed');
}
await new Promise(r => setTimeout(r, 5000)); // wait 5s
}
throw new Error('Timed out waiting for operation');
}Recipe 4: Bridge (Privily → EVM withdrawal)
Withdraw assets from your Privily balance back to an external chain.
async function bridgeOut(rpc: RPCWrapper, evmRecipient: string) {
const bridge = await rpc.getBridgeData({
inChain: 'PRIVILY',
outChain: 'ethereum',
inAsset: TOKENS.USDC.address,
outAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
amount: '1000000',
to: { address: evmRecipient },
});
// bridge.bridgeTx is a Privily app-chain transaction — submit via processTransfer
const opId = await rpc.processTransfer({ tx: bridge.bridgeTx });
console.log('Withdrawal submitted, operation ID:', opId);
await pollOperationStatus(rpc, opId);
}Recipe 5: Bridge — Delayed Withdrawal
Schedule a withdrawal to execute at a specific future Unix timestamp. Useful for vesting, escrow, or recurring payments.
async function delayedWithdrawal(rpc: RPCWrapper, evmRecipient: string) {
// Schedule execution 24 hours from now
const execTimestamp = Math.floor(Date.now() / 1000) + 86400;
const bridge = await rpc.getBridgeData({
inChain: 'PRIVILY',
outChain: 'ethereum',
inAsset: TOKENS.USDC.address,
outAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
amount: '1000000',
to: { address: evmRecipient },
execTimestamp, // Tells the bridge to schedule rather than execute immediately
});
// bridge.delayedBridgeTx contains the on-chain calls to schedule
// bridge.bridgeTx is the tx to approve/lock funds now
await rpc.processTransfer({ tx: bridge.bridgeTx, execTimestamp });
// Later, when execTimestamp is reached, submit the delayed tx:
if (bridge.delayedBridgeTx) {
await rpc.processDelayedWithdrawals(
bridge.delayedBridgeTx.map((tx: any) => ({ tx, execTimestamp }))
);
console.log('Delayed withdrawal submitted');
}
}Recipe 6: Bridge — Disperse (One-to-Many)
Send a single inbound amount and split it across multiple recipients in one operation.
async function disperseToMany(rpc: RPCWrapper, ethDepositor: string) {
const bridge = await rpc.getDisperseBridgeData({
inChain: 'ethereum',
inAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
from: ethDepositor,
disperseData: [
{
outChain: 'PRIVILY',
outAsset: TOKENS.USDC.address,
amount: '500000', // 0.5 USDC to recipient 1
to: { address: '0x...recipient1' },
},
{
outChain: 'PRIVILY',
outAsset: TOKENS.USDC.address,
amount: '500000', // 0.5 USDC to recipient 2
to: { address: '0x...recipient2' },
},
{
outChain: 'solana',
outAsset: '...usdcOnSolana',
amount: '1000000', // 1 USDC to a Solana recipient
to: { address: 'SolAnaRecipientBase58...' },
},
],
});
await executeBridgeTxByChain(bridge.bridgeTx, 'ethereum');
// Track all sub-operations via bridge.id / bridge.altId
}Recipe 7: Swap (Eligible Non-Privily Chains)
Swap is not supported when both legs are on PRIVILY. Use supported external chains (EVM/SNVM/SVM/etc.), and note liquidity-provider routing can make some swaps unavailable at runtime.
async function swapAssets(rpc: RPCWrapper) {
// Example: swap from Ethereum USDC to Starknet USDC (non-PRIVILY swap path)
const bridge = await rpc.getBridgeData({
inChain: 'ethereum',
outChain: 'STARKNET',
inAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Ethereum USDC
outAsset: '0x...starknetUsdc',
amount: '1000000', // 1 USDC
from: '0x...payerEvmAddress',
conf: { isExchange: true }, // Flag this as an exchange operation
});
await executeBridgeTxByChain(bridge.bridgeTx, 'ethereum');
console.log('Swap source transaction(s) submitted');
}Recipe 8: Anonymous Transfer
All Privily transfers are private by default. isAnon is an additional history-visibility control for counterparty metadata in recipient/request contexts (it is not a global on/off privacy switch).
async function anonTransfer(rpc: RPCWrapper) {
const bridge = await rpc.getBridgeData({
inChain: 'PRIVILY',
outChain: 'PRIVILY',
inAsset: TOKENS.USDC.address,
outAsset: TOKENS.USDC.address,
amount: '1000000',
to: { address: '0x...recipientPrivilyAddress' },
conf: { isAnon: true }, // Extra metadata masking in recipient/request history views
});
const opId = await rpc.processTransfer({
tx: bridge.bridgeTx,
conf: { isAnon: true },
});
console.log('Anonymous transfer submitted:', opId);
}Recipe 9: Paginate Operation History
Fetch and iterate through your full operation history using cursor-based pagination.
import { RPCWrapper, OPERATION_STATUS_STR, OPERATION_TYPE_STR, SIDE } from 'privily-sdk';
async function loadHistory(rpc: RPCWrapper) {
let startKey: Record<string, any> | undefined;
const allOps = [];
do {
const page = await rpc.getOperationHistory(
null, // side: null = all, SIDE.FROM = sent, SIDE.TO = received
20, // page size
startKey, // cursor from previous page (undefined for first page)
);
for (const op of page.items) {
console.log(
`[${OPERATION_TYPE_STR[op.operationType]}]`,
`Status: ${OPERATION_STATUS_STR[op.status]}`,
`Amount: ${op.inAmount}`,
`Chain: ${op.inChain} → ${op.outChain ?? op.inChain}`,
op.isSend ? '(sent)' : '(received)',
(op.isAnonFrom || op.isAnonTo) ? '(anon-flagged)' : '',
);
}
allOps.push(...page.items);
startKey = page.lastKey; // undefined when last page reached
} while (startKey);
console.log(`Total operations: ${allOps.length}`);
}Equivalent helpers for typed history:
// Deposits only
const deposits = await rpc.getDepositHistory(20, startKey);
// Transfers (sent/received)
const sentTxs = await rpc.getTransferHistory(SIDE.FROM, 20, startKey);
const receivedTxs = await rpc.getTransferHistory(SIDE.TO, 20, startKey);
// Withdrawals only
const withdrawals = await rpc.getWithdrawalHistory(20, startKey);Recipe 10: Manage Withdrawal Addresses
Save and manage trusted addresses for fast withdrawals.
async function manageAddresses(rpc: RPCWrapper) {
// Add a new address
await rpc.addWithdrawalAddress(
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // EVM address
'My Metamask (Ethereum)'
);
// List all saved addresses (paginated)
const { items } = await rpc.getWithdrawalAddresses(
undefined, // searchBy field (undefined = no filter)
undefined, // search term
20, // limit
);
console.log('Saved addresses:', items);
// Search by label
const { items: filtered } = await rpc.getWithdrawalAddresses('label', 'Metamask');
// Update a saved address
await rpc.updateWithdrawalAddress(
items[0].id,
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
'My Metamask (Ethereum) — updated'
);
// Delete an address
await rpc.deleteWithdrawalAddress(items[0].id);
}Recipe 11: Referral Program
async function referralFlow(rpc: RPCWrapper) {
// Initialize your referral code (do once)
const { refCode } = await rpc.initRefCode();
console.log('Your referral code:', refCode);
// Link a referrer (do once, before first transaction)
await rpc.setReferer('FRIEND123'); // by ref code string
// Get your referral stats
const refData = await rpc.getRefData();
console.log('Referral data:', refData);
// List users you referred (paginated)
const { items: referees } = await rpc.getReferees(20);
console.log('Referees:', referees);
// Get your trading rebates per asset
const rebates = await rpc.getTradingRebates();
console.log('Rebates:', rebates);
}Recipe 12: Create a Payment Request
Request funds from another Privily user (directed) or from anyone (open/public). The recipient pays by calling getBridgeData with the returned requestId.
import { RPCWrapper, TOKENS, PAYMENT_REQUEST_STATUS, PAYMENT_REQUEST_STATUS_STR } from 'privily-sdk';
async function createPaymentRequests(rpc: RPCWrapper) {
// --- Directed request: ask a specific Privily user to pay you ---
const requestId = await rpc.requestPayment({
to: { address: '0x...payerPrivilyAddress' }, // Privily account of the person you are billing
amount: '5000000', // 5 USDC (6 decimals)
asset: TOKENS.USDC.address,
memo: 'Invoice #42 — freelance work',
isAnon: false, // Set true to hide requester metadata from the payer's request view
});
console.log('Directed request ID:', requestId);
// --- Open (public) request: anyone can pay it ---
const openRequestId = await rpc.requestPayment({
to: undefined, // No specific payer → open request
amount: '1000000', // 1 USDC
asset: TOKENS.USDC.address,
memo: 'Tip jar ☕',
});
console.log('Open request ID:', openRequestId);
// --- Check status of a sent request ---
const req = await rpc.getPaymentRequest(requestId);
console.log('Status:', PAYMENT_REQUEST_STATUS_STR[req.status]);
// PENDING → EXECUTED once paid, CANCELLED if you cancel
// --- List all requests you have sent (paginated) ---
const { items: sent } = await rpc.getSentPaymentRequests(20);
for (const r of sent) {
console.log(r.id, PAYMENT_REQUEST_STATUS_STR[r.status], r.amount, r.memo);
}
// --- List all requests you have received (paginated) ---
const { items: received } = await rpc.getReceivedPaymentRequests(20);
for (const r of received) {
console.log(r.id, PAYMENT_REQUEST_STATUS_STR[r.status]);
}
// --- Cancel a pending request ---
await rpc.cancelPaymentRequest(requestId);
console.log('Request cancelled');
}Recipe 13: Pay a Received Payment Request
When you receive a payment request, settle it by passing its ID as requestId to getBridgeData. The bridge fields (outChain, outAsset, amount, to) are automatically resolved from the request — you only need to supply the source-chain details.
import { RPCWrapper, TOKENS, PAYMENT_REQUEST_STATUS } from 'privily-sdk';
async function payReceivedRequest(rpc: RPCWrapper, requestId: string) {
// First, inspect the request (optional — to confirm amount / asset)
const req = await rpc.getPaymentRequest(requestId);
if (req.status !== PAYMENT_REQUEST_STATUS.PENDING) {
throw new Error(`Request is not payable (status: ${req.status})`);
}
// Get bridge transaction data — pass requestId to auto-fill outChain/outAsset/amount/to
const bridge = await rpc.getBridgeData({
inChain: 'PRIVILY', // Paying from within your Privily balance
requestId, // Auto-fills destination details from the request
});
// Submit the bridge transaction from Privily
const opId = await rpc.processTransfer({ tx: bridge.bridgeTx });
console.log('Payment submitted, operation ID:', opId);
// Or if paying from an external chain (e.g. EVM depositor):
const bridgeFromEvm = await rpc.getBridgeData({
inChain: 'ethereum',
inAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
from: '0x...yourEvmAddress',
requestId, // Fills outChain/outAsset/amount/to from the request
});
await executeBridgeTxByChain(bridgeFromEvm.bridgeTx, 'ethereum');
}Recipe 14: Pay a Public (Open) Payment Request Without Auth
Open payment requests can be paid by anyone — including users who don't have a Privily account. Use getPublicPaymentRequest to inspect and publicPayRequest to obtain the bridge transaction.
import { RPCWrapper } from 'privily-sdk';
async function payPublicRequest(rpc: RPCWrapper, openRequestId: string) {
// Inspect the request — no auth required
const req = await rpc.getPublicPaymentRequest(openRequestId);
console.log('Paying:', req.amount, 'of asset', req.asset, '— memo:', req.memo);
// Get the bridge transaction — source chain and depositor address are all that's needed
const bridge = await rpc.publicPayRequest({
inChain: 'ethereum', // Source chain of the payer
requestId: openRequestId, // The open request to fulfill
from: '0x...payerEvmAddress', // Payer's address on the source chain (optional)
});
// Execute on the payer's source chain
await executeBridgeTxByChain(bridge.bridgeTx, 'ethereum');
// After submission, anyone can track the operation:
const status = await rpc.getPublicOperationStatus(bridge.id);
console.log('Operation status code:', status);
}Contributing
git clone https://github.com/PrivilyFi/sdk.git
cd sdk
npm install
npm run buildLicense
MIT — see LICENSE for details.
