# Get Token Balance Source: https://docs.chipipay.com/sdk/api/endpoint/get-token-balance GET /chipi-wallets/token-balance Get the balance of a specific token for a wallet ```bash cURL theme={null} curl -X GET "https://api.chipipay.com/v1/chipi-wallets/token-balance?externalUserId=user_123&chainToken=USDC&chain=STARKNET" \ -H "Authorization: Bearer sk_prod_xxxx" \ -H "x-api-key: pk_xxxx" ``` ```typescript TypeScript theme={null} const params = new URLSearchParams({ externalUserId: "user_123", chainToken: "USDC", chain: "STARKNET" }); const response = await fetch( `https://api.chipipay.com/v1/chipi-wallets/token-balance?${params}`, { headers: { "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } } ); const balance = await response.json(); // balance.balance is a raw integer string — use BigInt for precision const raw = BigInt(balance.balance); const scale = 10n ** BigInt(balance.decimals); const whole = raw / scale; const fraction = (raw % scale).toString().padStart(balance.decimals, "0").replace(/0+$/, ""); const humanReadable = fraction ? `${whole}.${fraction}` : `${whole}`; ``` ```python Python theme={null} import requests from decimal import Decimal response = requests.get( "https://api.chipipay.com/v1/chipi-wallets/token-balance", params={ "externalUserId": "user_123", "chainToken": "USDC", "chain": "STARKNET" }, headers={ "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } ) balance = response.json() human_readable = Decimal(balance["balance"]) / (Decimal(10) ** balance["decimals"]) ``` ```json 200 Response theme={null} { "chain": "STARKNET", "chainToken": "USDC", "chainTokenAddress": "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", "decimals": 6, "balance": "15000000" } ``` ## Notes * The `balance` field is a raw integer string. Divide by `10^decimals` to get the human-readable amount. In the example above, `15000000 / 10^6 = 15.0 USDC`. * You can identify the wallet by either `externalUserId` or `walletPublicKey`. At least one is required. # Get Transaction List Source: https://docs.chipipay.com/sdk/api/endpoint/get-transaction-list GET /transactions/transaction-list Get paginated transaction history for a wallet ```bash cURL theme={null} curl -X GET "https://api.chipipay.com/v1/transactions/transaction-list?walletAddress=0x049d365...&page=1&limit=10" \ -H "Authorization: Bearer sk_prod_xxxx" \ -H "x-api-key: pk_xxxx" ``` ```typescript TypeScript theme={null} const params = new URLSearchParams({ walletAddress: "0x049d365...", page: "1", limit: "10" }); const response = await fetch( `https://api.chipipay.com/v1/transactions/transaction-list?${params}`, { headers: { "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } } ); const { data, total, totalPages } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://api.chipipay.com/v1/transactions/transaction-list", params={ "walletAddress": "0x049d365...", "page": 1, "limit": 10 }, headers={ "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } ) result = response.json() transactions = result["data"] ``` ```json 200 Response theme={null} { "data": [ { "id": "tx_abc123", "type": "SEND", "chain": "STARKNET", "transactionHash": "0x1234567890abcdef...", "senderAddress": "0x049d365...", "destinationAddress": "0x0abcdef...", "token": "USDC", "tokenAddress": "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", "amount": "10500000", "status": "SUCCESS", "createdAt": "2025-03-15T10:30:00Z", "confirmedAt": "2025-03-15T10:31:00Z", "updatedAt": "2025-03-15T10:31:00Z", "apiPublicKey": "pk_xxxx" } ], "total": 42, "page": 1, "limit": 10, "totalPages": 5 } ``` ## Date Filtering You can filter transactions by date using `day`, `month`, and `year` parameters: ```bash theme={null} # Get all transactions from March 2025 curl "https://api.chipipay.com/v1/transactions/transaction-list?walletAddress=0x...&month=3&year=2025" # Get transactions from March 15, 2025 curl "https://api.chipipay.com/v1/transactions/transaction-list?walletAddress=0x...&day=15&month=3&year=2025" ``` ## Function Filtering Filter by contract function name to see specific types of transactions: ```bash theme={null} # Only transfer transactions curl "...&calledFunction=transfer" # Only approve transactions curl "...&calledFunction=approve" ``` # Get USD Amount Source: https://docs.chipipay.com/sdk/api/endpoint/get-usd-amount GET /exchanges/usd-amount Convert an amount from MXN or other currency to USD ```bash cURL theme={null} curl -X GET "https://api.chipipay.com/v1/exchanges/usd-amount?currencyAmount=250¤cy=MXN" \ -H "Authorization: Bearer sk_prod_xxxx" \ -H "x-api-key: pk_xxxx" ``` ```typescript TypeScript theme={null} const params = new URLSearchParams({ currencyAmount: "250", currency: "MXN" }); const response = await fetch( `https://api.chipipay.com/v1/exchanges/usd-amount?${params}`, { headers: { "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } } ); const usdAmount = await response.json(); // e.g., 13.42 ``` ```python Python theme={null} import requests response = requests.get( "https://api.chipipay.com/v1/exchanges/usd-amount", params={"currencyAmount": 250, "currency": "MXN"}, headers={ "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } ) usd_amount = response.json() # e.g., 13.42 ``` ```json 200 Response theme={null} 13.42 ``` ## Notes * The response is a plain number, not wrapped in an object. * Exchange rates are fetched in real-time. The rate may change between calls. * Supported currencies: `MXN` (Mexican Peso), `USD` (US Dollar). # Get Wallet Source: https://docs.chipipay.com/sdk/api/endpoint/get-wallet GET /chipi-wallets/by-user Retrieve a wallet by external user ID ```bash cURL theme={null} curl -X GET "https://api.chipipay.com/v1/chipi-wallets/by-user?externalUserId=user_123" \ -H "Authorization: Bearer sk_prod_xxxx" \ -H "x-api-key: pk_xxxx" \ -H "Content-Type: application/json" ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.chipipay.com/v1/chipi-wallets/by-user?externalUserId=user_123", { method: "GET", headers: { "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", "Content-Type": "application/json" } } ); const wallet = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://api.chipipay.com/v1/chipi-wallets/by-user", params={"externalUserId": "user_123"}, headers={ "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", } ) wallet = response.json() ``` ```json 200 Response theme={null} { "id": "wallet_abc123", "externalUserId": "user_123", "organizationId": "org_xyz", "apiPublicKey": "pk_xxxx", "publicKey": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "normalizedPublicKey": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "encryptedPrivateKey": "U2FsdGVkX1...", "isDeployed": true, "walletType": "CHIPI", "chain": "STARKNET", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ``` ```json 404 Not Found theme={null} { "error": "wallet_not_found", "message": "No wallet found for this user" } ``` # Record Send Transaction Source: https://docs.chipipay.com/sdk/api/endpoint/record-send-transaction POST /transactions/record-send Record a token transfer that was executed on-chain ```bash cURL theme={null} curl -X POST "https://api.chipipay.com/v1/transactions/record-send" \ -H "Authorization: Bearer sk_prod_xxxx" \ -H "x-api-key: pk_xxxx" \ -H "Content-Type: application/json" \ -d '{ "transactionHash": "0x1234567890abcdef...", "expectedSender": "0x049d365...", "expectedRecipient": "0x0abcdef...", "expectedToken": "USDC", "expectedAmount": "10.5", "chain": "STARKNET" }' ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.chipipay.com/v1/transactions/record-send", { method: "POST", headers: { "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", "Content-Type": "application/json" }, body: JSON.stringify({ transactionHash: "0x1234567890abcdef...", expectedSender: "0x049d365...", expectedRecipient: "0x0abcdef...", expectedToken: "USDC", expectedAmount: "10.5", chain: "STARKNET" }) } ); const transaction = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://api.chipipay.com/v1/transactions/record-send", headers={ "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", "Content-Type": "application/json" }, json={ "transactionHash": "0x1234567890abcdef...", "expectedSender": "0x049d365...", "expectedRecipient": "0x0abcdef...", "expectedToken": "USDC", "expectedAmount": "10.5", "chain": "STARKNET" } ) transaction = response.json() ``` ```json 200 Response theme={null} { "id": "tx_abc123", "type": "SEND", "chain": "STARKNET", "transactionHash": "0x1234567890abcdef...", "senderAddress": "0x049d365...", "destinationAddress": "0x0abcdef...", "token": "USDC", "tokenAddress": "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", "amount": "10500000", "status": "PENDING", "createdAt": "2025-03-15T10:30:00Z", "updatedAt": "2025-03-15T10:30:00Z", "apiPublicKey": "pk_xxxx" } ``` ## Notes * The `expectedAmount` is in human-readable format (e.g., `"10.5"` for 10.5 USDC). The API converts it to raw format internally. * The transaction is verified on-chain. If the hash doesn't match the expected sender/recipient/amount, the request will fail. # Update Wallet Encryption Source: https://docs.chipipay.com/sdk/api/endpoint/update-wallet-encryption PATCH /chipi-wallets/update-encryption-details Replace the stored encrypted private key after a passkey or PIN change ```bash cURL theme={null} curl -X PATCH "https://api.chipipay.com/v1/chipi-wallets/update-encryption-details" \ -H "Authorization: Bearer sk_prod_xxxx" \ -H "x-api-key: pk_xxxx" \ -H "Content-Type: application/json" \ -d '{ "externalUserId": "user_123", "newEncryptedPrivateKey": "U2FsdGVkX1+newEncryptedKey..." }' ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.chipipay.com/v1/chipi-wallets/update-encryption-details", { method: "PATCH", headers: { "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", "Content-Type": "application/json" }, body: JSON.stringify({ externalUserId: "user_123", newEncryptedPrivateKey: "U2FsdGVkX1+newEncryptedKey..." }) } ); ``` ```python Python theme={null} import requests response = requests.patch( "https://api.chipipay.com/v1/chipi-wallets/update-encryption-details", headers={ "Authorization": "Bearer sk_prod_xxxx", "x-api-key": "pk_xxxx", "Content-Type": "application/json" }, json={ "externalUserId": "user_123", "newEncryptedPrivateKey": "U2FsdGVkX1+newEncryptedKey..." } ) ``` ```json 200 Response theme={null} { "success": true } ``` After calling this endpoint, the previous passkey or PIN used to encrypt the private key will no longer work. Make sure the new encryption is persisted before discarding the old key material. # Introduction to Chipi Pay API Source: https://docs.chipipay.com/sdk/api/introduction REST API for creating self-custodial wallets, executing gasless transactions, and purchasing services on Starknet. ## Base URL ```bash theme={null} https://api.chipipay.com/v1 ``` ## Authentication All API requests require two headers: | Header | Value | Description | | --------------- | --------------------- | ------------------------------------------------------- | | `x-api-key` | `pk_xxxx` | Your Chipi public API key | | `Authorization` | `Bearer sk_prod_xxxx` | Your secret key (server-side) or user JWT (client-side) | ```bash theme={null} curl -H "x-api-key: pk_xxxx" \ -H "Authorization: Bearer sk_prod_xxxx" \ https://api.chipipay.com/v1/skus ``` For detailed instructions on obtaining your API keys, see the [API Quickstart](/sdk/api/quickstart). ## API Endpoints ### Wallets | Method | Endpoint | Description | | ------- | ---------------------------------------------------------------------------------------- | ------------------------ | | `GET` | [`/chipi-wallets/by-user`](/sdk/api/endpoint/get-wallet) | Get wallet by user ID | | `GET` | [`/chipi-wallets/token-balance`](/sdk/api/endpoint/get-token-balance) | Get token balance | | `PATCH` | [`/chipi-wallets/update-encryption-details`](/sdk/api/endpoint/update-wallet-encryption) | Update wallet encryption | ### Transactions | Method | Endpoint | Description | | ------ | -------------------------------------------------------------------------- | ------------------------- | | `POST` | [`/transactions/record-send`](/sdk/api/endpoint/record-send-transaction) | Record a send transaction | | `GET` | [`/transactions/transaction-list`](/sdk/api/endpoint/get-transaction-list) | Get transaction history | ### Services (SKUs) | Method | Endpoint | Description | | ------ | ----------------------------------------------------------- | ----------------------- | | `GET` | [`/skus`](/sdk/api/endpoint/get-skus) | List available services | | `GET` | [`/skus/{id}`](/sdk/api/endpoint/get-sku) | Get a single service | | `POST` | [`/sku-purchases`](/sdk/api/endpoint/buy-sku) | Purchase a service | | `GET` | [`/sku-purchases/{id}`](/sdk/api/endpoint/get-sku-purchase) | Get purchase status | ### Exchanges | Method | Endpoint | Description | | ------ | ----------------------------------------------------------- | ----------------------- | | `GET` | [`/exchanges/usd-amount`](/sdk/api/endpoint/get-usd-amount) | Convert currency to USD | ## Notifications and Webhooks When you purchase a service, you'll receive status updates via webhooks or email at the URL and email address you configured in your dashboard. ### Steps to set up your webhook * For setting up webhooks and email notifications, see the [API Quickstart](/sdk/api/quickstart). ## Rate Limits API requests are limited to 100 requests per minute per API key. ## Error Handling The API uses standard HTTP status codes to indicate success or failure: * `200` - Success * `400` - Bad Request * `401` - Unauthorized * `404` - Not Found * `429` - Rate Limited * `500` - Internal Server Error Error responses include a JSON object with an `error` and `message` field: ```json theme={null} { "error": "invalid_request", "message": "The request was invalid" } ``` # API Quickstart Source: https://docs.chipipay.com/sdk/api/quickstart Get started with the Chipi API - learn how to authenticate, make requests, and handle responses ## Base URL ```bash theme={null} https://api.chipipay.com/v1 ``` ## Authentication All API requests require authentication using your API key. Include it in the `Authorization` header: ```bash theme={null} Authorization: Bearer YOUR_API_KEY ``` ### Getting Your API Key To use the Chipi Pay API, you'll need to obtain your API key from the dashboard. #### Steps to Get Your API Key: Go to the [Configure API Keys](https://dashboard.chipipay.com/configure/api-keys) section in your dashboard. Inside you will see API Keys: * Public Key: Can be safely shared and is used to identify your application * **Secret Key**: This is your API Key. This is the key you will use to authenticate all the calls to our API Never commit your API key to version control or share it publicly. Always use environment variables or secure configuration management. ## Webhooks You can set up your webhook to receive notifications [here](https://dashboard.chipipay.com/configure/webhooks) ## Email Notifications You can set up an email to receive notifications [here](https://dashboard.chipipay.com/configure/notifications) ## Testing You have two environments in your dashboard **Production** and **Development**. Remember to Do your tests using the Development environment. Sandbox API keys are clearly marked and won't process real transactions. ## Support Need help? We're here to assist: * **Documentation**: Browse our comprehensive guides * **Community**: Join our [Telegram group](https://t.me/+e2qjHEOwImkyZDVh) * **Email**: [support@chipipay.com](mailto:support@chipipay.com) * **Dashboard**: [dashboard.chipipay.com](https://dashboard.chipipay.com) # Quickstart Source: https://docs.chipipay.com/sdk/api/wallets-quickstart Quick setup guide for gasless transactions using only the Chipi API You'll need to configure your auth provider's JWKS endpoint in the [Chipi Dashboard](https://dashboard.chipipay.com). The dashboard quickstart detects your auth provider and walks you through the setup. With this JWKS Endpoint setup, you can now run secure code in your frontend using a rotating JWT token. ### Common auth providers The dashboard auto-configures JWKS when you provide your Clerk Secret Key. See our tutorial on how to set up Chipi with [Firebase](/sdk/nextjs/gasless-firebase-setup). See our tutorial on how to set up Chipi with [Supabase](/sdk/react/gasless-supabase-setup). Go to the [Chipi Dashboard](https://dashboard.chipipay.com) and open your project. The quickstart shows your `.env` snippet with both keys ready to copy. You'll need: * **Public Key** (`pk_prod_xxxx`) — used client-side * **Secret Key** (`sk_prod_xxxx`) — used by the server-side `ChipiProvider` Since this wallet is self-custodial, there are many steps that need to be done in the frontend. Here is the code to create a wallet for your user. Chipi supports two wallet types: * **CHIPI** (default): OpenZeppelin account with SNIP-9 session keys support * **READY**: Argent X Account v0.4.0 ```typescript theme={null} // External dependencies (you'll need to install these) import CryptoJS from "crypto-js"; import { Account, CairoCustomEnum, CairoOption, CairoOptionVariant, CallData, ec, hash, num, RpcProvider, stark, } from "starknet"; // Wallet type configuration type WalletType = "CHIPI" | "READY"; // Class hashes for each wallet type const WALLET_CLASS_HASHES = { CHIPI: "0x53f4f8791ed5bed0fddaa553d180c664e32cfaf8316bb232ae77bb08f459f2a", READY: "0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f", }; // RPC endpoints per wallet type const WALLET_RPC_ENDPOINTS = { CHIPI: "https://starknet-mainnet.public.blastapi.io/rpc/v0_7", READY: "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.7", }; // Deployment data type interface DeploymentData { class_hash: string; salt: string; unique: string; calldata: string[]; } // Encryption utility const encryptPrivateKey = ( privateKey: string, password: string, ): string => { if (!privateKey || !password) { throw new Error("Private key and password are required"); } return CryptoJS.AES.encrypt(privateKey, password).toString(); }; // Types (included inline) interface WalletData { publicKey: string; encryptedPrivateKey: string; } interface CreateWalletParams { encryptKey: string; externalUserId: string; apiPublicKey: string; bearerToken: string; chain: string; // Currently only "STARKNET" is supported walletType?: WalletType; // Optional, defaults to "CHIPI" } // Backend URL constant const BACKEND_URL = "https://api.chipipay.com/v1"; // Build constructor calldata based on wallet type const buildConstructorCallData = ( walletType: WalletType, starkKeyPubAX: string ): string[] => { if (walletType === "READY") { // Argent X Account: owner (CairoCustomEnum) + guardian (CairoOption None) const axSigner = new CairoCustomEnum({ Starknet: { pubkey: starkKeyPubAX }, }); const axGuardian = new CairoOption(CairoOptionVariant.None); return CallData.compile({ owner: axSigner, guardian: axGuardian, }); } // ChipiWallet (default): Simple OpenZeppelin account with just public_key return CallData.compile({ public_key: starkKeyPubAX, }); }; // Main function export const createWallet = async ( params: CreateWalletParams ): Promise => { try { const { encryptKey, apiPublicKey, bearerToken, walletType = "CHIPI" // Default to CHIPI wallet } = params; // Select RPC endpoint based on wallet type const rpcUrl = WALLET_RPC_ENDPOINTS[walletType]; const provider = new RpcProvider({ nodeUrl: rpcUrl }); // Generating the private key with Stark Curve const privateKeyAX = stark.randomAddress(); const starkKeyPubAX = ec.starkCurve.getStarkKey(privateKeyAX); // Select class hash based on wallet type const accountClassHash = WALLET_CLASS_HASHES[walletType]; // Build constructor calldata based on wallet type const constructorCallData = buildConstructorCallData(walletType, starkKeyPubAX); // Calculate future address of the account const publicKey = hash.calculateContractAddressFromHash( starkKeyPubAX, accountClassHash, constructorCallData, 0 ); // Initiating Account const account = new Account(provider, publicKey, privateKeyAX); // Backend Call API to create the wallet const typeDataResponse = await fetch(`${BACKEND_URL}/chipi-wallets/prepare-creation`, { method: "POST", headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearerToken}`, 'x-api-key': apiPublicKey, }, body: JSON.stringify({ publicKey, walletType, }), }); const { typeData, accountClassHash: accountClassHashResponse } = await typeDataResponse.json(); // Sign the message const userSignature = await account.signMessage(typeData); const deploymentData: DeploymentData = { class_hash: accountClassHashResponse, salt: starkKeyPubAX, unique: `${num.toHex(0)}`, calldata: constructorCallData.map((value: any) => num.toHex(value)), }; const encryptedPrivateKey = encryptPrivateKey(privateKeyAX, encryptKey); // Call the API to save the wallet in dashboard const executeTransactionResponse = await fetch(`${BACKEND_URL}/chipi-wallets`, { method: "POST", headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearerToken}`, 'x-api-key': apiPublicKey, }, body: JSON.stringify({ apiPublicKey, publicKey, walletType, userSignature: { r: (userSignature as any).r.toString(), s: (userSignature as any).s.toString(), recovery: (userSignature as any).recovery }, typeData, encryptedPrivateKey, deploymentData: { ...deploymentData, salt: `${deploymentData.salt}`, calldata: deploymentData.calldata.map((data: any) => `${data}`), } }), }); const executeTransaction = await executeTransactionResponse.json(); console.log("Execute transaction: ", executeTransaction); if (executeTransaction.success) { return { success: true, txHash: executeTransaction.txHash, wallet: { publicKey: executeTransaction.walletPublicKey, encryptedPrivateKey: encryptedPrivateKey, } as WalletData, }; } else { return { success: false, txHash: "", wallet: { publicKey: "", encryptedPrivateKey: "", } as WalletData, }; } } catch (error: unknown) { console.error("Error detallado:", error); if (error instanceof Error && error.message.includes("SSL")) { throw new Error( "SSL connection error. Try using NODE_TLS_REJECT_UNAUTHORIZED=0 or verify the RPC URL" ); } throw new Error( `Error creating wallet: ${ error instanceof Error ? error.message : "Unknown error" }` ); } }; ``` Now you can create a wallet for your user. For the bearerToken you need to pass a valid JWT token from your auth provider. ### Create a ChipiWallet (Default) ChipiWallet is an OpenZeppelin account with SNIP-9 session keys support, perfect for gasless transactions and delegated access. ```typescript theme={null} // Example usage with ChipiWallet (default): const params: CreateWalletParams = { encryptKey: "user-secure-pin", // Encryption key for the private key externalUserId: "user-123", // Your user's unique identifier apiPublicKey: "pk_prod_***", // Your Chipi Pay public key bearerToken: "your-bearer-token", // Valid JWT token from your Auth Provider chain: "STARKNET", // Currently only STARKNET is supported // walletType: "CHIPI", // Optional - CHIPI is the default }; createWallet(params) .then(response => console.log("Wallet created:", response)) .catch(error => console.error("Error:", error)); ``` ### Create a Ready Wallet If you need Argent X compatibility: ```typescript theme={null} // Example usage with Ready wallet: const params: CreateWalletParams = { encryptKey: "user-secure-pin", externalUserId: "user-123", apiPublicKey: "pk_prod_***", bearerToken: "your-bearer-token", chain: "STARKNET", // Currently only STARKNET is supported walletType: "READY", // Explicitly use Argent X Account }; createWallet(params) .then(response => console.log("Wallet created:", response)) .catch(error => console.error("Error:", error)); ``` **Backward Compatibility**: If you don't specify `walletType`, the SDK will create a ChipiWallet by default. Existing integrations will continue to work without changes. # callAnyContract Source: https://docs.chipipay.com/sdk/backend/methods/call-any-contract Executes any StarkNet contract method. Handles all contract interactions not covered by specific methods, giving you full flexibility to interact with any smart contract. ## Usage ```typescript theme={null} const result = await serverClient.callAnyContract({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data" }, calls: [ { contractAddress: "0x...", // Target contract address entrypoint: "methodName", calldata: ["param1", "param2"] } ], }, }); ``` ### Parameters * `encryptKey` (string): User's decryption PIN * `wallet` (WalletData): Wallet credentials with publicKey and encryptedPrivateKey * `calls` (array): Array of contract calls to execute * `contractAddress` (string): Target contract address * `entrypoint` (string): Contract method name * `calldata` (any\[]): Arguments for the contract method ### Return Value Returns a Promise that resolves to a transaction hash string. ## Example Implementation ```typescript theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; const serverClient = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); async function executeContractCall( wallet: { publicKey: string; encryptedPrivateKey: string }, contractAddress: string, methodName: string, parameters: any[], userPin: string ) { try { const result = await serverClient.callAnyContract({ params: { encryptKey: userPin, wallet: wallet, calls: [ { contractAddress: contractAddress, entrypoint: methodName, calldata: parameters, } ], }, }); console.log('Contract call successful!'); console.log('Transaction hash:', result); return result; } catch (error) { console.error('Contract call failed:', error); throw error; } } // Usage example - Approve tokens for spending async function approveTokenSpending( userId: string, tokenContract: string, spenderAddress: string, amount: string, pin: string ) { // Get user's wallet const wallet = await serverClient.getWallet({ externalUserId: userId, }); // Call the approve method on the token contract const txHash = await executeContractCall( wallet, tokenContract, "approve", [spenderAddress, amount, "0"], // StarkNet uses felt252 for amounts pin ); console.log(`Approved ${amount} tokens for ${spenderAddress}`); return txHash; } ``` # createWallet Source: https://docs.chipipay.com/sdk/backend/methods/create-wallet Creates a new wallet on StarkNet. Deploys the wallet contract and sponsors gas fees via the Chipi paymaster, resulting in a frictionless onboarding experience. **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. ## Usage ```typescript theme={null} // Default CHIPI wallet (session keys, passkey support) const newWallet = await serverClient.createWallet({ params: { encryptKey: "user-secure-pin", externalUserId: "your-user-id-123", chain: Chain.STARKNET, }, }); // READY wallet (Argent X compatible, no session keys) const readyWallet = await serverClient.createWallet({ params: { encryptKey: "user-secure-pin", externalUserId: "your-user-id-456", chain: Chain.STARKNET, walletType: "READY", }, }); // Custom account implementation (advanced) const customWallet = await serverClient.createWallet({ params: { encryptKey: "user-secure-pin", externalUserId: "your-user-id-789", chain: Chain.STARKNET, classHash: "0x0484bbd2404b3c7264bea271f7267d6d4004821ac7787a9eed7f472e79ef40d1", }, }); ``` ### Parameters | Parameter | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `encryptKey` | string | Yes | User-defined code or password to encrypt the wallet's private key | | `externalUserId` | string | Yes | Your application's unique identifier for the user | | `chain` | `Chain` | Yes | Blockchain network. Use `Chain.STARKNET` | | `walletType` | string | No | `"CHIPI"` (default) or `"READY"`. CHIPI supports session keys and passkeys. READY is Argent X compatible. | | `classHash` | string | No | Custom StarkNet class hash for the wallet contract. Overrides the default for the wallet type. Must be declared on mainnet and implement SNIP-9. See [Custom Wallet Types](/sdk/guides/custom-wallet-types). | | `usePasskey` | boolean | No | Use WebAuthn passkey for encryption instead of PIN | ### Return Value Returns a Promise that resolves to an object containing: * `publicKey`: The wallet's StarkNet address * `encryptedPrivateKey`: The encrypted private key (store securely) * `walletType`: The wallet type that was created * `classHash`: The class hash used for deployment ## Example Implementation ```typescript theme={null} import { ChipiServerSDK, Chain } from "@chipi-stack/backend"; const serverClient = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); async function createUserWallet(userId: string, userPin: string) { const newWallet = await serverClient.createWallet({ params: { encryptKey: userPin, externalUserId: userId, chain: Chain.STARKNET, }, }); console.log("Wallet created:", newWallet.publicKey); // Store wallet data in your database await saveWalletToDatabase({ userId, publicKey: newWallet.publicKey, encryptedPrivateKey: newWallet.encryptedPrivateKey, }); return newWallet; } ``` Wallet creation is free! Gas fees are sponsored by the Chipi paymaster for all wallet types. ## Wallet Types | Type | Account | Session Keys | Passkeys | Use Case | | ------- | --------------------- | ------------ | -------- | -------------------------------- | | `CHIPI` | OpenZeppelin + SNIP-9 | Yes | Yes | Default. Best for most apps. | | `READY` | Argent X v0.4.0 | No | No | Argent X ecosystem compatibility | | Custom | Any SNIP-9 account | Depends | Depends | Advanced: bring your own account | ## Related Methods * [getWallet](/sdk/backend/methods/get-wallet) - Retrieve existing wallet information * [transfer](/sdk/backend/methods/transfer) - Send tokens from the created wallet * [prepareWalletUpgrade](/sdk/backend/methods/prepare-wallet-upgrade) - Upgrade wallet to a different type * [Custom Wallet Types Guide](/sdk/guides/custom-wallet-types) - Using custom account implementations # executeWalletUpgrade Source: https://docs.chipipay.com/sdk/backend/methods/execute-wallet-upgrade Executes a wallet upgrade after the owner has signed the typed data from prepareWalletUpgrade. Server-only method. Requires API secret key. ## Usage ```typescript theme={null} const result = await sdk.executeWalletUpgrade( { walletAddress: "0x...", typedData: upgrade.typedData, signature: ["0xr", "0xs"], }, bearerToken, ); ``` ## Parameters | Parameter | Type | Required | Description | | --------------- | ----------- | -------- | ------------------------------------------------------- | | `walletAddress` | `string` | Yes | Wallet address being upgraded | | `typedData` | `TypedData` | Yes | SNIP-12 typed data from `prepareWalletUpgrade` | | `signature` | `string[]` | Yes | User signature over the typed data | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise`: | Field | Type | Description | | ----------------- | ------------ | -------------------------------- | | `transactionHash` | `string` | Transaction hash for the upgrade | | `newClassHash` | `string` | New class hash after upgrade | | `newWalletType` | `WalletType` | New wallet type after upgrade | ## Example See [`prepareWalletUpgrade`](/sdk/backend/methods/prepare-wallet-upgrade) for a complete upgrade flow. # getTokenBalance Source: https://docs.chipipay.com/sdk/backend/methods/get-token-balance Fetches the on-chain token balance for a wallet on Starknet. ## Usage ```typescript theme={null} const balance = await sdk.getTokenBalance( { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey: "0x...", }, bearerToken, ); ``` ## Parameters | Parameter | Type | Required | Description | | ----------------- | ------------ | -------- | ------------------------------------------------------- | | `chainToken` | `ChainToken` | Yes | Token identifier (e.g. `"USDC"`) | | `chain` | `Chain` | Yes | Blockchain network. Use `Chain.STARKNET` | | `walletPublicKey` | `string` | No | Wallet address. Use this or `externalUserId` | | `externalUserId` | `string` | No | External user ID. Use this or `walletPublicKey` | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise` with the balance data. ## Example ```typescript theme={null} import { ChipiServerSDK, Chain, ChainToken } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); const balance = await sdk.getTokenBalance( { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey: "0x04...abc", }, bearerToken, ); console.log("Balance:", balance); ``` # getTransaction Source: https://docs.chipipay.com/sdk/backend/methods/get-transaction Fetches a single transaction by its hash or internal ID. ## Usage ```typescript theme={null} const transaction = await sdk.getTransaction("0x...", bearerToken); ``` ## Parameters | Parameter | Type | Required | Description | | ------------- | -------- | -------- | ------------------------------------------------------- | | `hashOrId` | `string` | Yes | Transaction hash (`0x...`) or internal database ID | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise` with the full transaction record including `id`, `transactionHash`, `status`, `senderAddress`, `destinationAddress`, `amount`, `token`, and timestamps. ## Example ```typescript theme={null} // Fetch by transaction hash const tx = await sdk.getTransaction( "0x04abc...def", bearerToken, ); console.log(tx.status, tx.amount, tx.token); // Fetch by internal ID const tx2 = await sdk.getTransaction("tx-123", bearerToken); ``` # getTransactionList Source: https://docs.chipipay.com/sdk/backend/methods/get-transaction-list Fetches a paginated list of transactions with optional filtering. ## Usage ```typescript theme={null} const result = await sdk.getTransactionList( { page: 1, limit: 10, walletAddress: "0x...", }, bearerToken, ); ``` ## Parameters | Parameter | Type | Required | Description | | ---------------- | -------- | -------- | ------------------------------------------------------- | | `page` | `number` | No | Page number (default: `1`) | | `limit` | `number` | No | Results per page (default: `10`) | | `walletAddress` | `string` | No | Filter by wallet address | | `calledFunction` | `string` | No | Filter by contract function name | | `day` | `number` | No | Filter by day (1–31) | | `month` | `number` | No | Filter by month (1–12) | | `year` | `number` | No | Filter by year | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise>` with `items`, `total`, `page`, and `limit`. ## Example ```typescript theme={null} const result = await sdk.getTransactionList( { page: 1, limit: 20, walletAddress: "0x04...abc", month: 2, year: 2026, }, bearerToken, ); console.log(`Found ${result.total} transactions`); result.items.forEach((tx) => { console.log(tx.transactionHash, tx.status); }); ``` # getTransactionStatus Source: https://docs.chipipay.com/sdk/backend/methods/get-transaction-status Fetches the on-chain status of a transaction from StarkNet. Useful for polling until confirmation. ## Usage ```typescript theme={null} const status = await sdk.getTransactionStatus("0x...", bearerToken); ``` ## Parameters | Parameter | Type | Required | Description | | ------------- | -------- | -------- | ------------------------------------------------------- | | `hash` | `string` | Yes | Transaction hash (`0x...`) | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise`: | Field | Type | Description | | ----------------- | --------------------- | ----------------------------------- | | `transactionHash` | `string` | Transaction hash | | `status` | `OnChainTxStatus` | Current on-chain status (see below) | | `blockNumber` | `number \| undefined` | Block number if included in a block | | `revertReason` | `string \| undefined` | Reason if status is `REVERTED` | ### OnChainTxStatus Values | Status | Description | Terminal | | ---------------- | ---------------------------- | -------- | | `RECEIVED` | Transaction received by node | No | | `PENDING` | Waiting for inclusion | No | | `ACCEPTED_ON_L2` | Confirmed on StarkNet L2 | No | | `ACCEPTED_ON_L1` | Proven on Ethereum L1 | Yes | | `REJECTED` | Rejected by sequencer | Yes | | `REVERTED` | Execution reverted on-chain | Yes | | `NOT_RECEIVED` | Not found on network | Yes | ## Example ```typescript theme={null} // Poll for confirmation const poll = async (hash: string) => { const terminalStatuses = ["ACCEPTED_ON_L1", "REJECTED", "REVERTED", "NOT_RECEIVED"]; const MAX_RETRIES = 60; // 60 retries * 3s = 3 minutes max for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { const { status, revertReason } = await sdk.getTransactionStatus(hash, bearerToken); if (terminalStatuses.includes(status)) { if (status === "REVERTED") console.error("Reverted:", revertReason); return status; } await new Promise((r) => setTimeout(r, 3000)); } throw new Error(`Transaction ${hash} did not reach terminal status after ${MAX_RETRIES} attempts`); }; ``` # getUsdAmount Source: https://docs.chipipay.com/sdk/backend/methods/get-usd-amount Converts a currency amount to its USD equivalent using current exchange rates. ## Usage ```typescript theme={null} const usdAmount = await sdk.getUsdAmount(100, "MXN", bearerToken); ``` ## Parameters | Parameter | Type | Required | Description | | ---------------- | ---------- | -------- | ------------------------------------------------------- | | `currencyAmount` | `number` | Yes | The amount in the source currency | | `currency` | `Currency` | Yes | The source currency code (e.g. `"MXN"`) | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise` — the equivalent amount in USD. ## Example ```typescript theme={null} const usdAmount = await sdk.getUsdAmount(500, "MXN", bearerToken); console.log(`500 MXN = $${usdAmount} USD`); ``` Exchange rates are fetched in real-time. Results may vary between calls. # getWallet Source: https://docs.chipipay.com/sdk/backend/methods/get-wallet Retrieves wallet information for an existing user by their external user ID. This method allows you to fetch wallet details for users who have already created wallets. ## Usage ```typescript theme={null} const wallet = await serverClient.getWallet({ externalUserId: "your-user-id-123", }); ``` ### Parameters * `externalUserId` (string): Your application's unique identifier for the user ### Return Value Returns a Promise that resolves to an object containing: * `publicKey` (string): The wallet's public address on StarkNet * `encryptedPrivateKey` (string): The encrypted private key for the wallet ## Example Implementation ```typescript theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; const serverClient = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); async function getUserWallet(userId: string) { try { const wallet = await serverClient.getWallet({ externalUserId: userId, }); console.log('Wallet found:'); console.log('Address:', wallet.publicKey); return wallet; } catch (error) { if (error.message.includes('not found')) { console.log('No wallet found for user:', userId); return null; } console.error('Error retrieving wallet:', error); throw error; } } // Usage example async function checkUserWallet(userId: string) { const wallet = await getUserWallet(userId); if (wallet) { console.log(`User ${userId} has wallet: ${wallet.publicKey}`); return wallet; } else { console.log(`User ${userId} needs to create a wallet`); return null; } } ``` ## Security Considerations **Important Security Notes:** * Never expose encrypted private keys in API responses * Validate user IDs before making requests * Implement proper access controls in your application * Use HTTPS for all communications * Log wallet access for audit purposes Chipi never stores your decrypted sensitive data. All private keys remain encrypted and are never accessible to Chipi servers. ## Related Methods * [createWallet](/sdk/backend/methods/create-wallet) - Create a new wallet for users who don't have one * [transfer](/sdk/backend/methods/transfer) - Send tokens from the retrieved wallet # prepareWalletUpgrade Source: https://docs.chipipay.com/sdk/backend/methods/prepare-wallet-upgrade Prepares a wallet upgrade (e.g., READY to CHIPI or CHIPI v29 to v33). Returns typed data that must be signed by the wallet owner. Server-only method. Requires API secret key. ## Usage ```typescript theme={null} const upgrade = await sdk.prepareWalletUpgrade( { walletAddress: "0x..." }, bearerToken, ); ``` ## Parameters | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | ------------------------------------------------------- | | `walletAddress` | `string` | Yes | Wallet address to upgrade | | `targetClassHash` | `string` | No | Target class hash (defaults to latest CHIPI) | | `bearerToken` | `string` | No | JWT token. Falls back to `apiSecretKey` if not provided | ## Return Value Returns a `Promise`: | Field | Type | Description | | ------------------- | ------------ | -------------------------------- | | `typedData` | `TypedData` | SNIP-12 typed data to sign | | `currentClassHash` | `string` | Current class hash of the wallet | | `targetClassHash` | `string` | Target class hash for upgrade | | `currentWalletType` | `WalletType` | Current wallet type | | `targetWalletType` | `WalletType` | Target wallet type after upgrade | ## Example ```typescript theme={null} // Prepare upgrade from READY to CHIPI const upgrade = await sdk.prepareWalletUpgrade( { walletAddress: "0x04abc...def" }, bearerToken, ); console.log(upgrade.currentWalletType); // "READY" console.log(upgrade.targetWalletType); // "CHIPI" // Sign the typed data (using starknet.js Account) const signature = await account.signMessage(upgrade.typedData); // Execute the upgrade const result = await sdk.executeWalletUpgrade( { walletAddress: "0x04abc...def", typedData: upgrade.typedData, signature: [signature.r.toString(), signature.s.toString()], }, bearerToken, ); ``` # Session Key Methods Source: https://docs.chipipay.com/sdk/backend/methods/sessions API reference for session key management - create, register, execute, query, and revoke delegated access **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. Session keys enable temporary, delegated transaction execution for [CHIPI wallets](https://github.com/chipi-pay/sessions-smart-contract). This page documents all available session methods. *** ## sessions.createSessionKey() Generate a session keypair locally. The SDK returns the data but does NOT store it. ### Usage ```typescript theme={null} const session = sdk.sessions.createSessionKey({ encryptKey: "user-secure-pin", durationSeconds: 3600, // Optional, default: 21600 (6 hours) }); ``` ### Parameters | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------------------- | | `encryptKey` | string | Yes | PIN to encrypt the session private key | | `durationSeconds` | number | No | Session duration in seconds. Default: 21600 (6 hours) | ### Return Value Returns a `SessionKeyData` object: ```typescript theme={null} { publicKey: string; // Stark public key (hex format) encryptedPrivateKey: string; // AES encrypted private key validUntil: number; // Unix timestamp (seconds) } ``` ### Example ```typescript theme={null} // Create a 1-hour gaming session const session = sdk.sessions.createSessionKey({ encryptKey: userPin, durationSeconds: 60 * 60, // 1 hour }); // Store in client-side secure storage (NOT your database!) localStorage.setItem('chipiSession', JSON.stringify(session)); console.log(`Session expires at: ${new Date(session.validUntil * 1000)}`); ``` Store session data only in client-side secure storage (localStorage, Secure Enclave, Keychain). Never in your backend database. *** ## sessions.addSessionKeyToContract() Register a session key on the [CHIPI wallet](https://github.com/chipi-pay/sessions-smart-contract) smart contract. Requires owner signature. ### Usage ```typescript theme={null} const txHash = await sdk.sessions.addSessionKeyToContract({ encryptKey: "user-secure-pin", wallet: { publicKey: "0x...", encryptedPrivateKey: "...", walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, allowedEntrypoints: [], }, }, bearerToken); ``` ### Parameters | Parameter | Type | Required | Description | | ---------------------------------- | ---------- | -------- | --------------------------------------------- | | `encryptKey` | string | Yes | PIN to decrypt owner's private key | | `wallet` | WalletData | Yes | User's wallet data with `walletType: "CHIPI"` | | `sessionConfig.sessionPublicKey` | string | Yes | Public key from `createSessionKey()` | | `sessionConfig.validUntil` | number | Yes | Unix timestamp when session expires | | `sessionConfig.maxCalls` | number | Yes | Maximum transactions allowed | | `sessionConfig.allowedEntrypoints` | string\[] | Yes | Whitelisted selectors. Empty = all allowed | | `bearerToken` | string | Yes | Valid JWT from your auth provider | ### Return Value Returns a `Promise` with the transaction hash. ### Example ```typescript theme={null} // Register session with restricted permissions const txHash = await sdk.sessions.addSessionKeyToContract({ encryptKey: userPin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 50, allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer ], }, }, bearerToken); console.log(`Session registered: https://starkscan.co/tx/${txHash}`); ``` This uses gasless transactions via the Chipi paymaster. Users don't need STRK for gas. *** ## executeTransactionWithSession() Execute a transaction using a session key instead of the owner's private key. ### Usage ```typescript theme={null} const txHash = await sdk.executeTransactionWithSession({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: "0x...", encryptedPrivateKey: "...", walletType: "CHIPI", }, session: { publicKey: "0x...", encryptedPrivateKey: "...", validUntil: 1702500000, }, calls: [ { contractAddress: "0x...", entrypoint: "transfer", calldata: ["0x...", "100", "0x0"], }, ], }, bearerToken, }); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------- | -------------- | -------- | -------------------------------------------- | | `params.encryptKey` | string | Yes | PIN to decrypt session private key | | `params.wallet` | WalletData | Yes | User's wallet data | | `params.session` | SessionKeyData | Yes | Session from `createSessionKey()` | | `params.calls` | Call\[] | Yes | Array of contract calls to execute | | `bearerToken` | string | No | JWT token (uses SDK default if not provided) | ### Return Value Returns a `Promise` with the transaction hash. ### Example ```typescript theme={null} // Execute transfer using session (no owner key needed!) const txHash = await sdk.executeTransactionWithSession({ params: { encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, session, calls: [ { contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }, ], }, bearerToken, }); console.log(`Transfer executed: https://starkscan.co/tx/${txHash}`); ``` **Signature Format:** Session transactions use a 4-element signature `[sessionPubKey, r, s, validUntil]` instead of the owner's 2-element `[r, s]`. The SDK handles this automatically. *** ## sessions.revokeSessionKey() Remove a session key's authorization from the [CHIPI wallet](https://github.com/chipi-pay/sessions-smart-contract) contract. ### Usage ```typescript theme={null} const txHash = await sdk.sessions.revokeSessionKey({ encryptKey: "user-secure-pin", wallet: { publicKey: "0x...", encryptedPrivateKey: "...", walletType: "CHIPI", }, sessionPublicKey: session.publicKey, }, bearerToken); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------ | ---------- | -------- | --------------------------------------------- | | `encryptKey` | string | Yes | PIN to decrypt owner's private key | | `wallet` | WalletData | Yes | User's wallet data with `walletType: "CHIPI"` | | `sessionPublicKey` | string | Yes | Public key of session to revoke | | `bearerToken` | string | Yes | Valid JWT from your auth provider | ### Return Value Returns a `Promise` with the transaction hash. ### Example ```typescript theme={null} // Revoke session on logout const handleLogout = async () => { const sessionStr = localStorage.getItem('chipiSession'); if (sessionStr) { const session = JSON.parse(sessionStr); await sdk.sessions.revokeSessionKey({ encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, sessionPublicKey: session.publicKey, }, bearerToken); localStorage.removeItem('chipiSession'); } await authProvider.signOut(); }; ``` Always revoke sessions on logout to prevent unauthorized access. *** ## sessions.getSessionData() Query the on-chain status of a session key. ### Usage ```typescript theme={null} const sessionData = await sdk.sessions.getSessionData({ walletAddress: "0x...", sessionPublicKey: "0x...", }); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | -------------------------------------------------------------------------------- | | `walletAddress` | string | Yes | The [CHIPI wallet](https://github.com/chipi-pay/sessions-smart-contract) address | | `sessionPublicKey` | string | Yes | Public key of the session to query | ### Return Value Returns a `Promise`: ```typescript theme={null} { isActive: boolean; // true if session is valid and not revoked validUntil: number; // Unix timestamp of expiration remainingCalls: number; // Transactions remaining before limit allowedEntrypoints: string[]; // Whitelisted function selectors } ``` ### Example ```typescript theme={null} // Check session before executing const sessionData = await sdk.sessions.getSessionData({ walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, }); if (!sessionData.isActive) { console.log("Session expired or revoked - need to create new session"); return; } if (sessionData.remainingCalls < 5) { console.log(`Warning: Only ${sessionData.remainingCalls} calls remaining`); } // Safe to proceed with transaction await sdk.executeTransactionWithSession({ ... }); ``` Use `getSessionData()` to check remaining calls before batch operations. *** ## Error Handling All session methods throw `ChipiSessionError` for session-specific failures: ```typescript theme={null} import { ChipiSessionError } from "@chipi-stack/backend"; try { await sdk.sessions.addSessionKeyToContract({ ... }, bearerToken); } catch (error) { if (error instanceof ChipiSessionError) { switch (error.code) { case "INVALID_WALLET_TYPE_FOR_SESSION": console.error("Session keys only work with CHIPI wallets"); break; case "SESSION_EXPIRED": console.error("Session has expired"); break; case "SESSION_DECRYPTION_FAILED": console.error("Invalid PIN - could not decrypt session key"); break; default: console.error("Session error:", error.message); } } throw error; } ``` ### Error Codes | Code | Description | | --------------------------------- | ------------------------------------------------------------------------------------ | | `INVALID_WALLET_TYPE_FOR_SESSION` | Wallet is not a [CHIPI wallet](https://github.com/chipi-pay/sessions-smart-contract) | | `SESSION_EXPIRED` | Session `validUntil` has passed | | `SESSION_NOT_REGISTERED` | Session not found on contract | | `SESSION_REVOKED` | Session was explicitly revoked | | `SESSION_MAX_CALLS_EXCEEDED` | No remaining calls | | `SESSION_ENTRYPOINT_NOT_ALLOWED` | Called function not in whitelist | | `SESSION_DECRYPTION_FAILED` | Invalid PIN for decryption | | `SESSION_CREATION_FAILED` | Failed to generate keypair | *** ## Related * [Session Keys Quickstart](/sdk/backend/sessions-quickstart) - Full lifecycle guide * [CHIPI Wallet Smart Contract](https://github.com/chipi-pay/sessions-smart-contract) - Cairo implementation * [Transfer](/sdk/backend/methods/transfer) - Basic token transfers * [Call Any Contract](/sdk/backend/methods/call-any-contract) - Custom contract interactions # Spending Policy Methods Source: https://docs.chipipay.com/sdk/backend/methods/spending-policies API reference for spending policy management — set, query, and remove per-token spending caps on session keys Spending policies enforce per-token limits on ERC-20 operations executed by session keys. The CHIPI wallet contract enforces these automatically during transaction execution. Requires **CHIPI v33** wallets. See the [Spending Policies guide](/sdk/guides/spending-policies) for concepts and use cases. *** **SHHH wallets supported (14.10.0):** `setSpendingPolicy` / `removeSpendingPolicy` now accept `walletType: "SHHH"` (V8.4) in addition to CHIPI — both embed the same SNIP-163 session-key + spending-policy component. `undefined` still defaults to CHIPI; READY remains unsupported (no sessions component). Proven on mainnet: in-cap approve succeeds, over-cap reverts with `'Spending: exceeds per-call'`. ## sessions.setSpendingPolicy() Set a spending policy for a session key + token pair. Requires owner signature. ### Usage ```typescript theme={null} const txHash = await sdk.sessions.setSpendingPolicy({ encryptKey: "user-secure-pin", wallet: userWallet, spendingPolicyConfig: { sessionPublicKey: session.publicKey, token: "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb", maxPerCall: 1_000_000n, maxPerWindow: 50_000_000n, windowSeconds: 86400, }, }, bearerToken); ``` ### Parameters | Parameter | Type | Required | Description | | --------------------------------------- | ---------- | -------- | ---------------------------------------------- | | `encryptKey` | string | Yes | PIN to decrypt owner's private key | | `wallet` | WalletData | Yes | Wallet data with encrypted private key | | `spendingPolicyConfig.sessionPublicKey` | string | Yes | Session key to apply the policy to | | `spendingPolicyConfig.token` | string | Yes | ERC-20 token contract address | | `spendingPolicyConfig.maxPerCall` | bigint | Yes | Max amount per single call (u256) | | `spendingPolicyConfig.maxPerWindow` | bigint | Yes | Max cumulative amount in rolling window (u256) | | `spendingPolicyConfig.windowSeconds` | number | Yes | Rolling window duration in seconds (u64) | ### Return Value `Promise` — Transaction hash of the on-chain `set_spending_policy` call. *** ## sessions.getSpendingPolicy() Query a spending policy from the contract. Read-only, no signature or gas required. ### Usage ```typescript theme={null} const policy = await sdk.sessions.getSpendingPolicy({ walletAddress: "0x04abc...def", sessionPublicKey: session.publicKey, token: "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb", }); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | ----------------------------- | | `walletAddress` | string | Yes | Wallet contract address | | `sessionPublicKey` | string | Yes | Session public key | | `token` | string | Yes | ERC-20 token contract address | ### Return Value `SpendingPolicyData`: | Field | Type | Description | | --------------- | ------ | ---------------------------------------------- | | `maxPerCall` | bigint | Maximum amount per single call | | `maxPerWindow` | bigint | Maximum cumulative amount in rolling window | | `windowSeconds` | number | Rolling window duration in seconds | | `spentInWindow` | bigint | Amount spent in the current active window | | `windowStart` | number | Unix timestamp when the current window started | Returns all zeros if no policy is set (meaning no enforcement). *** ## sessions.removeSpendingPolicy() Remove a spending policy. After removal, the session has no limits for this token. Requires owner signature. ### Usage ```typescript theme={null} const txHash = await sdk.sessions.removeSpendingPolicy({ encryptKey: "user-secure-pin", wallet: userWallet, sessionPublicKey: session.publicKey, token: "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb", }, bearerToken); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------ | ---------- | -------- | -------------------------------------- | | `encryptKey` | string | Yes | PIN to decrypt owner's private key | | `wallet` | WalletData | Yes | Wallet data with encrypted private key | | `sessionPublicKey` | string | Yes | Session key to remove policy from | | `token` | string | Yes | ERC-20 token contract address | ### Return Value `Promise` — Transaction hash of the on-chain `remove_spending_policy` call. # transfer Source: https://docs.chipipay.com/sdk/backend/methods/transfer Transfers tokens from a user wallet to another address. Uses Avnus gasless to cover gas fees, making transactions frictionless for your users. ## Usage ```typescript theme={null} const transferResult = await serverClient.transfer({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data" }, amount: "100", token: ChainToken.USDC, recipient: "0x1234567890abcdef...", }, }); ``` ### Parameters * `encryptKey` (string): PIN used to decrypt the private key * `wallet` (WalletData): Object with publicKey and encryptedPrivateKey * `amount` (string | number): Transfer amount * `token` (`ChainToken`): Token identifier. Use `ChainToken.USDC` * `recipient` (string): Destination wallet address * `decimals` (number, optional): Token decimals (default: 18) ### Return Value Returns a Promise that resolves to a transaction hash string. ## Example Implementation ```typescript theme={null} import { ChipiServerSDK, ChainToken } from "@chipi-stack/backend"; const serverClient = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); async function transferTokens( senderWallet: { publicKey: string; encryptedPrivateKey: string }, recipientAddress: string, amount: string, userPin: string ) { try { const transferResult = await serverClient.transfer({ params: { encryptKey: userPin, wallet: senderWallet, amount: amount, token: ChainToken.USDC, recipient: recipientAddress, }, }); console.log('Transfer completed successfully!'); console.log('Transaction hash:', transferResult); return transferResult; } catch (error) { console.error('Transfer failed:', error); throw error; } } // Usage example async function sendUSDC(userId: string, recipientAddress: string, amount: string, pin: string) { // First, get the user's wallet const wallet = await serverClient.getWallet({ externalUserId: userId, }); // Then perform the transfer const txHash = await transferTokens( wallet, recipientAddress, amount, pin ); console.log(`Sent ${amount} USDC to ${recipientAddress}`); console.log(`Transaction: https://starkscan.co/tx/${txHash}`); return txHash; } ``` Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Methods * [getWallet](/sdk/backend/methods/get-wallet) - Get wallet information before transfers * [callAnyContract](/sdk/backend/methods/call-any-contract) - For custom contract interactions # Backend SDK Quickstart Source: https://docs.chipipay.com/sdk/backend/quickstart Quick setup guide for the Chipi Backend SDK - server-side wallet management and transactions Since this is a server-side SDK, the wallets are not self-custodial. **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. ```bash npm theme={null} npm install @chipi-stack/backend ``` ```bash yarn theme={null} yarn add @chipi-stack/backend ``` ```bash pnpm theme={null} pnpm add @chipi-stack/backend ``` 1. Go to your [API Keys](https://dashboard.chipipay.com/configure/api-keys) in the Chipi Dashboard 2. Copy your **Public Key** (`pk_prod_xxxx`) and **Secret Key** (`sk_prod_xxxx`) Keep your Secret Key secure and never expose it in client-side code or version control. Create a new instance of the ChipiServerSDK with your API keys: ```typescript theme={null} import { ChipiServerSDK, Chain, ChainToken } from "@chipi-stack/backend"; const serverClient = new ChipiServerSDK({ apiPublicKey: "pk_prod_your_public_key", apiSecretKey: "sk_prod_your_secret_key", }); ``` Now you can create a wallet for your users: ```typescript theme={null} const newWallet = await serverClient.createWallet({ params: { encryptKey: "user-secure-pin", externalUserId: "your-user-id-123", chain: Chain.STARKNET, }, }); console.log('New wallet created:', newWallet); // Output: { publicKey: "0x...", encryptedPrivateKey: "...", walletType: "CHIPI", ... } ``` Transfer tokens between wallets: ```typescript theme={null} const transferResult = await serverClient.transfer({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: newWallet.publicKey, encryptedPrivateKey: newWallet.encryptedPrivateKey, }, amount: "100", token: ChainToken.USDC, recipient: "0x1234567890abcdef...", }, }); console.log('Transfer completed:', transferResult); ``` For production applications, store your API keys as environment variables: ```bash theme={null} # .env CHIPI_PUBLIC_KEY=pk_prod_your_public_key CHIPI_SECRET_KEY=sk_prod_your_secret_key ``` Then initialize the SDK: ```typescript theme={null} const serverClient = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); ``` ## Next Steps Now that you have the basic setup working, explore more advanced features: * **[Session Keys](/sdk/backend/sessions-quickstart)** - Enable delegated transaction execution * **[API Reference](/sdk/api/introduction)** - Full API documentation ## Security Best Practices * Never expose your secret API key in client-side code * Use environment variables for API keys in production * Validate user inputs before making API calls * Implement proper error handling and logging Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Session Keys Quickstart Source: https://docs.chipipay.com/sdk/backend/sessions-quickstart Enable temporary, delegated transaction execution without requiring the owner's private key for each operation **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. ## What are Session Keys? Session keys allow your application to execute transactions on behalf of a user without requiring their owner private key for every action. Instead, a temporary keypair is created and registered on-chain with specific permissions (time limit, call limit, allowed functions). This enables **gasless, frictionless UX** for gaming, DeFi automation, social apps, and more. Session keys only work with [CHIPI wallets](https://github.com/chipi-pay/sessions-smart-contract) - OpenZeppelin accounts extended with SNIP-9 session key support. ## When to Use Session Keys | Use Case | Why Sessions Help | | ------------------- | ------------------------------------------------- | | **Gaming** | Players make moves without wallet popups | | **DeFi Automation** | Yield optimizers execute strategies automatically | | **Social dApps** | Post, like, comment without friction | | **Subscriptions** | Recurring payments without manual approval | | **Airdrops** | Zero-friction claiming for users | ## Requirements * A [CHIPI wallet](https://github.com/chipi-pay/sessions-smart-contract) (not READY) * Valid bearer token from your auth provider * User's `encryptKey` (PIN) for signing *** ```bash npm theme={null} npm install @chipi-stack/backend ``` ```bash yarn theme={null} yarn add @chipi-stack/backend ``` ```bash pnpm theme={null} pnpm add @chipi-stack/backend ``` ```typescript theme={null} import { ChipiSDK } from "@chipi-stack/backend"; const sdk = new ChipiSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); ``` Generate a session keypair locally. The SDK returns the data but does **NOT** store it - you must persist it on the client side. ```typescript theme={null} // Create session key (self-custodial - store in client localStorage) const session = sdk.sessions.createSessionKey({ encryptKey: "user-secure-pin", durationSeconds: 3600, // 1 hour (default: 6 hours) }); console.log(session); // { // publicKey: "0x...", // Session public key // encryptedPrivateKey: "...", // AES encrypted with encryptKey // validUntil: 1702500000 // Unix timestamp (seconds) // } // Store in client-side secure storage (NOT your database!) localStorage.setItem('chipiSession', JSON.stringify(session)); ``` Never store session keys in your backend database. Store only in client-side secure storage (localStorage, Secure Enclave, Keychain). Before using a session key, you must register it on the [CHIPI wallet](https://github.com/chipi-pay/sessions-smart-contract) smart contract. This requires the owner's signature (one-time setup). ```typescript theme={null} // Get user's wallet const wallet = await sdk.getWallet({ externalUserId: "user-123" }); // Register session on contract (uses owner signature via paymaster) const txHash = await sdk.sessions.addSessionKeyToContract({ encryptKey: "user-secure-pin", wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, // Max transactions allowed allowedEntrypoints: [], // Empty = all functions allowed }, }, bearerToken); console.log("Session registered:", txHash); ``` ### Session Configuration Options | Parameter | Type | Description | | -------------------- | --------- | ------------------------------------------------------- | | `sessionPublicKey` | string | Public key from `createSessionKey()` | | `validUntil` | number | Unix timestamp when session expires | | `maxCalls` | number | Maximum transactions allowed (decrements with each use) | | `allowedEntrypoints` | string\[] | Whitelisted function selectors. Empty = all allowed | For security, whitelist specific entrypoints when possible: ```typescript theme={null} allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve ] ``` Now you can execute transactions using the session key - **no owner private key needed!** ```typescript theme={null} // Retrieve session from client storage const session = JSON.parse(localStorage.getItem('chipiSession')!); // Execute transaction with session signature (4-element format) const txHash = await sdk.executeTransactionWithSession({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, session, calls: [ { contractAddress: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }, ], }, bearerToken, }); console.log("Transaction executed:", txHash); ``` **Signature Format Difference:** * Owner signature: `[r, s]` (2 elements) * Session signature: `[sessionPubKey, r, s, validUntil]` (4 elements) The SDK handles this automatically. Check if a session is still active and how many calls remain: ```typescript theme={null} const sessionData = await sdk.sessions.getSessionData({ walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, }); console.log(sessionData); // { // isActive: true, // validUntil: 1702500000, // remainingCalls: 95, // allowedEntrypoints: [] // } if (sessionData.isActive) { console.log(`Session has ${sessionData.remainingCalls} calls remaining`); } ``` Always revoke sessions when no longer needed or when the user logs out: ```typescript theme={null} const revokeTxHash = await sdk.sessions.revokeSessionKey({ encryptKey: "user-secure-pin", wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionPublicKey: session.publicKey, }, bearerToken); // Clear from client storage localStorage.removeItem('chipiSession'); console.log("Session revoked:", revokeTxHash); ``` Always revoke sessions on logout to prevent unauthorized access. *** ## Security and Best Practices ### Storage Guidelines **Never store session keys in your backend database.** This defeats the purpose of self-custodial security. Store session keys only in client-side secure storage: | Platform | Recommended Storage | | ---------------- | ---------------------------------------------- | | **Web** | `localStorage` or `sessionStorage` | | **iOS** | Secure Enclave / Keychain | | **Android** | Android Keystore | | **React Native** | `expo-secure-store` or `react-native-keychain` | Use `sessionStorage` instead of `localStorage` for sessions that should expire when the browser tab closes. ### Lifecycle Best Practices 1. **Always revoke on logout** - Never leave active sessions when user signs out 2. **Set short durations** - Use 1-6 hours, not days 3. **Limit `maxCalls`** - Set realistic limits based on expected usage 4. **Whitelist entrypoints** - Restrict to only the functions your app needs ### Example: Secure Logout Handler ```typescript theme={null} const handleLogout = async () => { // Retrieve active session const sessionStr = localStorage.getItem('chipiSession'); if (sessionStr) { const session = JSON.parse(sessionStr); try { // Revoke session on-chain before logout await sdk.sessions.revokeSessionKey({ encryptKey: userPin, wallet, sessionPublicKey: session.publicKey, }, bearerToken); } catch (error) { console.warn("Failed to revoke session:", error); // Continue with logout even if revoke fails } // Clear from local storage localStorage.removeItem('chipiSession'); } // Proceed with auth logout await authProvider.signOut(); }; ``` ### Example: Session with Restricted Permissions ```typescript theme={null} // Create a session that can ONLY transfer USDC const restrictedSession = await sdk.sessions.addSessionKeyToContract({ encryptKey: userPin, wallet, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 10, // Only 10 transfers allowed allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer ], }, }, bearerToken); ``` *** ## Full Example: Gaming Session ```typescript theme={null} import { ChipiSDK } from "@chipi-stack/backend"; const sdk = new ChipiSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); // Game constants const GAME_CONTRACT = "0x..."; const MAKE_MOVE_SELECTOR = "0x..."; const CLAIM_REWARD_SELECTOR = "0x..."; async function startGameSession(userId: string, userPin: string, bearerToken: string) { // Get player's CHIPI wallet const wallet = await sdk.getWallet({ externalUserId: userId }); // Create 24-hour gaming session const session = sdk.sessions.createSessionKey({ encryptKey: userPin, durationSeconds: 24 * 60 * 60, // 24 hours }); // Register with game-only permissions await sdk.sessions.addSessionKeyToContract({ encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 1000, // Many moves allowed allowedEntrypoints: [MAKE_MOVE_SELECTOR, CLAIM_REWARD_SELECTOR], }, }, bearerToken); // Store session in client localStorage.setItem('gameSession', JSON.stringify(session)); return session; } async function makeMove(moveData: string, bearerToken: string) { const session = JSON.parse(localStorage.getItem('gameSession')!); const wallet = await sdk.getWallet({ externalUserId: currentUserId }); // Execute move instantly - no wallet popup! return sdk.executeTransactionWithSession({ params: { encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, session, calls: [{ contractAddress: GAME_CONTRACT, entrypoint: "make_move", calldata: [moveData], }], }, bearerToken, }); } ``` *** ## Next Steps * **[Session Methods Reference](/sdk/backend/methods/sessions)** - Detailed API for each session method * **[CHIPI Wallet Smart Contract](https://github.com/chipi-pay/sessions-smart-contract)** - Cairo implementation details * **[Transfer Tokens](/sdk/backend/methods/transfer)** - Basic token transfers Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support. # Amount Source: https://docs.chipipay.com/sdk/core/amount Immutable token amount class with safe BigInt arithmetic, parsing, and formatting. `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release. ## Overview `Amount` is an immutable value type that represents a token amount using `BigInt` internally. All arithmetic is exact with zero floating-point precision loss. ## Usage ```typescript theme={null} import { Amount } from "@chipi-stack/core"; const usdc = { symbol: "USDC", decimals: 6, address: "0x..." }; // Parse human-readable string const amount = Amount.parse("1.5", usdc); amount.toBase(); // 1_500_000n amount.toUnit(); // "1.5" amount.toFormatted(); // "1.5 USDC" ``` ## Factory Methods ### `Amount.parse(value, token)` Parse a human-readable string into an Amount. ```typescript theme={null} const amount = Amount.parse("100.50", { symbol: "USDC", decimals: 6, address: "0x..." }); ``` #### Parameters * `value` (string): Human-readable value (e.g., `"1.5"`, `"100"`) * `token` (TokenInfo): Object with `symbol`, `decimals`, and `address` #### Throws * If `value` is negative or non-numeric * If `value` has more decimal places than the token supports ### `Amount.fromRaw(raw, decimals, symbol?)` Create an Amount from a raw bigint value (base units). ```typescript theme={null} const amount = Amount.fromRaw(1_500_000n, 6, "USDC"); amount.toUnit(); // "1.5" ``` #### Parameters * `raw` (bigint): Raw value in base units (e.g., wei) * `decimals` (number): Token decimal places * `symbol` (string, optional): Token symbol, defaults to `""` #### Throws * If `raw` is negative ## Arithmetic All arithmetic methods return a new `Amount` (immutable). Both operands must have the same `decimals`. ```typescript theme={null} const a = Amount.parse("10.0", usdc); const b = Amount.parse("3.5", usdc); a.add(b); // 13.5 USDC a.subtract(b); // 6.5 USDC a.multiply(2n); // 20.0 USDC a.divide(4n); // 2.5 USDC (truncates, no rounding) ``` ### Methods | Method | Signature | Description | | ---------- | ---------------------------------- | --------------------------------------------- | | `add` | `add(other: Amount): Amount` | Add two amounts | | `subtract` | `subtract(other: Amount): Amount` | Subtract (throws if result would be negative) | | `multiply` | `multiply(scalar: bigint): Amount` | Multiply by a scalar | | `divide` | `divide(divisor: bigint): Amount` | Divide by a scalar (truncates) | ## Comparisons ```typescript theme={null} const a = Amount.parse("10.0", usdc); const b = Amount.parse("3.5", usdc); a.gt(b); // true a.lt(b); // false a.eq(b); // false a.gte(b); // true a.lte(b); // false a.isZero(); // false ``` ### Methods | Method | Signature | Description | | -------- | ----------------------------- | ----------------------- | | `eq` | `eq(other: Amount): boolean` | Equal | | `gt` | `gt(other: Amount): boolean` | Greater than | | `gte` | `gte(other: Amount): boolean` | Greater than or equal | | `lt` | `lt(other: Amount): boolean` | Less than | | `lte` | `lte(other: Amount): boolean` | Less than or equal | | `isZero` | `isZero(): boolean` | Check if amount is zero | ## Formatting ```typescript theme={null} const amount = Amount.parse("1500000.0", usdc); amount.toBase(); // 1_500_000_000_000n (raw bigint) amount.toUnit(); // "1500000.0" amount.toFormatted(); // "1500000.0 USDC" amount.toFormatted(true); // "1.5M USDC" (compressed) ``` ### Methods | Method | Signature | Description | | ------------- | ------------------------------------------- | ------------------------------------------------- | | `toBase` | `toBase(): bigint` | Raw value in base units | | `toUnit` | `toUnit(): string` | Human-readable string (e.g., `"1.5"`) | | `toFormatted` | `toFormatted(compressed?: boolean): string` | Formatted with symbol, optional compression (K/M) | ## Properties | Property | Type | Description | | ---------- | -------- | ---------------------------------- | | `raw` | `bigint` | Raw value in base units (readonly) | | `decimals` | `number` | Token decimal places (readonly) | | `symbol` | `string` | Token symbol (readonly) | ## Example ```typescript theme={null} import { Amount, TokenRegistry } from "@chipi-stack/core"; const registry = new TokenRegistry("mainnet"); const usdcInfo = registry.bySymbol("USDC")!; // Parse user input const sendAmount = Amount.parse("25.50", usdcInfo); // Get balance (returned by Erc20.balanceOf()) const balance = Amount.fromRaw(50_000_000n, 6, "USDC"); // Compare if (balance.lt(sendAmount)) { console.log(`Insufficient: have ${balance.toFormatted()}, need ${sendAmount.toFormatted()}`); } else { const remaining = balance.subtract(sendAmount); console.log(`After transfer: ${remaining.toFormatted()}`); // "24.5 USDC" } ``` ## Related * [TxBuilder](/sdk/core/tx-builder): Use Amount with TxBuilder for typed transfers * [Erc20](/sdk/core/erc20): Returns Amount objects from `balanceOf()` and `allowance()` * [TokenRegistry](/sdk/core/token-registry): Look up TokenInfo for Amount.parse() # Erc20 Source: https://docs.chipipay.com/sdk/core/erc20 Typed ERC20 contract wrapper that returns Amount objects for balances and allowances. `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release. ## Overview `Erc20` provides a typed wrapper around ERC20 contract interactions on StarkNet. Read methods (`balanceOf`, `allowance`) return `Amount` objects. Write methods (`approveCall`, `transferCall`) return `Call` objects for use with `TxBuilder` or `Account.execute()`. ## Usage ```typescript theme={null} import { Erc20, Amount, TokenRegistry } from "@chipi-stack/core"; const registry = new TokenRegistry("mainnet"); const usdcInfo = registry.bySymbol("USDC")!; const usdc = new Erc20(usdcInfo, account); // Query balance (returns Amount) const balance = await usdc.balanceOf(myAddress); console.log(balance.toFormatted()); // "150.5 USDC" ``` ## Constructor ```typescript theme={null} new Erc20(token: TokenInfo, account: Account) ``` ### Parameters * `token` (TokenInfo): Object with `symbol`, `decimals`, and `address` * `account` (Account): A starknet.js `Account` instance ## Methods ### `balanceOf(address)` Query the ERC20 balance of an address. ```typescript theme={null} const balance = await usdc.balanceOf("0x123..."); console.log(balance.toFormatted()); // "1500.0 USDC" console.log(balance.toBase()); // 1500000000n ``` #### Parameters * `address` (string): StarkNet address to query #### Returns `Promise`: The balance as an Amount object ### `allowance(owner, spender)` Query the allowance granted by `owner` to `spender`. ```typescript theme={null} const allowed = await usdc.allowance(ownerAddress, spenderAddress); if (allowed.lt(transferAmount)) { console.log("Need to approve more tokens"); } ``` #### Parameters * `owner` (string): Token owner address * `spender` (string): Approved spender address #### Returns `Promise`: The allowance as an Amount object ### `approveCall(spender, amount)` Build an ERC20 approve Call object. Does not execute the transaction. ```typescript theme={null} const call = usdc.approveCall(spenderAddress, amount); // Use with TxBuilder await new TxBuilder(account).add(call).send(); ``` #### Parameters * `spender` (string): Address to approve * `amount` (Amount): Amount to approve #### Returns `Call`: A StarkNet Call object for use with TxBuilder or Account.execute() #### Throws * If `amount.decimals` doesn't match the token's decimals * If `amount.symbol` doesn't match the token's symbol (when set) ### `transferCall(recipient, amount)` Build an ERC20 transfer Call object. Does not execute the transaction. ```typescript theme={null} const call = usdc.transferCall(recipientAddress, amount); await new TxBuilder(account).add(call).send(); ``` #### Parameters * `recipient` (string): Destination address * `amount` (Amount): Amount to transfer #### Returns `Call`: A StarkNet Call object #### Throws * If `amount.decimals` doesn't match the token's decimals * If `amount.symbol` doesn't match the token's symbol (when set) ## Example ### Full Flow: Check Balance, Approve, Transfer ```typescript theme={null} import { Erc20, Amount, TxBuilder, TokenRegistry } from "@chipi-stack/core"; const registry = new TokenRegistry("mainnet"); const usdcInfo = registry.bySymbol("USDC")!; const usdc = new Erc20(usdcInfo, account); const sendAmount = Amount.parse("25.0", usdcInfo); // Check balance const balance = await usdc.balanceOf(myAddress); if (balance.lt(sendAmount)) { throw new Error(`Insufficient: ${balance.toFormatted()} < ${sendAmount.toFormatted()}`); } // Check allowance const allowed = await usdc.allowance(myAddress, dexAddress); // Build atomic transaction const builder = new TxBuilder(account); if (allowed.lt(sendAmount)) { builder.add(usdc.approveCall(dexAddress, sendAmount)); } builder.add(usdc.transferCall(recipientAddress, sendAmount)); // Preview fee, then send const fee = await builder.estimateFee(); console.log(`Fee: ${fee.overall_fee} wei`); const result = await new TxBuilder(account) .add(usdc.approveCall(dexAddress, sendAmount)) .add(usdc.transferCall(recipientAddress, sendAmount)) .send(); ``` ## Related * [Amount](/sdk/core/amount): The value type returned by `balanceOf()` and `allowance()` * [TxBuilder](/sdk/core/tx-builder): Execute Call objects in atomic batches * [TokenRegistry](/sdk/core/token-registry): Look up TokenInfo for the Erc20 constructor # Core SDK Source: https://docs.chipipay.com/sdk/core/introduction Transaction primitives for the Chipi SDK ecosystem: Amount, TxBuilder, ERC20, TokenRegistry, and SignerAdapter. `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release. ## What is @chipi-stack/core? `@chipi-stack/core` provides opt-in transaction primitives for developers who need fine-grained control over StarkNet operations. It complements the existing hooks-based SDKs (Next.js, React, Expo) with lower-level building blocks. ### When to use core vs hooks | Use Case | Recommended | Why | | --------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------ | | Standard transfers in a React app | Hooks (`useTransfer`) | Simpler, handles state management | | Batch approve + transfer atomically | **Core** (`TxBuilder`) | Hooks don't support multicall batching | | Display formatted balances | Either | Core's `Amount` class or shared's `formatAmount` | | Custom contract interactions with fee preview | **Core** (`TxBuilder.estimateFee()`) | Hooks don't expose fee estimation | | Backend automation with programmatic signing | **Core** (`DirectSigner` + `TxBuilder`) | No React context available | | Swap passkey for Argent/Braavos wallet signer | **Core** (`ExternalSigner`) | Hooks are hardwired to passkey flow | | Gasless transactions with any wallet (Argent X, Braavos, Cartridge, DirectSigner) | **Core** (`TxBuilder.sendSponsored()`) | Uses Chipi's paymaster for gas sponsorship | ## Installation ```bash npm theme={null} npm install @chipi-stack/core @chipi-stack/backend ``` ```bash yarn theme={null} yarn add @chipi-stack/core @chipi-stack/backend ``` ```bash pnpm theme={null} pnpm add @chipi-stack/core @chipi-stack/backend ``` ## Quick Example ```typescript theme={null} import { Amount, TxBuilder, Erc20, TokenRegistry, createAccount, DirectSigner } from "@chipi-stack/core"; import { ChipiServerSDK } from "@chipi-stack/backend"; // Create signer + account const signer = new DirectSigner(process.env.PRIVATE_KEY!, myAddress); const account = await createAccount({ signer, provider: { nodeUrl: "https://starknet-mainnet.infura.io/v3/..." }, }); // Look up USDC from the built-in token registry const registry = new TokenRegistry("mainnet"); const usdcInfo = registry.bySymbol("USDC")!; // Parse a human-readable amount safely (no floating point) const amount = Amount.parse("10.0", usdcInfo); // Create a typed ERC20 wrapper const usdc = new Erc20(usdcInfo, account); // Check balance const balance = await usdc.balanceOf(myAddress); console.log(balance.toFormatted()); // "150.5 USDC" // Gasless: approve + transfer via Chipi's paymaster const sdk = new ChipiServerSDK({ apiPublicKey: "pk_...", apiSecretKey: "sk_..." }); const paymaster = sdk.createPaymasterAdapter(); const txHash = await new TxBuilder(account, { paymaster }) .add(usdc.approveCall(spender, amount)) .add(usdc.transferCall(recipient, amount)) .sendSponsored(); // gasless! // Or direct (user pays gas): const result = await new TxBuilder(account) .add(usdc.approveCall(spender, amount)) .add(usdc.transferCall(recipient, amount)) .send(); ``` ## Package Architecture ```mermaid theme={null} graph LR TYPES["@chipi-stack/types"] --> CORE["@chipi-stack/core"] CORE --> AMOUNT["Amount"] CORE --> TXBUILDER["TxBuilder"] CORE --> ERC20["Erc20"] CORE --> REGISTRY["TokenRegistry"] CORE --> SIGNER["SignerAdapter"] ``` `core` depends only on `@chipi-stack/types`. It does not modify or depend on `@chipi-stack/shared`, keeping the two packages independent. ## What's Inside Immutable token amount class with BigInt arithmetic, parsing, and formatting. Fluent builder for batching StarkNet multicalls into atomic transactions. Typed ERC20 contract wrapper that returns Amount objects. Token metadata lookup by symbol or address, with network presets. Unified signing interface for passkeys, direct keys, and external providers. # Privacy (STRK20 shield/unshield) Source: https://docs.chipipay.com/sdk/core/privacy The confidential-balance engine inside @chipi-stack/core: companion accounts, gas-tank funding, proving, and gateway submission. **Shielding (deposits) is temporarily paused.** After StarkWare's July 2026 STRK20 pool **v2.0** upgrade, `shield` / `execShield` into the shared privacy pool requires a screener attestation issued by StarkWare's hosted proving service — Chipi is finalizing that access. The `additional_data` plumbing in this release carries that attestation once available. **`unshield` and `getPrivateBalance` work today; shields will revert until access lands.** We'll announce on X ([@hichipipay](https://x.com/hichipipay)) when shielding is back on. The engine behind [Private Balances](/sdk/guides/private-balances) and the React hooks. Use this layer directly for headless/server flows; apps should prefer the hooks. ## The companion account The StarkWare pool requires its `user_addr` to implement SNIP-6 `is_valid_signature`, which advanced account classes (like Chipi's SHHH wallets) deliberately do not expose. Instead of changing an audited account class, each user gets a **companion**: a lightweight standard account (any SNIP-6 class, e.g. OpenZeppelin `0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f`) that acts as the pool identity. * **Derived, not created**: `deriveCompanionKeypair(anchor)` + `deriveCompanionAddress({publicKey, classHash})` are deterministic from the user's owner-independent anchor. Cache the address; never the keys. * **Counterfactual**: it exists on-chain only after first use. `execShield` deploys it automatically (self-paid from gas-tank-fronted STRK, salt = pubkey so the deployed address equals the derived one). * **Never holds user funds at rest**: deposits pass through it into the pool; withdrawals pass back out to the user's real wallet; leftover fronted STRK sweeps back to the gas tank. Why this design over changing the account class: see our write-up [Adding confidential payments to an advanced smart account — two ways to use the StarkWare privacy pool, and why we picked the companion](https://community.starknet.io/t/adding-confidential-payments-to-an-advanced-smart-account-two-ways-to-use-the-starkware-privacy-pool-and-why-we-picked-the-companion/). ## Headless example ```ts theme={null} import { ChipiProofProvider, derivePoolKeypair, deriveCompanionKeypair, deriveCompanionAddress, requestGasFunding, waitForFunding, execShield, GAS_FUND_STRK, } from "@chipi-stack/core"; import { RpcProvider } from "starknet"; const pool = await derivePoolKeypair({ anchorSecret, seedDomainTag: wallet.address }); const companion = await deriveCompanionKeypair(anchorSecret); const companionAddress = deriveCompanionAddress({ publicKey: companion.publicKey, classHash: COMPANION_CLASS_HASH, }); const provider = new RpcProvider({ nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" }); await requestGasFunding(companionAddress, { baseUrl: "https://api.chipipay.com/v1", apiKey: apiPublicKey, getBearerToken: () => getToken(), }); await waitForFunding(provider, companionAddress, { strk: GAS_FUND_STRK }); const result = await execShield( { provider, chainId: "0x534e5f4d41494e", companionAddress, companionKey: companion.privateKey, classHash: COMPANION_CLASS_HASH, poolKey: BigInt(pool.privateKey), provingProvider: new ChipiProofProvider({ baseUrl: "https://api.chipipay.com/v1", apiKey: apiPublicKey, getBearerToken: () => getToken(), chainId: "0x534e5f4d41494e", }), }, USDC_ADDRESS, 1_000_000n, ); ``` ## Multi-token operations ### Shield several tokens at once `execShieldMany` batches N deposits into ONE `apply_actions` — one proof, one transaction, **one 4-STRK pool fee for the whole batch** (mainnet-proven by the S5 smoke, `scripts/multi-token-smoke.mjs`). It takes the same `CompanionCtx` as `execShield`: ```ts theme={null} import { ChipiProofProvider, derivePoolKeypair, deriveCompanionKeypair, deriveCompanionAddress, execShieldMany, STRK_ADDRESS, } from "@chipi-stack/core"; import { RpcProvider } from "starknet"; const pool = await derivePoolKeypair({ anchorSecret, seedDomainTag: wallet.address }); const companion = await deriveCompanionKeypair(anchorSecret); const ctx = { provider: new RpcProvider({ nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" }), chainId: "0x534e5f4d41494e", companionAddress: deriveCompanionAddress({ publicKey: companion.publicKey, classHash: COMPANION_CLASS_HASH, }), companionKey: companion.privateKey, classHash: COMPANION_CLASS_HASH, poolKey: BigInt(pool.privateKey), provingProvider: new ChipiProofProvider({ baseUrl: "https://api.chipipay.com/v1", apiKey: apiPublicKey, getBearerToken: () => getToken(), chainId: "0x534e5f4d41494e", }), }; // One tx, one proof, ONE 4-STRK fee for the whole batch. const shielded = await execShieldMany(ctx, [ { token: USDC_ADDRESS, amount: 1_000_000n }, // 1 USDC { token: STRK_ADDRESS, amount: 10n ** 18n }, // 1 STRK ]); console.log(shielded.transactionHash, shielded.via); ``` Two things `execShieldMany` handles for you: * **Approvals are aggregated.** When STRK itself is in the batch, its deposit approval and the pool's 4-STRK fee approval share one allowance slot — separate approves would overwrite each other, so they are summed into a single approval. * **Single-item batches delegate** to the proven single-token path — calling it with one deposit is exactly `execShield`. Funding is the same pre-step as the single-token example above (`requestGasFunding` + `waitForFunding`), with one addition: the companion must hold every deposit token before the call, and when STRK is one of the deposits it needs `GAS_FUND_STRK` **plus** the STRK being shielded — the fronted envelope is gas/fee money, never the deposit. ### Unshielding several tokens There is no `unshieldMany` yet: **each withdrawal is its own proof, its own transaction, and its own 4-STRK pool fee.** Two operational constraints, both found on mainnet by the S5 smoke: 1. **Refuel before every withdrawal.** The gateway validates \~7.8 STRK of max resource bounds per transaction, so gas funded once runs dry after the first withdrawal. 2. **Wait for the prover's view between operations.** Proving runs against `head − 12`; state written moments ago is invisible to it for \~13 blocks (\~5–6 minutes). Withdrawals share the user's note channel, so the second compile must see the pool state the first one just changed. Submitting too early fails with the prover's `state_too_fresh` hint (`retry_after_seconds` in the error body) — wait and retry. ```ts theme={null} import { execUnshield, getPrivateBalance, requestGasFunding, waitForFunding, sweepCompanion, GAS_FUND_STRK, type CompanionCtx, } from "@chipi-stack/core"; import { Account } from "starknet"; declare const ctx: CompanionCtx; // the same ctx used to shield declare const gasTankAddress: string; // leftover fronted STRK returns here // Exact private amounts per token (viewing-key note scan, read-only). const account = new Account({ provider: ctx.provider, address: ctx.companionAddress, signer: ctx.companionKey, cairoVersion: "1", }); const { byToken } = await getPrivateBalance({ provider: ctx.provider, account, viewingKey: ctx.poolKey, provingProvider: ctx.provingProvider, }); // The prover proves against head − 12: wait until it can SEE the state the // previous operation wrote before compiling the next one. async function waitProverView(fromBlock: number) { for (;;) { const b = await ctx.provider.getBlockNumber(); if (b >= fromBlock + 13) return; await new Promise((r) => setTimeout(r, 30_000)); } } const gasOpts = { baseUrl: "https://api.chipipay.com/v1", apiKey: apiPublicKey, getBearerToken: () => getToken(), }; // `lastOpBlock` starts at the SHIELD's block — the freshly created notes are // just as invisible to the prover as a fresh withdrawal. let lastOpBlock = await ctx.provider.getBlockNumber(); for (const [token, amountRaw] of Object.entries(byToken)) { if (amountRaw === 0n) continue; await waitProverView(lastOpBlock); await requestGasFunding(ctx.companionAddress, gasOpts); // refuel EVERY op await waitForFunding(ctx.provider, ctx.companionAddress, { strk: GAS_FUND_STRK }); const result = await execUnshield(ctx, token, amountRaw); lastOpBlock = await ctx.provider.getBlockNumber(); // The sweep IS the delivery of the user's money here — strict, not // best-effort, so a failed delivery throws instead of claiming success. await sweepCompanion({ provider: ctx.provider, companionAddress: ctx.companionAddress, companionKey: ctx.companionKey, gasTank: gasTankAddress, deliverToken: { address: token, to: wallet.address }, strict: true, }); console.log(`unshielded ${token}: ${result.transactionHash}`); } ``` In an interactive app you usually won't hard-wait between withdrawals — run them sequentially, catch the `state_too_fresh` failure, tell the user what already came back (it IS back), and retry the remaining token a few minutes later. Money is never stranded: a failed withdrawal leaves the notes in the pool, spendable on the next attempt. ## Trust model Keys derive client-side and are never stored server-side, but delegated proving includes the pool key in each proof request: Chipi's private prover sees it per operation. Privacy holds against the public, not the proving operator. Details in the [guide](/sdk/guides/private-balances). # SignerAdapter Source: https://docs.chipipay.com/sdk/core/signer-adapter Unified signing interface with implementations for passkeys, direct keys, and external providers. `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release. ## Overview `SignerAdapter` is an interface that abstracts over different key management strategies. Instead of hardwiring passkey logic into transaction flows, you program against the `SignerAdapter` interface and swap implementations as needed. ## The Interface ```typescript theme={null} interface SignerAdapter { getPublicKey(): Promise; sign(messageHash: string): Promise; getAccountAddress(): Promise; } interface SignResult { r: string; s: string; } ``` ## Implementations ### DirectSigner Signs with a local private key. For development and testing only. ```typescript theme={null} import { DirectSigner } from "@chipi-stack/core"; const signer = new DirectSigner( "0x1234...privateKey", "0xabcd...accountAddress" // optional ); const pubKey = await signer.getPublicKey(); const sig = await signer.sign(messageHash); ``` Never use DirectSigner with real private keys in production. It exists for local development and testing. #### Constructor ```typescript theme={null} new DirectSigner(privateKey: string, accountAddress?: string) ``` * `privateKey` (string): Hex-encoded StarkNet private key (must start with `0x`) * `accountAddress` (string, optional): Account address. Defaults to the derived public key. #### Throws * If `privateKey` is not a valid hex string starting with `0x` ### PasskeySigner Wraps a WebAuthn assertion function that produces Starknet-compatible signatures `{r, s}` from a challenge. Users authenticate via biometrics (Face ID, fingerprint) and the result is used to sign transactions. ```typescript theme={null} import { PasskeySigner } from "@chipi-stack/core"; // Your WebAuthn assertion function that returns a Starknet signature async function passkeySign(challenge: string): Promise<{ r: string; s: string }> { // Call your WebAuthn authentication flow here // The challenge is the message hash to sign const assertion = await navigator.credentials.get({ publicKey: { challenge: new TextEncoder().encode(challenge), /* ... */ }, }); // Extract r, s from the assertion response return { r: "0x...", s: "0x..." }; } const signer = new PasskeySigner({ authenticateFn: passkeySign, publicKey: userPasskeyPublicKey, accountAddress: userAccountAddress, }); // Now use with TxBuilder via the Account const sig = await signer.sign(messageHash); ``` #### Constructor ```typescript theme={null} new PasskeySigner(opts: { authenticateFn: (challenge: string) => Promise<{ r: string; s: string }>; publicKey: string; accountAddress: string; }) ``` * `authenticateFn`: A function that takes a challenge hash and returns a Starknet-compatible signature `{r, s}` via WebAuthn assertion * `publicKey`: The passkey public key obtained during registration * `accountAddress`: The user's StarkNet account address Most developers should use the `usePasskeyAuth` hook from `@chipi-stack/chipi-passkey/hooks` instead of `PasskeySigner` directly. `PasskeySigner` is for advanced use cases where you need low-level control over the signing flow. ### ExternalSigner Generic adapter for any external signing provider. You supply the three interface methods and ExternalSigner wraps them. ```typescript theme={null} import { ExternalSigner } from "@chipi-stack/core"; // Example: Argent Web Wallet integration const signer = new ExternalSigner({ getPublicKey: async () => argentWallet.getPublicKey(), sign: async (hash) => { const sig = await argentWallet.signMessage(hash); return { r: sig.r, s: sig.s }; }, getAccountAddress: async () => argentWallet.address, }); ``` #### Constructor ```typescript theme={null} new ExternalSigner(opts: { getPublicKey: () => Promise; sign: (messageHash: string) => Promise; getAccountAddress: () => Promise; }) ``` ## Use Cases ### Backend Automation Use `DirectSigner` with a key stored in a vault/HSM for programmatic transaction execution: ```typescript theme={null} const signer = new DirectSigner(process.env.AUTOMATION_KEY!); // Build and sign transactions programmatically ``` ### Multi-Wallet Support Support multiple wallet providers without branching transaction code: ```typescript theme={null} function getSignerForUser(user: User): SignerAdapter { switch (user.walletType) { case "passkey": return new PasskeySigner({ ... }); case "argent": return new ExternalSigner({ ... }); case "braavos": return new ExternalSigner({ ... }); } } // Transaction code is the same regardless of signer const signer = getSignerForUser(currentUser); ``` ### Easy Testing Mock signers for unit tests: ```typescript theme={null} const mockSigner = new ExternalSigner({ getPublicKey: async () => "0xtest_pubkey", sign: async (hash) => ({ r: "0x1", s: "0x2" }), getAccountAddress: async () => "0xtest_account", }); ``` ### Auth Provider Migration If you switch from Privy to another provider, only the SignerAdapter implementation changes. All TxBuilder, Erc20, and Amount code stays the same. ## Connecting to TxBuilder `createAccount()` bridges any `SignerAdapter` into a starknet.js `Account` for use with `TxBuilder`. ### Gasless (recommended) ```typescript theme={null} import { createAccount, DirectSigner, TxBuilder } from "@chipi-stack/core"; import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: "pk_...", apiSecretKey: "sk_..." }); const paymaster = sdk.createPaymasterAdapter(); const provider = { nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY" }; const accountAddress = "0xYOUR_ACCOUNT_ADDRESS"; const signer = new DirectSigner(process.env.PRIVATE_KEY!, accountAddress); const account = await createAccount({ signer, provider }); const txHash = await new TxBuilder(account, { paymaster }) .transfer(USDC, [{ to, amount }]) .sendSponsored(); // gasless! ``` ### Direct (user pays gas) ```typescript theme={null} const provider = { nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY" }; const accountAddress = "0xYOUR_ACCOUNT_ADDRESS"; const signer = new DirectSigner(process.env.PRIVATE_KEY!, accountAddress); const account = await createAccount({ signer, provider }); const result = await new TxBuilder(account) .transfer(USDC, [{ to, amount }]) .send(); // user pays STRK for gas ``` ## Related * [TxBuilder](/sdk/core/tx-builder): Uses the Account (which uses the signer) to execute transactions * [Introduction](/sdk/core/introduction): Overview of when to use core vs hooks # TokenRegistry Source: https://docs.chipipay.com/sdk/core/token-registry Token metadata lookup by symbol or address, with built-in presets for StarkNet mainnet and Sepolia. `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release. ## Overview `TokenRegistry` provides a lookup table for token metadata (symbol, decimals, address, name) on StarkNet networks. It ships with built-in presets for mainnet and Sepolia, eliminating the need to hardcode token addresses. ## Usage ```typescript theme={null} import { TokenRegistry, Amount } from "@chipi-stack/core"; const registry = new TokenRegistry("mainnet"); const usdc = registry.bySymbol("USDC")!; console.log(usdc.address); // "0x033068F6539..." console.log(usdc.decimals); // 6 // Use with Amount const amount = Amount.parse("10.0", usdc); ``` ## Constructor ```typescript theme={null} new TokenRegistry(network: "mainnet" | "sepolia") ``` ### Parameters * `network` (`"mainnet"` | `"sepolia"`): The StarkNet network to load presets for ### Throws * If `network` is not `"mainnet"` or `"sepolia"` ## Methods ### `bySymbol(symbol)` Find a token by its symbol (case-insensitive). ```typescript theme={null} const eth = registry.bySymbol("ETH"); const usdc = registry.bySymbol("usdc"); // case-insensitive ``` #### Parameters * `symbol` (string): Token symbol (e.g., `"USDC"`, `"ETH"`) #### Returns `TokenEntry | undefined` ### `byAddress(address)` Find a token by its contract address (case-insensitive). ```typescript theme={null} // Native USDC on Starknet mainnet (not USDC.e bridged) const token = registry.byAddress("0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb"); console.log(token?.symbol); // "USDC" ``` #### Parameters * `address` (string): Token contract address #### Returns `TokenEntry | undefined` ### `all()` List all tokens in this registry. ```typescript theme={null} const tokens = registry.all(); console.log(tokens.map(t => t.symbol)); // ["ETH", "USDC", "USDT", "DAI", "STRK", "WBTC"] ``` #### Returns `TokenEntry[]` ## Built-in Presets ### Mainnet | Symbol | Decimals | Name | | ------ | -------- | --------------- | | ETH | 18 | Ether | | USDC | 6 | USD Coin | | USDT | 6 | Tether USD | | DAI | 18 | Dai Stablecoin | | STRK | 18 | Starknet Token | | WBTC | 8 | Wrapped Bitcoin | ### Sepolia (Testnet) | Symbol | Decimals | Name | | ------ | -------- | ------------------ | | ETH | 18 | Ether | | USDC | 6 | USD Coin (Testnet) | | STRK | 18 | Starknet Token | ## TokenEntry Type ```typescript theme={null} interface TokenEntry { symbol: string; // e.g., "USDC" decimals: number; // e.g., 6 address: string; // StarkNet contract address name: string; // e.g., "USD Coin" } ``` `TokenEntry` extends `TokenInfo` (used by `Amount.parse()`) with an additional `name` field. ## Example ```typescript theme={null} import { TokenRegistry, Amount, Erc20 } from "@chipi-stack/core"; // Set up registry for the target network const network = process.env.STARKNET_NETWORK === "mainnet" ? "mainnet" : "sepolia"; const registry = new TokenRegistry(network); // Look up token and create typed wrapper const usdcInfo = registry.bySymbol("USDC"); if (!usdcInfo) throw new Error("USDC not found in registry"); const usdc = new Erc20(usdcInfo, account); const amount = Amount.parse("50.0", usdcInfo); const balance = await usdc.balanceOf(myAddress); console.log(`Balance: ${balance.toFormatted()}`); ``` ## Related * [Amount](/sdk/core/amount): Use TokenEntry from the registry with `Amount.parse()` * [Erc20](/sdk/core/erc20): Pass TokenEntry to the Erc20 constructor # TxBuilder Source: https://docs.chipipay.com/sdk/core/tx-builder Fluent builder for batching StarkNet multicalls into atomic transactions with fee estimation. `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release. ## Overview `TxBuilder` provides a fluent API for composing multiple StarkNet calls into a single atomic transaction. All calls execute together via `Account.execute()`, which is more gas-efficient than separate transactions (important when sponsoring gas via a paymaster). ## Usage ```typescript theme={null} import { TxBuilder } from "@chipi-stack/core"; import type { Account } from "starknet"; const result = await new TxBuilder(account) .approve(tokenAddr, spenderAddr, amount) .transfer(tokenAddr, [{ to: recipient, amount: 1_000_000n }]) .send(); console.log(result.transaction_hash); ``` ## Constructor ```typescript theme={null} new TxBuilder(account: Account, opts?: { paymaster?: PaymasterAdapter }) ``` ### Parameters * `account` (Account): A starknet.js `Account` instance * `opts.paymaster` (PaymasterAdapter, optional): Adapter for gasless transactions via `sendSponsored()` ## Methods ### `add(call)` Add one or more raw StarkNet calls to the batch. ```typescript theme={null} builder.add({ contractAddress: "0x...", entrypoint: "swap", calldata: [tokenIn, tokenOut, amountIn, "0"], }); // Or add multiple at once builder.add([call1, call2, call3]); ``` #### Parameters * `call` (Call | Call\[]): A single Call or array of Calls #### Returns `this` (chainable) ### `approve(token, spender, amount)` Add an ERC20 approve call. Handles uint256 encoding automatically. ```typescript theme={null} builder.approve(USDC_ADDRESS, DEX_ADDRESS, 1_000_000n); ``` #### Parameters * `token` (string): Token contract address * `spender` (string): Address to approve * `amount` (bigint): Raw amount in base units #### Returns `this` (chainable) ### `transfer(token, targets)` Add one or more ERC20 transfer calls. ```typescript theme={null} builder.transfer(USDC_ADDRESS, [ { to: "0xalice...", amount: 500_000n }, { to: "0xbob...", amount: 300_000n }, ]); ``` #### Parameters * `token` (string): Token contract address * `targets` (TransferTarget\[]): Array of `{ to: string, amount: bigint }` pairs #### Returns `this` (chainable) ### `calls()` Returns a copy of the current call batch (for inspection). ```typescript theme={null} const currentCalls = builder.calls(); console.log(`${currentCalls.length} calls queued`); ``` #### Returns `Call[]` ### `send()` Execute all batched calls atomically. ```typescript theme={null} const result = await builder.send(); console.log(result.transaction_hash); ``` #### Returns `Promise` #### Throws * If no calls have been added * If `send()` was already called (builders are single-use) ### `sendSponsored()` Execute via Chipi's paymaster (gasless). Requires a `PaymasterAdapter` in the constructor. ```typescript theme={null} const txHash = await builder.sendSponsored(); console.log(txHash); // "0x..." ``` #### Returns `Promise` (transaction hash) #### Throws * If no `paymaster` was configured in the constructor * If no calls have been added * If `send()` or `sendSponsored()` was already called (builders are single-use) ### `estimateFee()` Estimate the fee for the current batch without executing. ```typescript theme={null} const fee = await builder.estimateFee(); console.log(`Estimated fee: ${fee.overall_fee} wei`); ``` #### Returns `Promise` #### Throws * If no calls have been added ### `preflight()` Simulate the transaction without submitting. Returns the transaction trace and fee estimation. Useful for checking if a transaction would succeed before sending. ```typescript theme={null} const sim = await builder.preflight(); console.log(`Would cost: ${sim.fee_estimation.overall_fee} wei`); ``` #### Returns `Promise` with `transaction_trace` and `fee_estimation` #### Throws * If no calls have been added ## Examples ### Simple Transfer ```typescript theme={null} const result = await new TxBuilder(account) .transfer(USDC_ADDRESS, [{ to: recipient, amount: 1_500_000n }]) .send(); ``` ### Approve + Swap (Atomic Batch) ```typescript theme={null} const result = await new TxBuilder(account) .approve(USDC_ADDRESS, DEX_ADDRESS, 1_000_000n) .add({ contractAddress: DEX_ADDRESS, entrypoint: "swap", calldata: [USDC_ADDRESS, ETH_ADDRESS, "1000000", "0"], }) .send(); ``` ### Fee Preview Before Sending ```typescript theme={null} const builder = new TxBuilder(account) .transfer(USDC_ADDRESS, [{ to, amount }]); const fee = await builder.estimateFee(); console.log(`This will cost: ${fee.overall_fee} wei`); // User confirms, create new builder to send const result = await new TxBuilder(account) .transfer(USDC_ADDRESS, [{ to, amount }]) .send(); ``` ### With Erc20 and Amount ```typescript theme={null} import { TxBuilder, Erc20, Amount, TokenRegistry } from "@chipi-stack/core"; const registry = new TokenRegistry("mainnet"); const usdcInfo = registry.bySymbol("USDC")!; const usdc = new Erc20(usdcInfo, account); const amount = Amount.parse("50.0", usdcInfo); const result = await new TxBuilder(account) .add(usdc.approveCall(spender, amount)) .add(usdc.transferCall(recipient, amount)) .send(); ``` ### Gasless with Argent X / Braavos (browser) ```typescript theme={null} import { connect } from "starknetkit"; import { TxBuilder } from "@chipi-stack/core"; import { ChipiBrowserSDK } from "@chipi-stack/backend"; const { wallet } = await connect(); const sdk = new ChipiBrowserSDK({ apiPublicKey: "pk_..." }); const paymaster = sdk.createPaymasterAdapter(userBearerToken); // wallet.account already has the Argent/Braavos signer — triggers popup on sign const txHash = await new TxBuilder(wallet.account, { paymaster }) .transfer(USDC, [{ to: recipient, amount: 1_000_000n }]) .sendSponsored(); // popup -> sign -> gasless execution ``` ### Gasless with Cartridge Controller ```typescript theme={null} import Controller from "@cartridge/controller"; import { TxBuilder } from "@chipi-stack/core"; import { ChipiBrowserSDK } from "@chipi-stack/backend"; const controller = new Controller(); await controller.connect(); const sdk = new ChipiBrowserSDK({ apiPublicKey: "pk_..." }); const paymaster = sdk.createPaymasterAdapter(userBearerToken); // controller IS Account-compatible const txHash = await new TxBuilder(controller, { paymaster }) .transfer(USDC, [{ to, amount }]) .sendSponsored(); ``` ### Backend Automation (gasless) ```typescript theme={null} import { createAccount, DirectSigner, TxBuilder } from "@chipi-stack/core"; import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: "pk_...", apiSecretKey: "sk_..." }); const paymaster = sdk.createPaymasterAdapter(); const provider = { nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY" }; const accountAddress = "0xYOUR_ACCOUNT_ADDRESS"; const signer = new DirectSigner(process.env.AUTOMATION_KEY!, accountAddress); const account = await createAccount({ signer, provider }); const txHash = await new TxBuilder(account, { paymaster }) .approve(USDC, spender, amount) .transfer(USDC, [{ to, amount }]) .sendSponsored(); // gasless, no popup ``` ## Related * [Amount](/sdk/core/amount): Type-safe token amounts for use with TxBuilder * [Erc20](/sdk/core/erc20): Generate typed Call objects for approve/transfer * [SignerAdapter](/sdk/core/signer-adapter): Different signing strategies for the Account # Expo with Clerk Source: https://docs.chipipay.com/sdk/expo/gasless-clerk-setup Learn how to integrate Chipi Pay with your Expo application using Clerk Auth for authentication and biometric security. # Building with Expo This guide will walk you through integrating Chipi Pay into your Expo application with biometric authentication and secure storage. We'll cover everything from installation to implementing secure payment flows. ## Prerequisites * Node.js 20.19.0 or later * **Expo SDK 55 or later** (`@chipi-stack/chipi-expo` peer-requires `expo-local-authentication >=55.0.0` and `expo-secure-store >=55.0.0`) * Expo CLI * Basic knowledge of React Native * A Chipi Pay account * Clerk account for authentication * Device with biometric authentication support (for biometric features) ## Getting Started To get started with Chipi Pay in your Expo application, you'll need to have a basic Expo project set up. If you don't have one yet, you can create it using: ```bash theme={null} npx create-expo-app@latest my-chipi-app cd my-chipi-app ``` `create-expo-app@latest` currently scaffolds Expo SDK 54, but `@chipi-stack/chipi-expo` requires SDK 55+ (for `expo-local-authentication` and `expo-secure-store` peer dependencies). Upgrade before installing the Chipi SDK: ```bash theme={null} npx expo install expo@~55.0.12 expo-local-authentication expo-secure-store ``` Verified: `npx expo install` picks SDK-55-compatible versions automatically (`expo-local-authentication@~55.0.12`, `expo-secure-store@~55.0.12`). ## Installation After upgrading to SDK 55, install the remaining packages: ```bash theme={null} # Install Chipi Pay SDK npx expo install @chipi-stack/chipi-expo # Install Clerk and authentication packages # (expo-local-authentication and expo-secure-store already installed in the SDK 55 upgrade above) npx expo install @clerk/clerk-expo expo-web-browser ``` ## Configuration 1. Create a `.env` file in your project root and add your **publishable** API keys: ```bash theme={null} EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY=pk_dev_your_public_key_here EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_clerk_publishable_key ``` **Never put secret keys in an Expo client app.** `@chipi-stack/chipi-expo` is a mobile SDK — treat `apiSecretKey` as backend-only and do not pass it from client code. (Note: `ChipiProvider` accepts `ChipiSDKConfig` which includes `apiSecretKey?: string` as optional — TypeScript will not block you, but passing it bundles the secret into your app binary. The type-level guard `apiSecretKey?: never` exists only on `ChipiBrowserSDKConfig` used by the web SDK.) Clerk's `@clerk/clerk-expo` only accepts a `publishableKey` — there is no `secretKey` prop on `ClerkProvider`. Secret keys (`CHIPI_SECRET_KEY`, `CLERK_SECRET_KEY`) belong on your backend, never in a mobile app bundle. You can get your Chipi API public key [here](https://dashboard.chipipay.com/configure/credentials). And your Clerk publishable key [here](https://dashboard.clerk.com/last-active?path=api-keys). 2. Update your `app.json` to include the required permissions: ```json theme={null} { "expo": { "plugins": [ [ "expo-local-authentication", { "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID to unlock your wallet." } ], "expo-secure-store" ], "ios": { "supportsTablet": true, "infoPlist": { "NSFaceIDUsageDescription": "Allow $(PRODUCT_NAME) to use Face ID to unlock your wallet." } } } } ``` ## Implementing Secure Authentication Flow 1. Wrap your app in `ClerkProvider` and `ChipiProvider`. In Expo Router projects (the default from `create-expo-app`), the root component lives at `app/_layout.tsx`: ```typescript theme={null} // app/_layout.tsx import { ClerkProvider } from "@clerk/clerk-expo"; import { ChipiProvider } from "@chipi-stack/chipi-expo"; import { tokenCache } from "@clerk/clerk-expo/token-cache"; import { Slot } from "expo-router"; export default function RootLayout() { return ( ); } ``` `ChipiProvider` takes configuration via the `config` prop (`ChipiSDKConfig`). In Expo usage, pass only public client-safe values (`apiPublicKey`, and optionally `alphaUrl`/`nodeUrl`). Do not pass `apiPublicKey` as a direct prop on `ChipiProvider`. ## Adding Clerk JWKS Endpoint Go into your Clerk dashboard and copy the [JWKS endpoint](https://dashboard.clerk.com/last-active?path=api-keys) available in the developers tab. Register JWKS Endpoint URL in the [dashboard](https://dashboard.chipipay.com/configure/jwks-endpoint). ## Next Steps * Add error handling and loading states * Customize the UI to match your app's design Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Expo with Custom Auth Source: https://docs.chipipay.com/sdk/expo/gasless-custom-auth-setup Integrate Chipi Pay with any OIDC-compliant auth provider (Auth0, Cognito, Okta, Keycloak) in your Expo application. ## What is JWKS? JSON Web Key Set (JWKS) is a standard that allows Chipi Pay to verify your users' JWT tokens without ever seeing their credentials. Your auth provider exposes a public endpoint with its signing keys, and Chipi uses those keys to verify that tokens are authentic. Any OIDC-compliant provider (Auth0, Cognito, Okta, Keycloak, etc.) exposes a JWKS endpoint. ```bash theme={null} npx expo install @chipi-stack/chipi-expo ``` Add your Chipi API key to your `.env` file: ```bash theme={null} EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY=your_chipi_api_public_key ``` You can get your API keys from the [Chipi Dashboard](https://dashboard.chipipay.com/configure/credentials). Wrap your app with `ChipiProvider` alongside your auth provider: ```tsx theme={null} // App.tsx import { ChipiProvider } from "@chipi-stack/chipi-expo"; export default function App() { return ( ); } ``` Use your auth provider's SDK to get a JWT token, then pass it to Chipi hooks: ```typescript theme={null} // components/CreateWallet.tsx import { useState } from "react"; import { View, TextInput, TouchableOpacity, Text } from "react-native"; import { useCreateWallet } from "@chipi-stack/chipi-expo"; // Replace with your auth provider's token method async function getBearerToken(): Promise { // Auth0: const { getAccessTokenSilently } = useAuth0(); // Cognito: const session = await Auth.currentSession(); // Okta: const { authState } = useOktaAuth(); return "your-jwt-token"; } export default function CreateWallet() { const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const handleCreateWallet = async () => { const bearerToken = await getBearerToken(); const response = await createWalletAsync({ params: { encryptKey }, bearerToken, }); console.log("Wallet created:", response.publicKey); }; return ( {isLoading ? "Creating..." : "Create Wallet"} ); } ``` Register your auth provider's JWKS endpoint in the [Chipi Dashboard](https://dashboard.chipipay.com/configure/jwks-endpoint): 1. Go to Configure > Auth Provider 2. Select **Other** as your provider 3. Paste your provider's URL or JWKS endpoint 4. The dashboard will try OIDC discovery automatically 5. Click **Verify & Save** ### Compatible providers | Provider | JWKS URL pattern | | ----------- | ------------------------------------------------------------------------ | | Auth0 | `https://YOUR_DOMAIN/.well-known/jwks.json` | | AWS Cognito | `https://cognito-idp.REGION.amazonaws.com/POOL_ID/.well-known/jwks.json` | | Okta | `https://YOUR_DOMAIN/oauth2/default/v1/keys` | | Keycloak | `https://YOUR_HOST/realms/REALM/protocol/openid-connect/certs` | Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Expo with Firebase Source: https://docs.chipipay.com/sdk/expo/gasless-firebase-setup Learn how to integrate Chipi Pay with your Expo application using Firebase Auth for authentication and secure wallet storage. 1. First, in order to create authentication with Firebase you will need two things: first, a [Firebase account](https://firebase.google.com/) and second, a [Expo](https://expo.dev/) project. Once you have your account you must: * Go to your [console](https://console.firebase.google.com) and create a new Firebase project. * Once you have a project, proceed to the `Authentication` section. * Click on the `Sign in method` tab and enable the `Email/password` sign-in method and click `save`. * Go to `project settings`, scroll and under your apps section click on the `web app` usually displayed with a `` icon. * Register your app and click save. 2. Install the npm package in your project: ```bash theme={null} npm install firebase ``` On the Firebase Register web app page, you will also be provided something like this. Save it for later. ```typescript theme={null} // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: "apikey", authDomain: "domain", projectId: "your-id", storageBucket: "storage-bucket", messagingSenderId: "000000", appId: "app-id" }; // Initialize Firebase const app = initializeApp(firebaseConfig); ``` 1. Create a file named `firebase.ts` with the following configuration: ```typescript theme={null} import { initializeApp } from "firebase/app"; import { initializeAuth, getReactNativePersistence } from 'firebase/auth'; import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage'; import { getFirestore } from "firebase/firestore"; import { getStorage } from 'firebase/storage'; const firebaseConfig = { apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID, storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID, }; // Initialize Firebase export const firebaseApp = initializeApp(firebaseConfig) export const auth = initializeAuth(firebaseApp, { persistence: getReactNativePersistence(ReactNativeAsyncStorage) }); export const db = getFirestore(firebaseApp); export const storage = getStorage(firebaseApp); ``` 2. Initialize metro config: run the following command to initialize metro config ```bash theme={null} npx expo customize metro.config.js ``` 3. Edit your `metro.config.js` file ```typescript theme={null} const { getDefaultConfig } = require('@expo/metro-config'); const defaultConfig = getDefaultConfig(__dirname); defaultConfig.resolver.sourceExts.push('cjs'); module.exports = defaultConfig; ``` 4. Go to `tsconfig.json` and add to paths for typescript issues ```json theme={null} { "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "paths": { "@firebase/auth": ["./node_modules/@firebase/auth/dist/index.rn.d.ts"], "@/*": [ "./*" ] } }, "include": [ "**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts" ] } ``` 5. If not already installed, install: ```bash theme={null} npm install @react-native-async-storage/async-storage ``` Install the required packages: ```bash theme={null} # Install Chipi Pay SDK npm install @chipi-stack/chipi-expo ``` 1. In your `.env` file, add your Chipi **publishable** API key: ```bash theme={null} EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY=your_chipi_api_public_key ``` Never put `CHIPI_SECRET_KEY` in an Expo app — the SDK explicitly forbids it at the type level via `ChipiBrowserSDKConfig`. Secret keys belong on your backend only. 2. Set up the Chipi Pay provider in your application: ```tsx theme={null} // app/_layout.tsx import { ChipiProvider } from "@chipi-stack/chipi-expo"; const API_PUBLIC_KEY = process.env.EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY; if (!API_PUBLIC_KEY) throw new Error("EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY is not set"); export default function RootLayout() { // ... return ( ); } ``` Visit the [dashboard JWKS and JWT configuration](https://dashboard.chipipay.com/configure/jwks-endpoint), select `Firebase` as your auth provider, and paste this under the JWKS Endpoint URL. ```bash theme={null} https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys ``` Visit the [Firebase Authentication guide](https://firebase.google.com/docs/auth/web/password-auth) to learn how to use `createUserWithEmailAndPassword` and create your sign up and login! Below you will see a simple example of how you can get and pass your tokens to the different Chipi hooks: ```typescript theme={null} import { useGetWallet } from "@chipi-stack/chipi-expo"; import { useEffect, useState } from "react"; import { View, Text, ActivityIndicator } from "react-native"; import { onAuthStateChanged } from "firebase/auth"; import { auth } from "@/utils/firebase"; export default function Home() { const [userId, setUserId] = useState(null); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (user) => { if (user) { setUserId(user.uid); } }); return () => unsubscribe(); }, []); const getToken = async (): Promise => { const user = auth.currentUser; if (!user) { throw new Error("User not authenticated"); } const token = await user.getIdToken(); return token; }; const { data: wallet, isLoading } = useGetWallet({ getBearerToken: getToken, params: { externalUserId: userId || "", }, }); return ( Home page {isLoading && } {wallet && {JSON.stringify(wallet, null, 2)}} ); } ``` Note: If you have not yet created a wallet, you may see an error with a message like: message:"Wallet not found for externalUserId: user\_id, apiPublicKey: pk\_prod\_something" This error is expected and shows that your app is correctly contacting Chipi, but there is not yet a wallet for this user. Once you create a wallet, this error will disappear. That's it! You should now have an initial working version of Chipi Pay integrated into your application. You can now start implementing various features like: * Wallet Creations * Sending tokens * Signing transactions * and more! Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Quickstart Source: https://docs.chipipay.com/sdk/expo/gasless-quickstart Quick setup guide for gasless transactions with Chipi SDK in Expo applications Gasless transactions are only available in Starknet Mainnet. You'll need to [add the JWKS Endpoint](https://dashboard.chipipay.com/configure/jwks-endpoint) from your auth provider to authenticate gasless transactions. With this JWKS Endpoint setup, you can now run secure code in your frontend using a rotating JWT token. ### Getting your JWKS Endpoint For these common auth providers, you can get your JWKS Endpoint like this: 1. Go to your [API Keys](https://dashboard.clerk.com/last-active?path=api-keys) 2. Copy your **JWKS Endpoint** and paste it into the JWKS Endpoint field in your Chipi Dashboard. 3. Follow our documentation to add [Clerk and Chipi](/sdk/expo/gasless-clerk-setup) to your application. See our tutorial on how to set up Chipi with [Firebase](/sdk/expo/gasless-firebase-setup)!. Now go to your [Credentials page](https://dashboard.chipipay.com/configure/credentials) and click on your project. Which will give you access to your **Public Key** (`pk_prod_xxxx`). Keep this handy as you'll need it to initialize the SDK. You won't need the Secret Key (`sk_prod_xxxx`) for now. ```bash npm theme={null} npm install @chipi-stack/chipi-expo@latest ``` ```bash yarn theme={null} yarn add @chipi-stack/chipi-expo@latest ``` ```bash pnpm theme={null} pnpm add @chipi-stack/chipi-expo@latest ``` 1. Add your **Public Key** to your environment variables. ```bash theme={null} EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY=pk_prod_xxxx ``` Never put `CHIPI_SECRET_KEY` in an Expo app — the SDK explicitly forbids it at the type level via `ChipiBrowserSDKConfig`. Secret keys belong on your backend only. 2. Wrap your main `_layout.tsx` with the `ChipiProvider` component. ```tsx theme={null} // app/_layout.tsx import { ChipiProvider } from "@chipi-stack/chipi-expo"; const API_PUBLIC_KEY = process.env.EXPO_PUBLIC_CHIPI_API_PUBLIC_KEY; if (!API_PUBLIC_KEY) throw new Error("API_PUBLIC_KEY is not set"); export default function RootLayout() { // ... return ( ); } ``` You're ready to use the SDK! We recommend starting with our **convenience hooks** that simplify common operations: ### useChipiWallet - Unified Wallet Management ```tsx theme={null} import { useChipiWallet } from "@chipi-stack/chipi-expo"; import { View, Text, TouchableOpacity } from "react-native"; function WalletComponent() { const { userId, getToken } = useYourAuthProvider(); const { wallet, hasWallet, formattedBalance, createWallet, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); if (!hasWallet) { return ( createWallet({ encryptKey: "1234" })}> Create Wallet ); } return Balance: ${formattedBalance} USDC; } ``` Learn about all the options and return values ### useChipiSession - Gasless Session Keys (CHIPI wallets only) ```tsx theme={null} import { useChipiWallet, useChipiSession } from "@chipi-stack/chipi-expo"; function GaslessTransactions() { const { wallet } = useChipiWallet({ ... }); const { hasActiveSession, createSession, registerSession, executeWithSession, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Setup session once const handleSetup = async () => { await createSession(); await registerSession(); }; // Execute transactions without owner signature const handleTransfer = async () => { await executeWithSession([{ contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }]); }; } ``` Learn about session key management That's it! You should now have an initial working version of Chipi Pay integrated into your application. Explore more features: * [All Available Hooks](/sdk/expo/hooks/use-create-wallet) - Complete hook reference * [Session Keys](/sdk/expo/hooks/use-chipi-session) - Enable gasless UX * [Buying Services](/sdk/expo/buying-services) - Accept crypto payments * [Biometrics](/sdk/expo/use-biometrics) - Secure PIN storage with Face ID / Touch ID Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) to ask us anything on your mind. # useAddSessionKeyToContract Source: https://docs.chipipay.com/sdk/expo/hooks/use-add-session-key-to-contract Register a session key on the CHIPI wallet smart contract. This one-time operation requires owner signature and enables the session key to execute transactions. Session keys only work with **CHIPI wallets**. This hook requires the owner's `encryptKey` (PIN) to sign the registration transaction. ## Usage ```typescript theme={null} const { addSessionKeyToContract, addSessionKeyToContractAsync, data, isLoading, error, isSuccess, reset } = useAddSessionKeyToContract(); ``` ### Parameters | Parameter | Type | Required | Description | | ---------------------- | ------------- | -------- | ------------------------------------------------------------------- | | `params.encryptKey` | string | Yes | Owner's PIN to decrypt wallet private key for signing | | `params.wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `params.sessionConfig` | SessionConfig | Yes | Session configuration (see below) | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### SessionConfig Structure ```typescript theme={null} { sessionPublicKey: string; // From useCreateSessionKey().data.publicKey validUntil: number; // Unix timestamp (seconds) maxCalls: number; // Maximum transactions allowed allowedEntrypoints: string[]; // Whitelisted function selectors (empty = all allowed) } ``` ### Return Value | Property | Type | Description | | ------------------------------ | ------------- | --------------------------------------------------- | | `addSessionKeyToContract` | function | Trigger registration (fire-and-forget) | | `addSessionKeyToContractAsync` | function | Trigger registration (returns Promise with tx hash) | | `data` | string | Transaction hash of registration | | `isLoading` | boolean | Whether registration is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether registration succeeded | | `reset` | function | Reset mutation state | ## Example Implementation ```typescript theme={null} import { useAddSessionKeyToContract, useCreateSessionKey, useGetWallet } from "@chipi-stack/chipi-react"; export function RegisterSession() { const { createSessionKeyAsync } = useCreateSessionKey(); const { addSessionKeyToContractAsync, isLoading, error } = useAddSessionKeyToContract(); const { data: wallet } = useGetWallet({ params: { externalUserId: "user-123" }, getBearerToken }); const [pin, setPin] = useState(''); const handleRegisterSession = async () => { const bearerToken = await getBearerToken(); // Step 1: Create session key const session = await createSessionKeyAsync({ encryptKey: pin, durationSeconds: 3600, }); // Step 2: Register on contract const txHash = await addSessionKeyToContractAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, // Allow 100 transactions allowedEntrypoints: [], // Empty = all functions allowed }, }, bearerToken, }); // Store session for later use localStorage.setItem('chipiSession', JSON.stringify(session)); console.log('Session registered:', txHash); }; return (

Register Session Key

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded mb-4" /> {error && (
Error: {error.message}
)}
); } ``` ## Restricting Session Permissions For enhanced security, whitelist specific function selectors: ```typescript theme={null} const txHash = await addSessionKeyToContractAsync({ params: { encryptKey: pin, wallet: walletData, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 10, // Only 10 calls allowed allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve ], }, }, bearerToken, }); ``` Limiting `allowedEntrypoints` restricts what functions the session can call. This is recommended for production to minimize attack surface. ## Error Handling Common errors: | Error | Cause | Solution | | ---------------------------------------- | -------------------------- | ---------------------------------------------------------- | | `Session keys require CHIPI wallet type` | Wallet is READY type | Create a new wallet with `walletType: "CHIPI"` | | `Invalid encryptKey` | Wrong PIN entered | Verify the PIN matches the one used during wallet creation | | `Session already exists` | Session already registered | Use `useGetSessionData` to check status | Registration requires a blockchain transaction. The transaction is gas-sponsored but may take 10-30 seconds to confirm. # useApprove Source: https://docs.chipipay.com/sdk/expo/hooks/use-approve Grants permission to a smart contract to spend tokens from the user wallet. Required before staking or other delegated actions. ## Usage ```typescript theme={null} const { approve, approveAsync, data, isLoading, isError, error, isSuccess, reset } = useApprove(); ``` ### Parameters The hook accepts an object with: * `params` (`ApproveHookInput`): * `encryptKey` (string): User's decryption PIN * `wallet` (`WalletData`): Wallet public/private key pair * `contractAddress` (string): Token contract to approve * `spender` (string): Contract address to authorize * `amount` (number): Maximum spend allowance (will be converted to string internally) * `decimals` (number, optional): Token decimals (default: 18) * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `approve`: Function to trigger token approval (fire-and-forget) * `approveAsync`: Promise-based function that resolves with the transaction hash * `data`: Transaction hash of the approval (string | undefined) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the approval completed successfully * `reset`: Function to reset the mutation state ## Example Implementation for Approving VESU ```typescript theme={null} const VESU_CONTRACT = "0x037ae3f583c8d644b7556c93a04b83b52fa96159b2b0cbd83c14d3122aef80a2"; // Sample wallet data - replace with your wallet in production. // Keep walletType + signerKind from createWallet / useGetWallet: the SDK // routes by walletType, and omitting it defaults to "READY" (a SHHH wallet // would revert). const sampleWallet: { publicKey: string; encryptedPrivateKey: string; walletType: "SHHH" | "CHIPI" | "READY"; signerKind?: "STARK" | "WEBAUTHN_P256" | "EIP191_SECP256K1" | "ED25519"; } = { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data", // This would be encrypted data in production walletType: "SHHH", signerKind: "STARK", }; export function ApproveForm() { const { approveAsync, data, isLoading, isError, error } = useApprove(); const [wallet] = useState(sampleWallet); const [form, setForm] = useState({ pin: '', amount: '' }); const handleApprove = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken) { console.error('No bearer token available'); return; } try { await approveAsync({ params: { encryptKey: form.pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: wallet.walletType, // routing-critical (SHHH/CHIPI/READY) signerKind: wallet.signerKind, }, contractAddress: VESU_CONTRACT, spender: VESU_CONTRACT, // Authorizing VESU contract amount: Number(form.amount), decimals: 18 }, bearerToken, }); } catch (err) { console.error('Approval failed:', err); } }; return (

Approve Token Access

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, amount: e.target.value})} className="w-full p-2 border rounded-md" required />
{data && (

Approval TX: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always use encrypted private keys * Never store or transmit raw private keys * Implement proper PIN validation * Use secure storage for wallet data * Consider implementing approval limits ## Error Handling * Handle insufficient token balance * Validate contract addresses * Check for existing approvals * Monitor gas fees * Implement retry logic for failed transactions Approvals are specific to each token-contract pair. You'll need to re-approve when interacting with new contracts. ## Related Hooks * **useTransfer** - For moving tokens * **useCallAnyContract** - For custom contract interactions that need prior approval # useCallAnyContract Source: https://docs.chipipay.com/sdk/expo/hooks/use-call-any-contract Executes any StarkNet contract method. Handles all contract interactions not covered by specific hooks. ## Usage ```typescript theme={null} const { callAnyContract, callAnyContractAsync, data, isLoading, isError, error, isSuccess, reset } = useCallAnyContract(); ``` ### Parameters The hook accepts an object with: * `params` (`CallAnyContractParams`): * `encryptKey` (string): User's decryption PIN (or passkey-derived key) * `wallet` (`WalletData`): the full wallet object — `publicKey`, `encryptedPrivateKey`, and (routing-critical) **`walletType`** + **`signerKind`** from `createWallet` / `useGetWallet`. Omitting `walletType` defaults to `"READY"` and reverts on a SHHH wallet. * `contractAddress` (string): Target contract address * `calls` (`Call[]`): Array of contract calls with `contractAddress`, `entrypoint`, and `calldata` * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `callAnyContract`: Function to trigger contract call (fire-and-forget) * `callAnyContractAsync`: Promise-based function that resolves with the transaction hash * `data`: Transaction hash of the call (string | undefined) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the call completed successfully * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} export function GenericContractForm() { const { callAnyContractAsync, data, isLoading, isError, error } = useCallAnyContract(); const [form, setForm] = useState({ pin: '', contractAddress: '', entrypoint: '', calldata: '' }); const handleCall = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getBearerToken(); try { await callAnyContractAsync({ params: { encryptKey: form.pin, wallet: walletData, contractAddress: form.contractAddress, calls: [ { contractAddress: form.contractAddress, entrypoint: form.entrypoint, calldata: JSON.parse(form.calldata || '[]') } ], }, bearerToken, }); } catch (err) { console.error('Contract call failed:', err); } }; return (

Contract Interaction

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, contractAddress: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, entrypoint: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, calldata: e.target.value})} className="w-full p-2 border rounded-md" placeholder='Example: [123, "0xabc...", true]' />
{data && (

TX Hash: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Verify contract addresses * Validate calldata format * Use encrypted private keys * Implement proper PIN validation * Review contract ABI before calling ## Error Handling * Handle invalid contract addresses * Validate calldata format * Monitor gas fees * Implement retry logic for failed calls Use with caution. Direct contract calls require deep understanding of the target contract's ABI and security implications. ## Related Hooks * **useApprove** - For token approvals * **useTransfer** - For token transfers # useChipiSession Source: https://docs.chipipay.com/sdk/expo/hooks/use-chipi-session Unified session key management for gasless UX - create, register, execute, and revoke sessions with a single hook. ## Overview `useChipiSession` provides a unified API for managing session keys, enabling gasless transactions without requiring the owner's signature for each operation. This hook combines all session-related operations: * **Session creation** (local keypair generation) * **Session registration** (on-chain) * **Transaction execution** (using session key) * **Session revocation** (on-chain) * **Status checking** (remaining calls, expiration) Session keys only work with **CHIPI wallets**. READY wallets do not support session keys. ## Prerequisites Before using `useChipiSession`, you need: 1. A **CHIPI wallet** (check `wallet.walletType === "CHIPI"` or `wallet.supportsSessionKeys`) 2. The user's **encryption key (PIN)** 3. An **authentication token** (from Clerk, Firebase, etc.) ## Session Lifecycle | State | Description | | --------- | ---------------------------------------------------- | | `none` | No session exists | | `created` | Session created locally, not yet registered on-chain | | `active` | Session registered and usable | | `expired` | Session has expired (time or call limit) | | `revoked` | Session has been revoked on-chain | ## Quick Start ```tsx theme={null} import { useChipiWallet, useChipiSession } from "@chipi-stack/chipi-expo"; import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native"; function SessionComponent() { const { userId, getToken } = useYourAuthProvider(); const [pin, setPin] = useState(""); const { wallet } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); const { session, sessionState, hasActiveSession, remainingCalls, createSession, registerSession, executeWithSession, isCreating, isRegistering, isExecuting, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Setup session (create + register) const handleSetup = async () => { await createSession(); await registerSession(); }; // Execute a transfer using the session const handleTransfer = async () => { await executeWithSession([{ contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }]); }; return ( Session: {sessionState} {hasActiveSession && Calls remaining: {remainingCalls}} {!hasActiveSession ? ( Setup Session ) : ( Transfer (Gasless) )} ); } ``` ## Configuration Options ```typescript theme={null} interface UseChipiSessionConfig { // Required wallet: SessionWallet | null | undefined; encryptKey: string; getBearerToken: () => Promise; // Optional storedSession?: SessionKeyData | null; onSessionCreated?: (session: SessionKeyData) => void | Promise; defaultDurationSeconds?: number; // Default: 21600 (6 hours) defaultMaxCalls?: number; // Default: 1000 autoCheckStatus?: boolean; // Default: true } ``` | Option | Type | Default | Description | | ------------------------ | ----------------------- | -------- | -------------------------------- | | `wallet` | `SessionWallet` | Required | Wallet data (must be CHIPI type) | | `encryptKey` | `string` | Required | User's PIN for signing | | `getBearerToken` | `() => Promise` | Required | Auth token function | | `storedSession` | `SessionKeyData` | `null` | Previously stored session | | `onSessionCreated` | `(session) => void` | - | Callback to persist new session | | `defaultDurationSeconds` | `number` | `21600` | Session duration (6 hours) | | `defaultMaxCalls` | `number` | `1000` | Max calls per session | | `autoCheckStatus` | `boolean` | `true` | Auto-fetch on-chain status | ## Return Values ### Session Data | Property | Type | Description | | ------------------ | ------------------------ | -------------------------------- | | `session` | `SessionKeyData \| null` | Current session data | | `sessionStatus` | `SessionDataResponse` | On-chain session status | | `sessionState` | `SessionState` | Lifecycle state | | `hasActiveSession` | `boolean` | Whether session is usable | | `isSessionExpired` | `boolean` | Whether session has expired | | `remainingCalls` | `number \| undefined` | Calls left in session | | `supportsSession` | `boolean` | Whether wallet supports sessions | ### Actions | Method | Returns | Description | | --------------------------- | ------------------------- | ---------------------------------- | | `createSession(config?)` | `Promise` | Create new session locally | | `registerSession(config?)` | `Promise` | Register on-chain (returns txHash) | | `revokeSession()` | `Promise` | Revoke on-chain (returns txHash) | | `executeWithSession(calls)` | `Promise` | Execute calls (returns txHash) | | `clearSession()` | `void` | Clear local session state | | `refetchStatus()` | `Promise` | Refresh on-chain status | ### Loading States | Property | Description | | ----------------- | ---------------------------- | | `isCreating` | Creating session locally | | `isRegistering` | Registering session on-chain | | `isRevoking` | Revoking session on-chain | | `isExecuting` | Executing transaction | | `isLoadingStatus` | Fetching on-chain status | ## Persisting Sessions with SecureStore In Expo, use `expo-secure-store` to safely persist session data: ```tsx theme={null} import * as SecureStore from 'expo-secure-store'; import { useState, useEffect } from "react"; import { useChipiWallet, useChipiSession } from "@chipi-stack/chipi-expo"; const SESSION_KEY = "chipi-session"; function PersistentSession() { const { wallet } = useChipiWallet({ ... }); const [storedSession, setStoredSession] = useState(null); // Load session on mount useEffect(() => { async function loadSession() { const saved = await SecureStore.getItemAsync(SESSION_KEY); if (saved) { setStoredSession(JSON.parse(saved)); } } loadSession(); }, []); const { session, hasActiveSession, createSession, registerSession, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, storedSession, onSessionCreated: async (newSession) => { // Persist to SecureStore await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify(newSession)); setStoredSession(newSession); }, }); // Session is automatically restored on mount! } ``` ## Using with Biometrics Combine with `useBiometrics` hook for secure PIN-less UX: ```tsx theme={null} import { useBiometrics, useChipiWallet, useChipiSession } from "@chipi-stack/chipi-expo"; function BiometricSession() { const { authenticate, isAvailable } = useBiometrics(); const { wallet } = useChipiWallet({ ... }); // Store PIN securely and retrieve with biometrics const [encryptKey, setEncryptKey] = useState(""); const { hasActiveSession, createSession, registerSession, executeWithSession, } = useChipiSession({ wallet, encryptKey, getBearerToken: getToken, }); const handleSetupWithBiometrics = async () => { // Authenticate with Face ID / Touch ID const success = await authenticate("Confirm to setup session"); if (!success) return; // Retrieve stored PIN (implement your own secure storage) const pin = await getStoredPin(); setEncryptKey(pin); await createSession(); await registerSession(); }; // ... } ``` ## Executing Transactions The `executeWithSession` method accepts an array of Starknet calls: ```typescript theme={null} // Single transfer await executeWithSession([{ contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }]); // Batch multiple calls await executeWithSession([ { contractAddress: USDC_CONTRACT, entrypoint: "approve", calldata: [spender, amount, "0x0"], }, { contractAddress: DEX_CONTRACT, entrypoint: "swap", calldata: [...], }, ]); ``` ## Custom Session Configuration ### Custom Duration ```typescript theme={null} // Create session valid for 1 hour await createSession({ durationSeconds: 3600 }); ``` ### Custom Max Calls ```typescript theme={null} // Register with 500 max calls await registerSession({ maxCalls: 500 }); ``` ## Error Handling ```tsx theme={null} const { error, session } = useChipiSession({...}); // Check for errors if (error) { Alert.alert("Session Error", error.message); } // Handle specific errors const handleSetup = async () => { try { await createSession(); await registerSession(); } catch (err) { if (err.message.includes("does not support sessions")) { Alert.alert("Error", "Please use a CHIPI wallet for sessions"); } else { Alert.alert("Error", err.message); } } }; ``` # useChipiWallet Source: https://docs.chipipay.com/sdk/expo/hooks/use-chipi-wallet All-in-one hook for wallet management - combines wallet fetching, creation, and balance checking into a single unified API. ## Overview `useChipiWallet` is a convenience hook that simplifies wallet management by combining multiple operations into one: * **Wallet fetching** (replaces `useGetWallet`) * **Wallet creation** (replaces `useCreateWallet`) * **Balance checking** (replaces `useGetTokenBalance`) Use this hook when you want a streamlined API. Use the individual hooks (`useGetWallet`, `useCreateWallet`, `useGetTokenBalance`) when you need more granular control. ## Quick Start ```typescript theme={null} import { useChipiWallet } from "@chipi-stack/chipi-expo"; function WalletComponent() { const { userId, getToken } = useYourAuthProvider(); // Clerk, Firebase, etc. const { wallet, hasWallet, balance, formattedBalance, createWallet, isCreating, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); if (isLoadingWallet) return Loading...; if (!hasWallet) { return ( {error && (
Error: {error.message}
)} {session && (

Session created! Expires: {new Date(session.validUntil * 1000).toLocaleString()}

)} ); } ``` ## Security Considerations **Never store session keys in your backend database.** This defeats the purpose of self-custodial security. Store only in client-side secure storage. | Platform | Recommended Storage | | ---------------- | ---------------------------------------------- | | **Web** | `localStorage` or `sessionStorage` | | **iOS** | Secure Enclave / Keychain | | **Android** | Android Keystore | | **React Native** | `expo-secure-store` or `react-native-keychain` | ## Best Practices 1. **Set short durations** - Use 1-6 hours, not days 2. **Clear on logout** - Always remove session from storage when user logs out 3. **Register on-chain** - After creating, use `useAddSessionKeyToContract` to register it This hook only generates the session keypair locally. You must call `useAddSessionKeyToContract` to register it on-chain before it can be used for transactions. # useCreateWallet Source: https://docs.chipipay.com/sdk/expo/hooks/use-create-wallet Creates a new Argent-compatible wallet on StarkNet. This hook deploys the wallet contract behind the scenes and uses Avnus gasless to sponsor gas fees, resulting in a frictionless onboarding experience. ## Usage ```typescript theme={null} const { createWalletAsync, data, isLoading, error } = useCreateWallet(); ``` ### Parameters The mutation accepts an object with: * `params` (`CreateWalletParams`): * `encryptKey` (string): PIN or passkey-derived key used to encrypt the private key * `externalUserId` (string): Your application's unique identifier for the user * `chain` (`Chain`): The blockchain network. Use `Chain.STARKNET` * `walletType` (`"SHHH" | "CHIPI" | "READY"`, optional): **defaults to `"SHHH"`** (the pluggable-signer ShhhAccount — guardian recovery, multisig). `"CHIPI"` is the legacy account that supports session keys; `"READY"` is Argent X compatible. Whatever you create here, persist `walletType` (and `signerKind`) and pass them back on every transfer/approve/call. * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey (via `@chipi-stack/chipi-passkey`) * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `createWallet`: Function to trigger wallet creation (fire-and-forget) * `createWalletAsync`: Promise-based function that resolves with the wallet data * `data`: The created wallet (`CreateWalletResponse`) — flat object with `publicKey`, `encryptedPrivateKey`, `walletType`, `chain`, etc. * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if wallet creation succeeded * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useCreateWallet, Chain } from "@chipi-stack/nextjs"; export function CreateWallet() { const { userId, getToken } = useAuth(); const { createWalletAsync, data, isLoading, error } = useCreateWallet(); const [pin, setPin] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken || !userId) { console.error('No bearer token or user ID available'); return; } try { const wallet = await createWalletAsync({ params: { encryptKey: pin, externalUserId: userId, chain: Chain.STARKNET, // Omit walletType to get the default SHHH account, or set it // explicitly. Use "CHIPI" only if you need legacy session keys. walletType: "SHHH", }, bearerToken, }); alert('Wallet created successfully!'); } catch (err) { console.error('Wallet creation failed:', err); } }; return (

Create New Wallet

setPin(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500" required minLength={4} />
{error && (
Error: {error.message}
)} {data && (

Wallet Details

View Contract

Address: {data.publicKey}

)}
); } ``` ## Security Considerations * PINs should always be collected client-side * Never log or store raw PIN values * Use secure encryption before any transmission * Store encrypted private key securely * Associate with user session (not persistent storage) ## Error Handling * Catch and handle RPC connection errors * Monitor gasless API limits * Implement retry logic for failed deployments Wallet creation is free! Gas fees are covered by our gasless integration. # useExecuteWithSession Source: https://docs.chipipay.com/sdk/expo/hooks/use-execute-with-session Execute gasless transactions using a session key instead of the owner private key. Enables frictionless UX for gaming, DeFi automation, and more. This hook uses the session key signature (4-element format) instead of the owner signature. The session must be registered on-chain first via `useAddSessionKeyToContract`. ## Usage ```typescript theme={null} const { executeWithSession, executeWithSessionAsync, data, isLoading, error, isSuccess, reset } = useExecuteWithSession(); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------- | -------------- | -------- | --------------------------------------------------------- | | `params.encryptKey` | string | Yes | PIN to decrypt the **session** private key | | `params.wallet` | WalletData | Yes | Wallet object (session executes on behalf of this wallet) | | `params.session` | SessionKeyData | Yes | Session key data from `useCreateSessionKey` | | `params.calls` | Call\[] | Yes | Array of contract calls to execute | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### Call Structure ```typescript theme={null} { contractAddress: string; // Target contract address entrypoint: string; // Function name to call calldata: string[]; // Function arguments } ``` ### Return Value | Property | Type | Description | | ------------------------- | ------------- | ------------------------------------------------ | | `executeWithSession` | function | Trigger execution (fire-and-forget) | | `executeWithSessionAsync` | function | Trigger execution (returns Promise with tx hash) | | `data` | string | Transaction hash | | `isLoading` | boolean | Whether execution is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether execution succeeded | | `reset` | function | Reset mutation state | ## Example: Token Transfer ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/chipi-react"; // Native USDC on Starknet mainnet (not USDC.e bridged) const USDC_ADDRESS = "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb"; export function SessionTransfer() { const { executeWithSessionAsync, isLoading, error, data: txHash } = useExecuteWithSession(); const [amount, setAmount] = useState(''); const [recipient, setRecipient] = useState(''); const handleTransfer = async () => { const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession')!); const pin = await promptForPin(); // Your PIN input method try { const hash = await executeWithSessionAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, session, calls: [{ contractAddress: USDC_ADDRESS, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }], }, bearerToken, }); console.log('Transfer executed:', hash); } catch (err) { console.error('Transfer failed:', err); } }; return (

Transfer with Session

setRecipient(e.target.value)} placeholder="Recipient address (0x...)" className="w-full p-2 border rounded" /> setAmount(e.target.value)} placeholder="Amount (in smallest units)" className="w-full p-2 border rounded" />
{error && (
Error: {error.message}
)} {txHash && ( )}
); } ``` ## Example: Gaming Actions ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/chipi-react"; const GAME_CONTRACT = "0x..."; export function GameBoard() { const { executeWithSessionAsync, isLoading } = useExecuteWithSession(); const makeMove = async (moveData: string) => { const session = JSON.parse(localStorage.getItem('chipiSession')!); const bearerToken = await getBearerToken(); // Execute instantly - no wallet popup! await executeWithSessionAsync({ params: { encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, session, calls: [{ contractAddress: GAME_CONTRACT, entrypoint: "make_move", calldata: [moveData], }], }, bearerToken, }); }; return (
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((cell) => ( ))}
); } ``` ## Multi-Call Transactions Execute multiple calls in a single transaction: ```typescript theme={null} const txHash = await executeWithSessionAsync({ params: { encryptKey: pin, wallet: walletData, session, calls: [ // Approve spending { contractAddress: USDC_ADDRESS, entrypoint: "approve", calldata: [SWAP_CONTRACT, amount, "0x0"], }, // Execute swap { contractAddress: SWAP_CONTRACT, entrypoint: "swap", calldata: [USDC_ADDRESS, ETH_ADDRESS, amount, minReceived], }, ], }, bearerToken, }); ``` ## Error Handling | Error | Cause | Solution | | ----------------------------------------------- | ----------------------------- | -------------------------------------------------- | | `Session execution only supports CHIPI wallets` | Wallet is READY type | Use a CHIPI wallet | | `Session has expired` | `validUntil` timestamp passed | Create and register a new session | | `Session not found` | Not registered on-chain | Call `useAddSessionKeyToContract` first | | `Max calls exceeded` | `remainingCalls` is 0 | Create a new session with higher `maxCalls` | | `Entrypoint not allowed` | Function not in whitelist | Register session with correct `allowedEntrypoints` | Session transactions decrement `remainingCalls` on the contract. Monitor usage with `useGetSessionData` and create new sessions before exhaustion. For the best UX, pre-create sessions during onboarding and store the PIN securely. This allows instant transactions without prompting for PIN each time. # useGetSessionData Source: https://docs.chipipay.com/sdk/expo/hooks/use-get-session-data Query session key status from the CHIPI wallet smart contract. Returns whether the session is active, remaining calls, and allowed entrypoints. ## Usage ```typescript theme={null} const { data, isLoading, isError, error, isSuccess, refetch } = useGetSessionData(params, options); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------- | ------- | -------- | ---------------------------------------- | | `params.walletAddress` | string | Yes | Wallet address to query | | `params.sessionPublicKey` | string | Yes | Session public key to query | | `options.enabled` | boolean | No | Enable/disable the query (default: true) | Pass `null` as params to disable the query. ### Return Value | Property | Type | Description | | ----------- | ------------------- | ----------------------------- | | `data` | SessionDataResponse | Session status from contract | | `isLoading` | boolean | Whether query is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether query succeeded | | `refetch` | function | Manually refetch session data | ### SessionDataResponse Structure ```typescript theme={null} { isActive: boolean; // Whether session is currently valid validUntil: number; // Expiration timestamp (Unix seconds) remainingCalls: number; // Transactions remaining before exhausted allowedEntrypoints: string[]; // Whitelisted function selectors } ``` ## Example Implementation ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; export function SessionStatus() { // Get session from local storage const session = JSON.parse(localStorage.getItem('chipiSession') || 'null'); const { data: sessionData, isLoading, refetch } = useGetSessionData( session ? { walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, } : null ); if (!session) { return
No active session
; } if (isLoading) { return
Loading session status...
; } return (

Session Status

Status: {sessionData?.isActive ? "Active" : "Inactive"}
Expires: {sessionData?.validUntil ? new Date(sessionData.validUntil * 1000).toLocaleString() : "N/A"}
Remaining Calls: {sessionData?.remainingCalls ?? 0}
Restrictions: {sessionData?.allowedEntrypoints?.length ? `${sessionData.allowedEntrypoints.length} functions` : "All functions"}
{!sessionData?.isActive && (
Session is inactive. Create a new session to continue.
)}
); } ``` ## Conditional Rendering Based on Session ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; export function GameInterface() { const session = JSON.parse(localStorage.getItem('chipiSession') || 'null'); const { data: sessionData } = useGetSessionData( session ? { walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, } : null ); // Check if session is valid before allowing actions const canPlay = sessionData?.isActive && sessionData?.remainingCalls > 0; return (
{canPlay ? ( ) : (

Session expired or no calls remaining

)}
); } ``` ## Auto-Refresh Session Status ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; import { useEffect } from "react"; export function useSessionMonitor(walletAddress: string, sessionPublicKey: string) { const { data, refetch } = useGetSessionData({ walletAddress, sessionPublicKey, }); // Refresh every 30 seconds useEffect(() => { const interval = setInterval(() => { refetch(); }, 30000); return () => clearInterval(interval); }, [refetch]); return { isActive: data?.isActive ?? false, remainingCalls: data?.remainingCalls ?? 0, expiresAt: data?.validUntil ? new Date(data.validUntil * 1000) : null, }; } ``` This is a **read-only** query that doesn't require gas or signatures. It directly calls the smart contract's view function. If the session doesn't exist on-chain, `isActive` will be `false` with `remainingCalls: 0`. This is normal for sessions that were never registered or have been revoked. # useGetTokenBalance Source: https://docs.chipipay.com/sdk/expo/hooks/use-get-token-balance Fetches the on-chain token balance for a wallet address. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTokenBalance, } = useGetTokenBalance({ params: { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey: "0x...", }, getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ------------------------ | ----------------------- | -------- | --------------------------------------------------- | | `params.chainToken` | `ChainToken` | Yes | Token identifier. Use `ChainToken.USDC` | | `params.chain` | `Chain` | Yes | Blockchain network. Use `Chain.STARKNET` | | `params.walletPublicKey` | `string` | No | Wallet address to check balance for | | `params.externalUserId` | `string` | No | External user ID (alternative to `walletPublicKey`) | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ------------------- | --------------------------------------------- | --------------------------------------------- | | `data` | `GetTokenBalanceResponse \| undefined` | Balance data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => void` | Re-run the query | | `fetchTokenBalance` | `(input) => Promise` | Imperatively fetch balance with custom params | ## Example Implementation ```typescript theme={null} import { useGetTokenBalance, Chain, ChainToken } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function TokenBalance({ walletPublicKey }: { walletPublicKey: string }) { const { getToken } = useAuth(); const { data, isLoading, error } = useGetTokenBalance({ params: { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey, }, getBearerToken: getToken, }); if (isLoading) return

Loading balance...

; if (error) return

Error: {error.message}

; return (

Balance: {data?.balance} USDC

); } ``` Either `walletPublicKey` or `externalUserId` must be provided to identify the wallet. # useGetTransaction Source: https://docs.chipipay.com/sdk/expo/hooks/use-get-transaction Fetches a single transaction by its hash or internal ID. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransaction, } = useGetTransaction({ hashOrId: "0x...", getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ---------------- | ----------------------- | -------- | ------------------------------------------------- | | `hashOrId` | `string` | Yes | Transaction hash (`0x...`) or internal ID | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ------------------ | ------------------------------------ | ------------------------------------- | | `data` | `Transaction \| undefined` | Transaction data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => Promise` | Re-run the query | | `fetchTransaction` | `(input) => Promise` | Imperatively fetch with custom params | ## Example Implementation ```typescript theme={null} import { useGetTransaction } from "@chipi-stack/chipi-react"; export function TransactionDetail({ txHash, getBearerToken, }: { txHash: string; getBearerToken: () => Promise; }) { const { data: tx, isLoading, error } = useGetTransaction({ hashOrId: txHash, getBearerToken, }); if (isLoading) return

Loading transaction...

; if (error) return

Error: {error.message}

; return (

Hash: {tx?.transactionHash}

Status: {tx?.status}

From: {tx?.senderAddress}

To: {tx?.destinationAddress}

Amount: {tx?.amount} {tx?.token}

); } ``` # useGetTransactionList Source: https://docs.chipipay.com/sdk/expo/hooks/use-get-transaction-list Fetches a paginated list of transactions with optional date and address filters. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransactionList, } = useGetTransactionList({ query: { page: 1, limit: 10, walletAddress: "0x...", }, getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ---------------------- | ----------------------- | -------- | ------------------------------------------------- | | `query.page` | `number` | No | Page number (default: `1`) | | `query.limit` | `number` | No | Results per page (default: `10`) | | `query.walletAddress` | `string` | No | Filter by wallet address | | `query.calledFunction` | `string` | No | Filter by contract function name | | `query.day` | `number` | No | Filter by day (1–31) | | `query.month` | `number` | No | Filter by month (1–12) | | `query.year` | `number` | No | Filter by year | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ---------------------- | ---------------------------------------------------- | ------------------------------------- | | `data` | `PaginatedResponse \| undefined` | Paginated transaction results | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => void` | Re-run the query | | `fetchTransactionList` | `(input) => Promise>` | Imperatively fetch with custom params | ## Example Implementation ```typescript theme={null} import { useGetTransactionList } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; import { useState } from "react"; export function TransactionList({ walletAddress }: { walletAddress: string }) { const { getToken } = useAuth(); const [page, setPage] = useState(1); const { data, isLoading, error } = useGetTransactionList({ query: { page, limit: 10, walletAddress, }, getBearerToken: getToken, }); if (isLoading) return

Loading transactions...

; if (error) return

Error: {error.message}

; return (
    {data?.data.map((tx) => (
  • {tx.transactionHash} — {tx.status}
  • ))}
Page {page}
); } ``` # useGetTransactionStatus Source: https://docs.chipipay.com/sdk/expo/hooks/use-get-transaction-status Fetches and polls the on-chain status of a transaction. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransactionStatus, } = useGetTransactionStatus({ hash: "0x...", getBearerToken: getToken, refetchInterval: 3000, // polls every 3s, stops on terminal status }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ----------------- | ----------------------- | -------- | ------------------------------------------------------------- | | `hash` | `string` | Yes | Transaction hash (`0x...`) | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `refetchInterval` | `number \| false` | No | Polling interval in ms (default: `3000`). `false` to disable. | | `queryOptions` | `UseQueryOptions` | No | React Query options | ### Return Value | Property | Type | Description | | ------------------------ | ----------------------------------------------- | ---------------------------- | | `data` | `TransactionStatusResponse \| undefined` | Transaction status data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => Promise` | Re-run the query | | `fetchTransactionStatus` | `(input) => Promise` | Imperatively fetch | Polling automatically stops when the status reaches a terminal state: `ACCEPTED_ON_L1`, `REJECTED`, or `REVERTED`. ## Example Implementation ```typescript theme={null} import { useGetTransactionStatus } from "@chipi-stack/chipi-react"; import { OnChainTxStatus } from "@chipi-stack/types"; export function TransactionTracker({ txHash, getBearerToken, }: { txHash: string; getBearerToken: () => Promise; }) { const { data, isLoading } = useGetTransactionStatus({ hash: txHash, getBearerToken, refetchInterval: 3000, }); if (isLoading) return

Checking status...

; const confirmed = data?.status === OnChainTxStatus.ACCEPTED_ON_L2 || data?.status === OnChainTxStatus.ACCEPTED_ON_L1; return (

Status: {data?.status}

{data?.blockNumber &&

Block: {data.blockNumber}

} {data?.revertReason &&

Revert reason: {data.revertReason}

} {confirmed &&

Transaction confirmed!

}
); } ``` # useGetWallet Source: https://docs.chipipay.com/sdk/expo/hooks/use-get-wallet Get an authenticated user Wallet ## Usage ```typescript theme={null} const { fetchWallet, data, isLoading, error } = useGetWallet(); ``` ### Parameters * `params` (`GetWalletParams`): * `externalUserId` (string): User ID from your auth provider * `getBearerToken` (`() => Promise`): Function returning the auth token * `queryOptions` (`UseQueryOptions`, optional): React Query options (e.g. `enabled`) ### Return Value Returns an object containing: * `fetchWallet`: Function to manually fetch wallet data with custom params * `data`: The wallet object (`publicKey`, `encryptedPrivateKey`, `walletType`, etc.) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `isSuccess`: Boolean indicating if the query succeeded * `error`: Any error that occurred during the process * `refetch`: Function to re-run the query ## Example Implementation ```typescript theme={null} import { useGetWallet } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function WalletPage(){ const { userId, getToken } = useAuth(); const { data: wallet, isLoading, error, fetchWallet } = useGetWallet({ params: { externalUserId: userId || "", }, getBearerToken: async () => { const token = await getToken(); if (!token) throw new Error("No token found"); return token; }, queryOptions: { enabled: Boolean(userId), }, }); // Or use fetchWallet manually: const loadWallet = async () => { if (!userId) return; try { const token = await getToken(); if (!token) throw new Error("No token found"); const wallet = await fetchWallet({ params: { externalUserId: userId, }, getBearerToken: async () => token, }); console.log("Wallet loaded:", wallet); } catch (error) { console.error("Error loading wallet:", error); } }; return (
{isLoading &&

Loading wallet...

} {error &&

Error: {error.message}

} {wallet && (

Public Key: {wallet.publicKey}

Normalized Public Key: {wallet.normalizedPublicKey}

)}
); } ``` Chipi never stores your decrypted sensitive data. All private keys and authentication tokens remain secure and are never accessible to Chipi servers. # useMigrateWalletToPasskey Source: https://docs.chipipay.com/sdk/expo/hooks/use-migrate-wallet-to-passkey Migrates an existing PIN-encrypted wallet to passkey (biometric) authentication. ## Usage ```typescript theme={null} const { migrateWalletToPasskey, migrateWalletToPasskeyAsync, data, isLoading, isError, isSuccess, error, reset, } = useMigrateWalletToPasskey(); ``` ### Parameters The mutation accepts an object with: | Parameter | Type | Required | Description | | ---------------- | ------------ | -------- | --------------------------------------------------------------- | | `wallet` | `WalletData` | Yes | The current wallet object (`publicKey` + `encryptedPrivateKey`) | | `oldEncryptKey` | `string` | Yes | The user's current PIN used to decrypt the wallet | | `externalUserId` | `string` | Yes | Your app's user ID — used as the passkey username | | `bearerToken` | `string` | Yes | Auth token from your provider | ### Return Value | Property | Type | Description | | ----------------------------- | -------------------------------------------------- | --------------------------- | | `migrateWalletToPasskey` | `(input) => void` | Fire-and-forget migration | | `migrateWalletToPasskeyAsync` | `(input) => Promise` | Promise-based migration | | `data` | `MigrateWalletToPasskeyResult \| undefined` | Migration result | | `isLoading` | `boolean` | True while migrating | | `isError` | `boolean` | True if migration failed | | `isSuccess` | `boolean` | True if migration succeeded | | `error` | `Error \| null` | Error details | | `reset` | `() => void` | Reset mutation state | ### `MigrateWalletToPasskeyResult` | Property | Type | Description | | -------------- | ------------ | ------------------------------------------------------ | | `success` | `boolean` | Whether migration succeeded | | `wallet` | `WalletData` | Updated wallet with new passkey-encrypted private key | | `credentialId` | `string` | The passkey credential ID (store this for future auth) | ## How It Works 1. A new passkey is created in the browser/device (triggers biometric prompt) 2. The wallet's private key is decrypted using `oldEncryptKey` (the PIN) 3. The private key is re-encrypted using the passkey-derived key 4. The updated wallet is returned — **persist the new wallet object and `credentialId`** After migration, the old PIN (`oldEncryptKey`) will no longer work. Store the updated wallet object and `credentialId` securely. ## Example Implementation ```typescript theme={null} import { useMigrateWalletToPasskey } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; import { useState } from "react"; export function MigrateToPasskey({ currentWallet, userId }: { currentWallet: WalletData; userId: string; }) { const { getToken } = useAuth(); const [currentPin, setCurrentPin] = useState(""); const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey(); const handleMigrate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; try { const result = await migrateWalletToPasskeyAsync({ wallet: currentWallet, oldEncryptKey: currentPin, externalUserId: userId, bearerToken, }); // Persist the updated wallet and credentialId localStorage.setItem("wallet", JSON.stringify(result.wallet)); localStorage.setItem("passkeyCredentialId", result.credentialId); alert("Successfully migrated to passkey!"); } catch (err) { console.error("Migration failed:", err); } }; return (
setCurrentPin(e.target.value)} /> {error &&

Error: {error.message}

}
); } ``` ## Related * [Use Passkeys Guide](/sdk/react/use-passkeys) — Full passkey setup walkthrough * **useCreateWallet** — Create a wallet with passkey from the start (set `usePasskey: true`) # useRecordSendTransaction Source: https://docs.chipipay.com/sdk/expo/hooks/use-record-transaction Records a sent transaction in the Chipi system for tracking and analytics purposes. ## Usage ```typescript theme={null} const { recordSendTransactionAsync, data, isLoading, error } = useRecordSendTransaction(); ``` ### Parameters The hook accepts an object with: * `params` (`RecordSendTransactionParams`): * `transactionHash` (string): The hash of the transaction to record * `chain` (`Chain`): The blockchain network. Use `Chain.STARKNET` * `expectedSender` (string): The expected sender wallet address * `expectedRecipient` (string): The expected recipient wallet address * `expectedToken` (`ChainToken`): The expected token. Use `ChainToken.USDC` * `expectedAmount` (string): The expected transaction amount * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `recordSendTransaction`: Function to record transaction (fire-and-forget) * `recordSendTransactionAsync`: Promise-based function that resolves with the transaction record * `data`: The recorded transaction data (`Transaction | undefined`) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the recording completed successfully * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} import { useRecordSendTransaction } from "@chipi-stack/chipi-react"; import { Chain, ChainToken } from "@chipi-stack/types"; export function RecordTransactionForm() { const { recordSendTransactionAsync, data, isLoading, isError, error } = useRecordSendTransaction(); const { getToken } = useAuth(); const handleRecordTransaction = async () => { try { const token = await getToken(); if (!token) throw new Error("No token found"); // After a transfer, record the transaction const transactionHash = "0x..."; // From your transfer transaction const senderWallet = { publicKey: "0x123..." }; const merchant = "0x456..."; const amountUsd = "100.00"; await recordSendTransactionAsync({ params: { transactionHash, chain: Chain.STARKNET, expectedSender: senderWallet.publicKey, expectedRecipient: merchant, expectedToken: ChainToken.USDC, expectedAmount: amountUsd, }, bearerToken: token, }); } catch (err) { console.error('Recording transaction failed:', err); } }; return (

Record Transaction

{data && (

Transaction Recorded: {data.id}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always verify recipient addresses * Use encrypted private keys * Implement proper PIN validation * Monitor transaction status ## Error Handling * Handle insufficient token balance * Validate wallet addresses * Monitor gas fees * Implement retry logic for failed transactions Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Hooks * **useTransfer** - Execute the transfer before recording it * **useGetTransactionList** - List all recorded transactions # useRevokeSessionKey Source: https://docs.chipipay.com/sdk/expo/hooks/use-revoke-session-key Revoke a session key from the CHIPI wallet smart contract. After revocation, the session key can no longer execute transactions. Always revoke active sessions when a user logs out to prevent unauthorized access. ## Usage ```typescript theme={null} const { revokeSessionKey, revokeSessionKeyAsync, data, isLoading, error, isSuccess, reset } = useRevokeSessionKey(); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------- | ---------- | -------- | ------------------------------------------------------------------- | | `params.encryptKey` | string | Yes | Owner's PIN to decrypt wallet private key for signing | | `params.wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `params.sessionPublicKey` | string | Yes | Public key of the session to revoke | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### Return Value | Property | Type | Description | | ----------------------- | ------------- | ------------------------------------------------- | | `revokeSessionKey` | function | Trigger revocation (fire-and-forget) | | `revokeSessionKeyAsync` | function | Trigger revocation (returns Promise with tx hash) | | `data` | string | Transaction hash of revocation | | `isLoading` | boolean | Whether revocation is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether revocation succeeded | | `reset` | function | Reset mutation state | ## Example Implementation ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/chipi-react"; export function RevokeSession() { const { revokeSessionKeyAsync, isLoading, error, isSuccess } = useRevokeSessionKey(); const [pin, setPin] = useState(''); const handleRevoke = async () => { const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession') || '{}'); if (!session.publicKey) { alert('No active session found'); return; } try { const txHash = await revokeSessionKeyAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionPublicKey: session.publicKey, }, bearerToken, }); // Clear from local storage localStorage.removeItem('chipiSession'); console.log('Session revoked:', txHash); } catch (err) { console.error('Failed to revoke session:', err); } }; return (

Revoke Session

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded mb-4" /> {error && (
Error: {error.message}
)} {isSuccess && (
Session revoked successfully!
)}
); } ``` ## Secure Logout Handler Always revoke sessions during logout: ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/chipi-react"; export function useSecureLogout() { const { revokeSessionKeyAsync } = useRevokeSessionKey(); const logout = async (wallet: WalletData, pin: string) => { const sessionStr = localStorage.getItem('chipiSession'); if (sessionStr) { const session = JSON.parse(sessionStr); const bearerToken = await getBearerToken(); try { // Revoke session on-chain before logout await revokeSessionKeyAsync({ params: { encryptKey: pin, wallet: { ...wallet, walletType: "CHIPI" }, sessionPublicKey: session.publicKey, }, bearerToken, }); } catch (error) { console.warn("Failed to revoke session:", error); // Continue with logout even if revoke fails } // Clear from local storage localStorage.removeItem('chipiSession'); } // Proceed with auth logout await authProvider.signOut(); }; return { logout }; } ``` If revocation fails (e.g., network error), the session will still expire at its `validUntil` timestamp. However, it's best practice to always attempt revocation. ## When to Revoke | Scenario | Should Revoke? | | ------------------------- | --------------------------------------------- | | User logs out | ✅ Yes - always | | Session expired naturally | ❌ No - already inactive | | User requests new session | ⚠️ Optional - old session will expire | | Security concern | ✅ Yes - immediately | | App uninstall | N/A - Can't revoke, session expires naturally | Revocation requires a blockchain transaction which may take 10-30 seconds. Don't block the logout flow - fire the revocation and continue. # useSyncOnChainTransfers Source: https://docs.chipipay.com/sdk/expo/hooks/use-sync-on-chain-transfers Sync on-chain token transfers into the database. Discovers external receives not made through the SDK. ## Usage ```typescript theme={null} const { syncTransfers, syncTransfersAsync, data, isLoading, isError, error, isSuccess, reset } = useSyncOnChainTransfers(); ``` ### Parameters `syncTransfers` / `syncTransfersAsync` accept an object with: | Parameter | Type | Required | Description | | --------------- | -------- | -------- | --------------------------------------- | | `walletAddress` | `string` | Yes | Wallet public key to sync transfers for | | `bearerToken` | `string` | Yes | Bearer token for authentication | ### Return Value | Property | Type | Description | | -------------------- | -------------------------------------------------- | ------------------------------ | | `syncTransfers` | `(input) => void` | Trigger sync (fire-and-forget) | | `syncTransfersAsync` | `(input) => Promise` | Promise-based sync | | `data` | `SyncOnChainTransfersResponse \| undefined` | Sync result | | `isLoading` | `boolean` | Whether sync is in progress | | `isError` | `boolean` | Whether an error occurred | | `error` | `Error \| null` | Error from last sync attempt | | `isSuccess` | `boolean` | Whether last sync succeeded | | `reset` | `() => void` | Reset mutation state | ### SyncOnChainTransfersResponse | Field | Type | Description | | -------- | -------- | ---------------------------------------------- | | `synced` | `number` | New transactions discovered and saved to DB | | `total` | `number` | Total transfers found on-chain for this wallet | ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useSyncOnChainTransfers, useGetTransactionList, } from "@chipi-stack/nextjs"; export function TransactionHistory({ wallet }: { wallet: { publicKey: string } }) { const { getToken } = useAuth(); const { syncTransfersAsync, isLoading: isSyncing } = useSyncOnChainTransfers(); const { data: txList, refetch } = useGetTransactionList({ query: { walletAddress: wallet.publicKey }, getBearerToken: getToken, }); const handleSync = async () => { const token = await getToken(); if (!token) return; // Discover external transfers (e.g., USDC received from Argent X) const result = await syncTransfersAsync({ walletAddress: wallet.publicKey, bearerToken: token, }); console.log(`Found ${result.synced} new transfers`); // Transaction list cache is automatically invalidated — refetch happens }; return (
{txList?.data.map((tx) => (
{tx.subType === "receive" ? "Received" : "Sent"} {tx.amount} {tx.token} {tx.transactionHash.slice(0, 12)}...
))}
); } ``` ## How It Works 1. Calls `POST /transactions/sync-on-chain` on the backend 2. Backend queries Voyager API for all token transfers for the wallet 3. Compares with existing DB records by transaction hash 4. Saves new transfers to the `Transaction` table (read-through cache) 5. Returns `{ synced, total }` 6. Invalidates `useGetTransactionList` cache so new transfers appear immediately After sync, `useGetTransactionList` returns both SDK-initiated transfers AND external receives. ## When to Use * **On wallet load**: Sync once when user opens their wallet to discover any external receives * **Pull-to-refresh**: Let users manually trigger sync * **After receiving funds**: If user expects incoming funds from an external wallet Synced transfers are cached in the database. Subsequent `useGetTransactionList` calls return them without re-querying Voyager. ## Limitations * Only discovers token transfers (ERC-20). Contract interactions without transfers are not synced. * Voyager API rate limits apply. Avoid calling sync on every render. * First sync for a wallet with many transfers may take a few seconds (paginated, max 500 transfers per sync). ## Related * [useGetTransactionList](/sdk/nextjs/hooks/use-get-transaction-list) — Read the transaction list (includes synced transfers) * [useGetTransaction](/sdk/nextjs/hooks/use-get-transaction) — Get a single transaction by hash * [useGetTransactionStatus](/sdk/nextjs/hooks/use-get-transaction-status) — Poll transaction status # useTransfer Source: https://docs.chipipay.com/sdk/expo/hooks/use-transfer Transfers tokens from the user wallet to another address. Uses Avnus gasless to cover gas fees. ## Usage ```typescript theme={null} const { transfer, transferAsync, data, isLoading, isError, error, isSuccess, reset, } = useTransfer(); ``` ### Parameters * `transfer` / `transferAsync` both accept an object with: * `params` (`TransferHookInput`): * `encryptKey` (string): PIN used to decrypt the private key. * `wallet` (`WalletData`): the wallet object. **Pass the full object from `createWallet` / `useGetWallet`** — `publicKey`, `encryptedPrivateKey`, **`walletType`**, and **`signerKind`**. `walletType` is routing-critical: SHHH wallets execute through a different path, and omitting it defaults to `"READY"`, which **reverts** on a SHHH account. `signerKind` is SHHH-only (defaults to `"STARK"`). * `token` (`ChainToken`): Token identifier (e.g. `ChainToken.USDC`). * `usePasskey` (boolean, optional): When `true`, the hook authenticates with passkey internally (requires `externalUserId`). Do not use with external `setupPasskey`/`authenticate` calls. * `otherToken` (optional): Custom token configuration with: * `contractAddress` (string): ERC-20 token contract address. * `decimals` (number): Token decimals. * `recipient` (string): Destination wallet address. * `amount` (number): Transfer amount (will be converted to a string internally). * `bearerToken` (string): Bearer token for authentication (e.g. from your auth provider). ### Return Value Returns an object containing: * `transfer`: Function to trigger the transfer (fire-and-forget style). * `transferAsync`: Promise-based function that resolves with the transaction hash. * `data`: Transaction hash of the transfer (string | undefined). * `isLoading`: Boolean indicating if the operation is in progress. * `isError`: Boolean indicating if an error occurred. * `error`: Error instance when `isError` is true, otherwise `null`. * `isSuccess`: Boolean indicating if the transfer completed successfully. * `reset`: Function to reset the mutation state. ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useTransfer, ChainToken } from "@chipi-stack/chipi-react"; export function TransferForm() { const { getToken } = useAuth(); const { transferAsync, data, isLoading, isError, error } = useTransfer(); const [form, setForm] = useState({ pin: '', recipient: '', amount: '' }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken) { console.error('No bearer token available'); return; } try { await transferAsync({ params: { amount: Number(form.amount), encryptKey: form.pin, // Use the wallet you got from createWallet / useGetWallet, with its // walletType + signerKind. These route the transfer correctly — // omitting walletType defaults to "READY" and a SHHH wallet reverts. wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: wallet.walletType, // "SHHH" | "CHIPI" | "READY" signerKind: wallet.signerKind, // SHHH only; defaults to "STARK" }, token: ChainToken.USDC, recipient: form.recipient }, bearerToken, }); } catch (err) { console.error('Transfer failed:', err); } }; return (

Transfer Tokens

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, recipient: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, amount: e.target.value})} className="w-full p-2 border rounded-md" required />
{data && (

TX Hash: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always verify recipient addresses * Use encrypted private keys * Implement proper PIN validation * Monitor transaction status ## Error Handling * Handle insufficient token balance * Validate wallet addresses * Monitor gas fees * Implement retry logic for failed transactions Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Hooks * **useApprove** - Required before transferring new token types * **useCallAnyContract** - For custom contract interactions # useUpdateWalletEncryption Source: https://docs.chipipay.com/sdk/expo/hooks/use-update-wallet-encryption Updates a wallet's encrypted private key after the user changes their passkey or PIN. ## Usage ```typescript theme={null} const { updateWalletEncryption, updateWalletEncryptionAsync, data, isLoading, isError, error, isSuccess, reset, } = useUpdateWalletEncryption(); ``` ### Parameters The mutation accepts an object with: | Parameter | Type | Required | Description | | ------------------------ | -------- | -------- | --------------------------------------------------------------- | | `externalUserId` | `string` | Yes | Your app's user ID | | `newEncryptedPrivateKey` | `string` | Yes | The wallet private key re-encrypted with the new passkey or PIN | | `publicKey` | `string` | No | Wallet public key; required when the user has multiple wallets | | `bearerToken` | `string` | Yes | Auth token from your provider | ### Return Value | Property | Type | Description | | ----------------------------- | ---------------------------------------------------- | ------------------------------------ | | `updateWalletEncryption` | `(input) => void` | Fire-and-forget update | | `updateWalletEncryptionAsync` | `(input) => Promise` | Promise-based update | | `data` | `UpdateWalletEncryptionResponse \| undefined` | Response from the API | | `isLoading` | `boolean` | True while the update is in progress | | `isError` | `boolean` | True if the update failed | | `isSuccess` | `boolean` | True if the update succeeded | | `error` | `Error \| null` | Error details | | `reset` | `() => void` | Reset mutation state | ### `UpdateWalletEncryptionResponse` | Property | Type | Description | | --------- | --------- | ---------------------------- | | `success` | `boolean` | Whether the update succeeded | ## How It Works 1. The client re-encrypts the wallet private key with a new passkey or PIN 2. `updateWalletEncryption` sends the new ciphertext to the Chipi backend 3. The backend replaces the stored encrypted private key 4. The wallet query cache is automatically invalidated so subsequent reads return fresh data After calling this hook, any previous passkey or PIN used to encrypt the private key will no longer work. Make sure the new encryption is persisted before discarding the old key material. ## Example Implementation ```typescript theme={null} import { useUpdateWalletEncryption } from "@chipi-stack/chipi-react"; import { usePasskeySetup } from "@chipi-stack/chipi-passkey/hooks"; import { useAuth } from "@clerk/nextjs"; export function RotatePasskey({ wallet, userId }: { wallet: WalletData; userId: string; }) { const { getToken } = useAuth(); const { setupPasskey } = usePasskeySetup(); const { updateWalletEncryptionAsync, isLoading, isSuccess, error, } = useUpdateWalletEncryption(); const handleRotate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; try { // Create a new passkey (triggers biometric prompt). setupPasskey needs // userId + userName and returns an object — destructure encryptKey. const { encryptKey: newEncryptKey } = await setupPasskey(userId, userName); // Re-encrypt the private key client-side with the new key. There is no // `wallet.privateKey` — decrypt `wallet.encryptedPrivateKey` with the OLD // key first to recover the plaintext, then re-encrypt with the new one. // Keep plaintext key material in-memory only — never log or persist it. const plaintextKey = decrypt(wallet.encryptedPrivateKey, oldEncryptKey); const newEncryptedPrivateKey = encrypt( plaintextKey, newEncryptKey ); await updateWalletEncryptionAsync({ externalUserId: userId, newEncryptedPrivateKey, publicKey: wallet.publicKey, bearerToken, }); alert("Passkey rotated successfully!"); } catch (err) { console.error("Failed to rotate passkey:", err); } }; return (
{isSuccess &&

Encryption updated.

} {error &&

Error: {error.message}

}
); } ``` ## Related * [useMigrateWalletToPasskey](/sdk/react/hooks/use-migrate-wallet-to-passkey) — Migrate from PIN to passkey * [Use Passkeys Guide](/sdk/react/use-passkeys) — Full passkey setup walkthrough # useX402Payment Source: https://docs.chipipay.com/sdk/expo/hooks/use-x402-payment React hook for the x402 payment protocol. Wraps fetch with automatic HTTP 402 payment handling using USDC on Starknet. ## Usage ```typescript theme={null} const { x402Fetch, payFetch, signPayment, isPaying, lastTxHash, lastAmount, lastPayment, totalSpent, error, paymentCount, } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "1.00", }); ``` ### Configuration | Parameter | Type | Required | Description | | ------------------- | ------------------------------------------ | -------- | ----------------------------------------------------------------- | | `wallet` | `WalletData \| null` | Yes | Wallet data with `publicKey` and `encryptedPrivateKey` | | `encryptKey` | `string` | Yes | PIN or passkey-derived key for signing | | `getBearerToken` | `() => Promise` | No | Function returning auth token (for session key path) | | `bearerToken` | `string` | No | Static bearer token (alternative to `getBearerToken`) | | `maxPaymentAmount` | `string` | No | Max USDC per payment in human-readable format (e.g. `"1.00"`) | | `maxAmount` | `string` | No | Alias for `maxPaymentAmount` | | `allowedRecipients` | `string[]` | No | Whitelist of recipient addresses. Empty = allow all | | `session` | `SessionKeyData \| null` | No | Active session key for automatic payments without owner signature | | `onPaymentComplete` | `(txHash: string, amount: string) => void` | No | Callback fired after successful payment | ### Return Value | Property | Type | Description | | -------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `x402Fetch` | `(url: string, init?: RequestInit) => Promise` | Drop-in `fetch` replacement — auto-handles 402 | | `payFetch` | Same as `x402Fetch` | Alias | | `signPayment` | `(requirement: PaymentRequirement) => Promise` | Manual payment signing for custom flows | | `isPaying` | `boolean` | Whether a payment is in progress | | `lastTxHash` | `string \| null` | Last payment transaction hash (session payments only — standard wallet returns `null`) | | `lastAmount` | `string \| null` | Last payment amount in human-readable USDC | | `lastPayment` | `LastPaymentInfo \| null` | Last payment details: `{ txHash, amount, timestamp }` | | `totalSpent` | `string` | Total USDC spent this session (e.g. `"0.030000"`) | | `error` | `Error \| null` | Error from last payment attempt | | `paymentCount` | `number` | Total payments made this session | ## Example Implementation ```typescript theme={null} import { useX402Payment, ChainToken } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function PremiumApiClient({ wallet, pin }: { wallet: WalletData; pin: string }) { const { getToken } = useAuth(); const { x402Fetch, isPaying, lastAmount, paymentCount, totalSpent, error, } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "0.10", // Max 0.10 USDC per request onPaymentComplete: (txHash, amount) => { console.log(`Paid ${amount} USDC`); }, }); const handleQuery = async () => { // x402Fetch works like fetch — if the server returns 402, // the hook automatically signs and retries with X-PAYMENT header const response = await x402Fetch("https://api.example.com/premium/data"); const data = await response.json(); console.log("Response:", data); }; return (
{lastAmount &&

Last payment: {lastAmount} USDC

}

Total spent: {totalSpent} USDC ({paymentCount} payments)

{error &&

Error: {error.message}

}
); } ``` ## Payment Flows ### Standard Wallet (no session) 1. `x402Fetch` makes initial request 2. Server returns `402` with `PAYMENT-REQUIRED` header 3. Hook parses requirement, validates against `maxPaymentAmount` and `allowedRecipients` 4. Signs SNIP-12 typed data locally via `signTypedData()` (no on-chain transaction) 5. Retries with `X-PAYMENT` header containing the signed payload 6. Facilitator settles the payment on-chain ### Session Key (automatic payments) 1. Same flow, but signs using session key via `executeTransactionWithSession()` 2. Payment executes on-chain immediately (pre-signed) 3. `lastTxHash` contains the actual transaction hash 4. No owner signature needed — session key handles it ## Security * `maxPaymentAmount` enforced before signing — prevents overpayment * `allowedRecipients` whitelist — prevents payment to unauthorized addresses * Only `"exact"` scheme and `"starknet-mainnet"` network accepted * Only USDC asset accepted (native USDC contract address validated) * Amount validation uses BigInt math (no float precision issues) Standard wallet payments return `lastTxHash: null` because the signature is SNIP-12 off-chain — the facilitator settles on-chain later. Session payments return the actual transaction hash. # Expo SDK Introduction Source: https://docs.chipipay.com/sdk/expo/introduction Learn about the Expo SDK capabilities and what you can build with it # Expo SDK Our Expo SDK allows you to create self-custodial wallets and sign transactions without the need for gas in mobile applications. You can then use the Chipi Dashboard to manage your wallets and transactions, as well as see analytics on your transactions. The Chipi Expo SDK is designed specifically for mobile applications, focusing on gasless transactions and wallet management for React Native and Expo apps. ## ✨ Key Features ### Complete Wallet Suite * **Wallet Management** - Create and manage wallets * **Transaction Signing** - Sign transactions without the need for gas * **Social Login Integration** - Bring your own auth provider (Clerk, Firebase, etc.) * **Automatic Wallet Creation** - No manual key management ### Advanced Wallet Features * **Sessions** - Allow users to sign once and use their wallet for multiple transactions * **Biometrics** - Use biometrics to sign transactions (Touch ID, Face ID, fingerprint) * **Mobile Authentication** - Touch ID, Face ID, and PIN support * **MPC Support** - Coming soon! ### Developer Experience * **TypeScript Support** - Full type safety * **React Native Support** - Full RN compatibility * **Expo Integration** - Works with Expo managed workflow * **Quickstart** - We provide a quickstart guide to get you started ## 🏗️ Architecture Our Expo SDK is a simple wrapper around the Chipi API, optimized for mobile development. It is designed to be used with the Chipi Dashboard, which is a web-based dashboard for setting up your project. Simply follow the steps in the [Quickstart Guide](/sdk/expo/gasless-quickstart) to get started. If you wish to use an unlisted programming language, you can use the Chipi API directly. Send us a message on [Telegram](https://t.me/+e2qjHEOwImkyZDVh) and we will help you get started. ## 📱 Platform Support * **iOS** - Full feature support with Face ID/Touch ID * **Android** - Full feature support with fingerprint/face unlock * **Expo Go** - Compatible with Expo managed workflow * **Development Builds** - Custom native code support ## 🚦 Getting Started Ready to build? Start with our [Gasless Quickstart Guide](/sdk/expo/gasless-quickstart) to set up gasless transactions, or explore our other guides: * **[Gasless Clerk Setup](/sdk/expo/gasless-clerk-setup)** - Learn gasless transactions for mobile * **[Use Biometrics as a PIN](/sdk/expo/use-biometrics)** - Advanced mobile authentication ## 🔗 Resources * [Dashboard](https://dashboard.chipipay.com/) - Manage your account * [API Reference](/sdk/api/quickstart) - Complete API documentation * [GitHub](https://github.com/chipi-pay) - Source code and examples * [Support](https://t.me/+e2qjHEOwImkyZDVh) - Community help # Use Biometrics as a PIN Source: https://docs.chipipay.com/sdk/expo/use-biometrics Learn how to implement biometric authentication as a PIN alternative in your Expo app ## Overview Biometric authentication provides a secure and convenient way for users to authenticate without remembering PINs or passwords. The Chipi SDK supports fingerprint and face recognition on compatible devices. ## Prerequisites * Expo SDK 55 or later * Chipi SDK installed and configured * Device with biometric capabilities (fingerprint sensor or face recognition) ## Installation First, install the required dependencies: ```bash theme={null} npx expo install expo-local-authentication ``` ## Basic Implementation This is the minimal configuration required to enroll in and use biometrics instead of a PIN to sign transactions. ```typescript theme={null} await SecureStore.setItemAsync("wallet_pin", pin, { requireAuthentication: true, }); ``` ```typescript theme={null} import { useTransfer } from "@chipi-stack/chipi-expo"; import { ChainToken } from "@chipi-stack/chipi-expo"; const storedWallet = await SecureStore.getItemAsync("wallet"); const pin = await SecureStore.getItemAsync("wallet_pin",{ requireAuthentication: true, }); const token = await getToken(); if (!token) { setError("No bearer token found"); return; } const transferResponse = await transferAsync({ params: { encryptKey: pin, wallet: JSON.parse(storedWallet), token: ChainToken.USDC, recipient: recipientAddress, amount: Number(amount), }, bearerToken: token, }); ``` ## Example Here's a simple example of how to implement a secure transfer flow using biometric authentication: To use biometrics for authentication, you first need to create and securely store a wallet with a PIN (which can be protected by biometrics). Here’s an example wallet creation screen using the [`useCreateWallet`](/sdk/expo/hooks/use-create-wallet) hook: ```typescript theme={null} // Create Wallet Screen with biometric authentication import { useCreateWallet, Chain } from '@chipi-stack/chipi-expo'; import { useAuth, useUser } from '@clerk/clerk-expo'; import * as SecureStore from 'expo-secure-store'; import { useState } from 'react'; import { StyleSheet, Text, View, Alert, Linking } from 'react-native'; import { PrimaryButton } from '@/components/ui/PrimaryButton'; import { SimpleInput } from '@/components/ui/SimpleInput'; export const CreateWalletView = () => { const { getToken } = useAuth(); const { user } = useUser(); const [pin, setPin] = useState(''); const [error, setError] = useState(''); const { createWalletAsync, isLoading } = useCreateWallet(); const [walletData, setWalletData] = useState(null); const handleCreateWallet = async () => { try { setError(''); const token = await getToken(); if (!token) { setError('No bearer token found'); return; } const result = await createWalletAsync({ params: { encryptKey: pin, externalUserId: user?.id || '', chain: Chain.STARKNET, }, bearerToken: token, }); // `createWalletAsync` returns a flat GetWalletResponse. Wallet fields // (publicKey, normalizedPublicKey, etc.) live at the top level of // `result` — see types/src/wallet.ts: `CreateWalletResponse = GetWalletResponse`. await SecureStore.setItemAsync('wallet', JSON.stringify(result)); await SecureStore.setItemAsync('wallet_pin', pin, { requireAuthentication: true, }); setWalletData(result); Alert.alert('Success', 'Wallet successfully created!'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); setError('Error creating wallet: ' + errorMessage); } }; const openStarkscan = (address: string) => { const url = `https://starkscan.co/contract/${address}`; Linking.openURL(url); }; return ( Create New Wallet Set a PIN to secure your wallet {error ? {error} : null} = 4 && !isLoading} onPress={handleCreateWallet} /> {walletData && ( Wallet Details openStarkscan(walletData.publicKey)}> View Contract → Address: {walletData.publicKey} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, padding: 20, backgroundColor: '#fff', }, title: { fontSize: 28, fontWeight: 'bold', marginBottom: 8, color: '#11181C', }, subtitle: { fontSize: 16, color: '#687076', marginBottom: 24, }, errorText: { color: '#ff6b6b', fontSize: 14, marginTop: 8, }, walletDetails: { marginTop: 32, padding: 16, backgroundColor: '#f5f5f5', borderRadius: 8, }, detailHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, detailTitle: { fontSize: 18, fontWeight: '600', color: '#11181C', }, viewContract: { fontSize: 14, color: '#0a7ea4', fontWeight: '600', }, detailItem: { marginBottom: 12, }, detailLabel: { fontSize: 14, fontWeight: '600', color: '#687076', marginBottom: 4, }, detailValue: { fontSize: 14, color: '#11181C', fontFamily: 'monospace', }, }); ``` Use the [useTransfer](/sdk/expo/hooks/use-transfer) hook to send money to another wallet. Setting **requireAuthentication: true** will prompt for your biometric instead of your PIN. ```typescript theme={null} // Transfer example with biometric authentication import { useState } from 'react'; import { View, TextInput, TouchableOpacity, StyleSheet, Alert, Text } from 'react-native'; import { useTransfer } from '@chipi-stack/chipi-expo'; import { ChainToken } from '@chipi-stack/chipi-expo'; import { useAuth } from '@clerk/clerk-expo'; import { getPinStorage, getWalletStorage } from '@/utils/secureStorage'; import { ThemedText } from './themed-text'; export function TransferExample() { const { getToken } = useAuth(); const { transferAsync, isLoading, error } = useTransfer(); const [recipientAddress, setRecipientAddress] = useState(''); const [amount, setAmount] = useState(''); const handleTransfer = async () => { try { if (!recipientAddress || !amount) { Alert.alert('Error', 'Please enter recipient address and amount'); return; } // Get stored wallet and PIN (triggers biometric prompt) const storedWallet = await getWalletStorage(); const pin = await getPinStorage(); if (!storedWallet || !pin) { Alert.alert('Error', 'Wallet or PIN not found. Please create a wallet first.'); return; } // Get JWT token from Clerk const token = await getToken(); if (!token) { Alert.alert('Error', 'No bearer token found'); return; } // Execute transfer with updated API const transferResponse = await transferAsync({ params: { encryptKey: pin, wallet: JSON.parse(storedWallet), token: ChainToken.USDC, recipient: recipientAddress, amount: Number(amount), }, bearerToken: token, }); Alert.alert('Success', `Transfer completed! TX: ${transferResponse || 'Pending'}`); setRecipientAddress(''); setAmount(''); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); Alert.alert('Transfer Error', errorMessage); } }; return ( Recipient Address Amount (USDC) {isLoading ? 'Sending...' : 'Send USDC'} {error && ( Error: {error instanceof Error ? error.message : String(error)} )} ); } const styles = StyleSheet.create({ container: { gap: 16, marginTop: 12, }, inputContainer: { gap: 8, }, label: { fontSize: 14, fontWeight: '600', }, input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, fontSize: 16, }, button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center', marginTop: 8, }, buttonDisabled: { opacity: 0.5, }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600', }, errorText: { color: '#ff6b6b', fontSize: 14, marginTop: 8, }, }); ``` **Note:** This example assumes you have utility functions `getPinStorage()` and `getWalletStorage()` in your `@/utils/secureStorage` file. These functions should use `SecureStore.getItemAsync()` with `requireAuthentication: true` for the PIN to trigger biometric prompts. ## Useful resources * Making an EAS Build in [Expo](https://docs.expo.dev/build/introduction/) ## Next Steps Now that you have biometric authentication working, you can: * Integrate it with other Chipi SDK features * Add biometric authentication to wallet operations * Implement multi-factor authentication combining biometrics and PINs * Add biometric authentication to transaction signing For more information, check out the [Expo Local Authentication documentation](https://docs.expo.dev/versions/latest/sdk/local-authentication/). # Use Passkeys with PIN Backup Source: https://docs.chipipay.com/sdk/expo/use-passkeys Secure wallet authentication with Face ID / Touch ID on iOS and fingerprint on Android, with a mandatory PIN backup for recovery. ## Overview On mobile, passkeys work differently from browsers. Instead of WebAuthn, the Chipi Expo SDK uses: * **`expo-local-authentication`** — Face ID, Touch ID, and fingerprint gate * **`expo-secure-store`** — iOS Keychain / Android Keystore for protected key storage When you set `usePasskey: true`, the SDK generates a random encryption key, stores it in the device's secure enclave, and gates it behind biometric authentication. **Always pair a passkey with a PIN backup.** The Chipi Passkeys guide ([dual-key architecture](/sdk/guides/passkeys)) requires every passkey wallet to have a mandatory PIN backup so that biometric deletion, device reset, or OS-level passkey loss does not permanently lock the wallet. Pass the user's PIN as `encryptKey` alongside `usePasskey: true` — the SDK stores two encrypted copies of the private key: one unlocked by the biometric key, one unlocked by the PIN. Passkey-only mode (no `encryptKey`) is supported but labelled *not recommended* in the SDK source and should not be used in production. This is a native mobile implementation. For browser (Next.js / React), see [Use Passkeys (React)](/sdk/react/use-passkeys) which uses WebAuthn instead. ## Prerequisites * Expo SDK 55 or later * A physical iOS or Android device **with biometrics enrolled** (Face ID, Touch ID, or fingerprint) * A [development build](https://docs.expo.dev/develop/development-builds/introduction/) — biometrics are **not** supported in Expo Go The iOS Simulator does not support biometric authentication. You must test on a real device or configure the simulator to use device passcode fallback. ## Installation ```bash theme={null} npx expo install expo-local-authentication expo-secure-store ``` ## Configuration Add the `expo-local-authentication` plugin to your `app.json` to request Face ID permission on iOS: ```json theme={null} { "expo": { "plugins": [ [ "expo-local-authentication", { "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID to secure your wallet." } ] ] } } ``` If you skip `faceIDPermission`, Apple will reject your app during review and Face ID will fall back to device passcode on iOS without an explanation to the user. After updating `app.json`, rebuild your development client: ```bash theme={null} npx expo run:ios # or npx expo run:android ``` ## Usage ### Create a wallet with biometric passkey + PIN backup Pass `usePasskey: true`, an `externalUserId`, and the user's PIN as `encryptKey`. The SDK uses the biometric-derived key as the primary encryption key and stores a second copy of the private key encrypted with the PIN as a recovery fallback. ```typescript theme={null} import { useCreateWallet, Chain } from '@chipi-stack/chipi-expo'; import * as SecureStore from 'expo-secure-store'; import { useAuth } from '@clerk/clerk-expo'; import { useState } from 'react'; export function CreateWalletScreen() { const { getToken, userId } = useAuth(); const { createWalletAsync, isLoading, error } = useCreateWallet(); const [pin, setPin] = useState(''); const handleCreateWallet = async () => { const token = await getToken(); if (!token || !userId || pin.length < 4) return; // Face ID / Touch ID prompt is shown automatically. // `encryptKey: pin` wires up the PIN backup — see // chipi-expo/src/hooks/useCreateWallet.ts for the dual-key swap logic. const wallet = await createWalletAsync({ params: { usePasskey: true, externalUserId: userId, encryptKey: pin, chain: Chain.STARKNET, }, bearerToken: token, }); // `createWalletAsync` returns a flat `GetWalletResponse` (no nested // `wallet` field — see types/src/wallet.ts: `CreateWalletResponse = GetWalletResponse`). await SecureStore.setItemAsync('wallet', JSON.stringify(wallet)); }; return (/* your UI — collect the PIN, then call handleCreateWallet */); } ``` ### Sign a transaction (retrieve the key with biometrics) When you need the encryption key later (e.g. to sign a transfer), retrieve it from secure storage — this automatically triggers the Face ID / Touch ID prompt: ```typescript theme={null} import { useTransfer, getNativeWalletEncryptKey, ChainToken } from '@chipi-stack/chipi-expo'; import * as SecureStore from 'expo-secure-store'; import { useAuth } from '@clerk/clerk-expo'; export function TransferScreen() { const { getToken, userId } = useAuth(); const { transferAsync, isLoading } = useTransfer(); const handleTransfer = async () => { const token = await getToken(); if (!token || !userId) return; // Triggers Face ID / Touch ID automatically const encryptKey = await getNativeWalletEncryptKey(userId); if (!encryptKey) throw new Error('No wallet key found. Please re-create your wallet.'); const storedWallet = await SecureStore.getItemAsync('wallet'); if (!storedWallet) throw new Error('No wallet found.'); await transferAsync({ params: { encryptKey, wallet: JSON.parse(storedWallet), token: ChainToken.USDC, recipient: '0x...', amount: 1.0, }, bearerToken: token, }); }; return (/* your UI */); } ``` ### Use `usePasskey: true` directly in transaction hooks `useTransfer`, `useApprove`, and `useCallAnyContract` now use the Expo native passkey adapter automatically when wrapped with `@chipi-stack/chipi-expo`'s `ChipiProvider`. When you pass `usePasskey: true` to those hooks, also pass `externalUserId` (usually your auth provider user id), so the SDK can fetch the correct key from secure storage: ```typescript theme={null} await transferAsync({ params: { wallet, token: ChainToken.USDC, recipient: "0x...", amount: 1, usePasskey: true, externalUserId: userId, }, bearerToken: token, }); ``` ### Migrate an existing PIN wallet to biometrics If your users already have a PIN-based wallet, migrate them with one call: ```typescript theme={null} import { useMigrateWalletToPasskey } from '@chipi-stack/chipi-expo'; import * as SecureStore from 'expo-secure-store'; import { useAuth } from '@clerk/clerk-expo'; export function MigrateScreen({ currentPin }: { currentPin: string }) { const { getToken, userId } = useAuth(); const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey(); const handleMigrate = async () => { const token = await getToken(); const storedWallet = await SecureStore.getItemAsync('wallet'); if (!token || !userId || !storedWallet) return; // Face ID / Touch ID prompt is shown during migration const result = await migrateWalletToPasskeyAsync({ wallet: JSON.parse(storedWallet), oldEncryptKey: currentPin, externalUserId: userId, bearerToken: token, }); // `migrateWalletToPasskeyAsync` returns MigrateWalletToPasskeyResult, // a wrapped shape: { success: boolean, wallet: WalletData, credentialId: string }. // See chipi-react/src/hooks/useMigrateWalletToPasskey.ts for the type. // Replace the stored wallet with the re-encrypted inner `wallet` field: await SecureStore.setItemAsync('wallet', JSON.stringify(result.wallet)); // currentPin no longer works — biometrics are now required }; return (/* your UI */); } ``` After migration, the old PIN (`oldEncryptKey`) will no longer decrypt the wallet. Store the updated wallet object returned by `migrateWalletToPasskeyAsync` immediately. ## Full Example ```typescript theme={null} import { useCreateWallet, Chain } from '@chipi-stack/chipi-expo'; import type { GetWalletResponse } from '@chipi-stack/chipi-expo'; import * as SecureStore from 'expo-secure-store'; import { useAuth } from '@clerk/clerk-expo'; import { useState } from 'react'; import { View, Text, TextInput, TouchableOpacity, Alert, StyleSheet } from 'react-native'; export function CreateWalletWithPasskey() { const { getToken, userId } = useAuth(); const { createWalletAsync, isLoading, error } = useCreateWallet(); const [pin, setPin] = useState(''); const [wallet, setWallet] = useState(null); const handleCreate = async () => { try { const token = await getToken(); if (!token || !userId) { Alert.alert('Error', 'No auth token or user ID found.'); return; } if (pin.length < 4) { Alert.alert('Error', 'Please set a PIN (at least 4 digits) as your backup.'); return; } // Face ID / Touch ID prompt shown automatically. // `encryptKey: pin` wires the dual-key flow (biometric primary + PIN backup). const created = await createWalletAsync({ params: { usePasskey: true, externalUserId: userId, encryptKey: pin, chain: Chain.STARKNET, }, bearerToken: token, }); // `createWalletAsync` returns a flat GetWalletResponse — the wallet // fields (publicKey, normalizedPublicKey, etc.) live at the top level // of `created`, not nested under `created.wallet`. await SecureStore.setItemAsync('wallet', JSON.stringify(created)); setWallet(created); Alert.alert('Success', 'Wallet created — biometrics primary, PIN backup.'); } catch (err) { Alert.alert('Error', err instanceof Error ? err.message : String(err)); } }; return ( Create Wallet Secured with Face ID / Touch ID + PIN backup {isLoading ? 'Creating...' : 'Create Wallet with Biometrics + PIN'} {error && {error.message}} {wallet && ( {wallet.normalizedPublicKey ?? wallet.publicKey} )} ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 20 }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 }, subtitle: { color: '#687076', marginBottom: 24 }, pinInput: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, fontSize: 16, marginBottom: 16 }, button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center' }, buttonDisabled: { opacity: 0.5 }, buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 }, error: { color: '#ff3b30', marginTop: 12 }, address: { fontFamily: 'monospace', fontSize: 12, marginTop: 16, color: '#333' }, }); ``` ```typescript theme={null} import { useTransfer, getNativeWalletEncryptKey, ChainToken } from '@chipi-stack/chipi-expo'; import * as SecureStore from 'expo-secure-store'; import { useAuth } from '@clerk/clerk-expo'; import { useState } from 'react'; import { View, TextInput, TouchableOpacity, Alert, Text, StyleSheet } from 'react-native'; export function TransferWithPasskey() { const { getToken, userId } = useAuth(); const { transferAsync, isLoading } = useTransfer(); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); const handleTransfer = async () => { try { const token = await getToken(); if (!token || !userId) return; // Triggers Face ID / Touch ID — user authenticates to access the key const encryptKey = await getNativeWalletEncryptKey(userId); if (!encryptKey) { Alert.alert('Error', 'No wallet key found. Create a wallet first.'); return; } const storedWallet = await SecureStore.getItemAsync('wallet'); if (!storedWallet) { Alert.alert('Error', 'No wallet found.'); return; } const txHash = await transferAsync({ params: { encryptKey, wallet: JSON.parse(storedWallet), token: ChainToken.USDC, recipient, amount: Number(amount), }, bearerToken: token, }); Alert.alert('Success', `Transfer sent! TX: ${txHash}`); setRecipient(''); setAmount(''); } catch (err) { Alert.alert('Error', err instanceof Error ? err.message : String(err)); } }; return ( {isLoading ? 'Sending...' : 'Send USDC (Biometric Auth)'} ); } const styles = StyleSheet.create({ container: { gap: 12, padding: 20 }, input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, fontSize: 16 }, button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center' }, disabled: { opacity: 0.5 }, buttonText: { color: '#fff', fontWeight: '600' }, }); ``` ## How it Works | Step | What happens | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Wallet creation** | SDK generates a random 32-byte key, prompts Face ID / Touch ID, stores key in iOS Keychain / Android Keystore with biometric protection | | **Wallet use** | `getNativeWalletEncryptKey(userId)` retrieves the key — this automatically triggers the biometric prompt | | **Migration** | Old PIN decrypts the private key, new biometric key re-encrypts it | ## Security Notes * The encryption key is stored in the device's **secure enclave** (iOS Keychain / Android Keystore) * `requireAuthentication: true` means the key **cannot be read without biometric approval** * The key never leaves the device * If the user uninstalls the app or re-installs it, the key is lost — ensure your users understand wallet recovery options ## Utilities Reference | Export | Description | | --------------------------------------------- | ------------------------------------------------------------------ | | `isNativeBiometricSupported()` | Check if the device has enrolled biometrics | | `createNativeWalletPasskey(userId, userName)` | Manually create a passkey (called by `useCreateWallet` internally) | | `getNativeWalletEncryptKey(userId)` | Retrieve the stored key (triggers biometric prompt) | | `hasNativeWalletPasskey()` | Check if a passkey was already created on this device | | `removeNativeWalletPasskey(userId)` | Delete the stored key (destructive — use with caution) | | `getNativeWalletCredential()` | Get credential metadata (credentialId, userId, createdAt) | ## Related * [Use Biometrics as a PIN](/sdk/expo/use-biometrics) — Manual approach storing your own PIN with biometric protection * [useCreateWallet](/sdk/expo/hooks/use-create-wallet) * [useMigrateWalletToPasskey](/sdk/expo/hooks/use-migrate-wallet-to-passkey) # Connect with Chipi Source: https://docs.chipipay.com/sdk/guides/connect-with-chipi Let any Starknet dApp connect to a Chipi hosted smart-account wallet — passkey, gasless, no extension, no WalletConnect. The Argent/Ready "Web Wallet" model. `@chipi-stack/starknet-connector` is a [starknet-react](https://starknet-react.com) / [get-starknet](https://github.com/starknet-io/get-starknet) connector. A dApp adds **one line** and its users can connect with their Chipi wallet — a passkey smart-account that signs **gasless** through the paymaster. No browser extension, no WalletConnect; the same hosted-wallet model Argent/Ready's "Web Wallet" and Cartridge Controller use. This is how Starknet dApps actually connect — via the get-starknet / starknet-react connector interface, not WalletConnect (which is draft/unused on Starknet). ## Install ```bash theme={null} npm install @chipi-stack/starknet-connector ``` ## Add the connector ```tsx theme={null} import { StarknetConfig, publicProvider } from "@starknet-react/core"; import { mainnet } from "@starknet-react/chains"; import { ChipiConnector } from "@chipi-stack/starknet-connector"; const connectors = [ new ChipiConnector(), // "Connect with Chipi" // ...argent(), braavos(), etc. ]; export function Providers({ children }) { return ( {children} ); } ``` Your existing `useConnect` / `useAccount` flow now offers **Connect with Chipi**, and `account.signMessage(...)` / `account.execute(...)` route to the user's Chipi wallet — gasless, signed with their passkey. ## How it works ``` your dApp Chipi hosted wallet (wallet.chipipay.com) ───────── ────────────────────────────────────────── ChipiConnector.connect() ├─ opens a popup ──────────────► /connect (the user's authenticated wallet) └─ postMessage ◄────────────► approval UI (passkey + gasless paymaster) account.execute(calls) └─ wallet_addInvokeTransaction ─► user approves ─► sponsored tx ─► tx hash ``` 1. `ChipiConnector` is **transport only** — it opens the hosted wallet in a popup and forwards get-starknet `wallet_*` RPC calls over `postMessage`. It holds no keys and signs nothing itself. 2. The **hosted wallet** (`/connect`) authenticates the user, renders a **decoded approval** for every connect / sign / execute, then runs the real passkey + paymaster path and posts the result back. 3. `connector.account(provider)` wraps the channel in a `starknet.js` `WalletAccount`, so the dApp sees a standard `AccountInterface`. ## The approval & security model The hosted wallet — not the dApp — owns approval, and it decodes what's being signed so the user is never blind-signing: * **Connect** shows the dApp's origin + the address being shared, and the user must approve (never automatic). * **Execute** decodes calls against known tokens into human intent — *"Approve spending 10 USDC to 0x…"* with a loud **UNLIMITED** badge over the threshold, *"Send 5 USDC to 0x…"*, or an explicit *"Unrecognized contract"* warning (it never invents intent). Gas reads **"Free · covered by Chipi"**. * **signTypedData** decodes the SNIP-12 message into labeled fields and highlights spend-authorization keys (spender / amount / deadline). Typed data it can't read is **hard-blocked** — the user can't blind-sign it. (Of 2024's \~\$494M in reported wallet-drainer losses, 56.7% of phishing thefts used blind-signed `Permit`-style typed data; this discipline closes that vector.) ## Try the example A runnable starknet-react example lives in the repo: [`examples/connect-with-chipi`](https://github.com/chipi-pay/sdks/tree/main/examples/connect-with-chipi). It connects, signs typed data, and runs a gasless execute against a local Chipi wallet — the full round-trip. ## Options ```ts theme={null} new ChipiConnector({ walletUrl: "https://wallet.chipipay.com", // hosted wallet base URL (default) id: "chipi", name: "Connect with Chipi", icon: "data:image/svg+xml;base64,...", register: true, // self-register on window for scanning dApps (default) }); ``` Instantiating the connector also **self-registers** the wallet on `window` (`window.starknet_chipi` + `starknet:announceWallet`, the get-starknet / Starknet-Wallet-Standard discovery path) so dApps that *scan* for wallets find "Connect with Chipi" without importing this package — the same trick Cartridge Controller uses. Pass `register: false` to opt out; it's a no-op during SSR. ## Compatibility & limits * `@starknet-react/core` **>= 3** (peer) · `starknet` **>= 8.1.2** (peer). * **Starknet mainnet** — pass a mainnet `provider`. * **Popup-based** (external dApps). An in-app-browser ("parent") mode is in the transport but not yet exposed. * One approval at a time; a dApp that fires two prompts truly simultaneously has its second request rejected (most dApps serialize). # Custom Wallet Types Source: https://docs.chipipay.com/sdk/guides/custom-wallet-types Use custom StarkNet account implementations with the Chipi SDK. Bring your own class hash for advanced account features. ## Overview By default, the SDK creates **CHIPI** wallets (OpenZeppelin account with SNIP-9 session keys) or **READY** wallets (Argent X v0.4.0). You can also pass a custom `classHash` to deploy any StarkNet account that implements SNIP-9. ## When to Use Custom Class Hashes * Your project has a custom account contract with special features (spending limits, social recovery, multisig) * You need a specific audited account version (e.g., CHIPI v33 with spending policy) * You're building on top of an account standard not yet included in the SDK defaults ## Requirements Your custom account **must**: | Requirement | Standard | Why | | ------------------------------- | ------------------------------------- | --------------------------------------------------- | | Be declared on StarkNet mainnet | `starknet_getClass` returns the class | SDK computes the wallet address from the class hash | | Implement SRC-6 | `__validate__`, `__execute__` | Standard StarkNet account interface | | Implement SNIP-9 v2 | `execute_from_outside_v2` | Required for Chipi paymaster to sponsor gas | Accounts without SNIP-9 support cannot use the Chipi paymaster. Transactions will fail at estimation with `TRANSACTION_EXECUTION_ERROR`. ## Creating a Wallet with Custom Class Hash ```typescript Backend SDK theme={null} const wallet = await serverClient.createWallet({ params: { encryptKey: "user-pin", externalUserId: "user-123", chain: Chain.STARKNET, classHash: "0x0484bbd2404b3c7264bea271f7267d6d4004821ac7787a9eed7f472e79ef40d1", }, }); ``` ```python Python SDK theme={null} wallet = client.create_wallet( encrypt_key="user-pin", external_user_id="user-123", chain=Chain.STARKNET, class_hash="0x0484bbd2404b3c7264bea271f7267d6d4004821ac7787a9eed7f472e79ef40d1", ) ``` When `classHash` is provided: * The SDK uses your class hash instead of the default CHIPI or READY hash * The wallet address is computed from your class hash + constructor calldata * The Chipi paymaster sponsors the deployment (if your account supports SNIP-9) ## Known Compatible Class Hashes | Account | Version | Class Hash | Features | | ---------------- | ------- | -------------------- | --------------------------------- | | CHIPI (default) | v29 | `0x053f4f8...459f2a` | Session keys, passkeys, SNIP-9 v2 | | CHIPI | v33 | `0x0484bbd...f40d1` | + Spending policy, audit fixes | | READY (Argent X) | v0.4.0 | `0x036078...927f` | Argent X compatible, SNIP-9 v2 | ## Upgrading Custom Wallets Custom wallets can be upgraded to newer class hashes using the [wallet upgrade flow](/sdk/guides/wallet-upgrades): ```typescript theme={null} const upgrade = await serverClient.prepareWalletUpgrade({ publicKey: wallet.publicKey, targetClassHash: "0x", }); ``` ## Feature Compatibility | Feature | CHIPI | READY | Custom (SNIP-9) | | -------------------- | ----- | ----- | ------------------- | | Gasless transactions | Yes | Yes | Yes | | Session keys | Yes | No | Depends on contract | | Passkey auth | Yes | No | Depends on contract | | Wallet upgrade | Yes | Yes | Yes | | x402 payments | Yes | Yes | Yes | ## Related * [createWallet](/sdk/backend/methods/create-wallet) - API reference * [Wallet Upgrades](/sdk/guides/wallet-upgrades) - Upgrade between wallet types # Error Handling Source: https://docs.chipipay.com/sdk/guides/error-handling Handle errors gracefully with Chipi's typed error hierarchy and utility functions ## Error Hierarchy All Chipi SDK errors extend a base `ChipiError` class. Each error type carries specific context to help you handle failures precisely. ``` ChipiError (base) ├── ChipiApiError → HTTP errors (status code, response body) ├── ChipiAuthError → Authentication failures (401) ├── ChipiValidationError → Invalid input (400) ├── ChipiWalletError → Wallet operations (creation, encryption) ├── ChipiTransactionError → Transaction failures (signing, broadcasting) ├── ChipiSessionError → Session key errors (expired, revoked, not registered) └── ChipiSkuError → SKU/billing operations ``` ## Type Guards ### `isChipiError(error)` Check if an error is any Chipi SDK error: ```typescript theme={null} import { isChipiError } from "@chipi-stack/nextjs"; try { await createWalletAsync({ ... }); } catch (error) { if (isChipiError(error)) { // Typed as ChipiError - has .message, .code console.error("Chipi error:", error.message); } else { // Network error, timeout, etc. console.error("Unexpected error:", error); } } ``` ### `hasHttpStatus(error)` Check if an error has an HTTP status code. Useful for retry logic and status-specific handling: ```typescript theme={null} import { hasHttpStatus } from "@chipi-stack/nextjs"; try { await transferAsync({ ... }); } catch (error) { if (hasHttpStatus(error)) { switch (error.status) { case 401: // Token expired - refresh and retry await refreshToken(); break; case 429: // Rate limited - back off await delay(5000); break; case 500: // Server error - show retry button showRetryUI(); break; } } } ``` ## Error Patterns by Type ### API Errors ```typescript theme={null} import { ChipiApiError } from "@chipi-stack/nextjs"; try { await transferAsync({ ... }); } catch (error) { if (error instanceof ChipiApiError) { console.error(`API error ${error.status}: ${error.message}`); // error.status → HTTP status code (number) // error.message → Human-readable error message } } ``` ### Session Errors Session errors include a `.code` field for precise handling: ```typescript theme={null} import { ChipiSessionError } from "@chipi-stack/nextjs"; try { await executeWithSessionAsync({ ... }); } catch (error) { if (error instanceof ChipiSessionError) { switch (error.code) { case "SESSION_EXPIRED": // Session validUntil has passed await createNewSession(); break; case "SESSION_NOT_REGISTERED": // Session not found on-chain await registerSession(); break; case "SESSION_REVOKED": // Session was explicitly revoked await createNewSession(); break; case "SESSION_MAX_CALLS_EXCEEDED": // remainingCalls reached 0 await createNewSession(); break; case "SESSION_ENTRYPOINT_NOT_ALLOWED": // Function not in allowedEntrypoints whitelist console.error("This action is not permitted by the session"); break; case "SESSION_DECRYPTION_FAILED": // Wrong PIN/encryptKey promptForPin(); break; case "INVALID_WALLET_TYPE_FOR_SESSION": // Wallet is READY, not CHIPI console.error("Session keys require a CHIPI wallet"); break; } } } ``` ### Session Error Codes Reference | Code | Meaning | Recovery | | --------------------------------- | --------------------------------- | ------------------------------------------- | | `SESSION_EXPIRED` | `validUntil` timestamp has passed | Create and register a new session | | `SESSION_NOT_REGISTERED` | Session key not found on-chain | Call `addSessionKeyToContract` | | `SESSION_REVOKED` | Session was explicitly revoked | Create a new session | | `SESSION_MAX_CALLS_EXCEEDED` | `remainingCalls` reached 0 | Create a new session with higher `maxCalls` | | `SESSION_ENTRYPOINT_NOT_ALLOWED` | Called function not in whitelist | Register with correct `allowedEntrypoints` | | `SESSION_DECRYPTION_FAILED` | Wrong PIN for session private key | Prompt user to re-enter PIN | | `SESSION_CREATION_FAILED` | Keypair generation error | Retry the operation | | `SESSION_INVALID_RESPONSE` | Malformed contract response | Check wallet type and contract state | | `INVALID_WALLET_TYPE_FOR_SESSION` | Wallet is READY, not CHIPI | Use a CHIPI wallet for sessions | ### Wallet Errors ```typescript theme={null} import { ChipiWalletError } from "@chipi-stack/nextjs"; try { await createWalletAsync({ ... }); } catch (error) { if (error instanceof ChipiWalletError) { // Wallet already exists, encryption failed, etc. console.error("Wallet error:", error.message); } } ``` ### Auth Errors ```typescript theme={null} import { ChipiAuthError } from "@chipi-stack/nextjs"; try { await transferAsync({ ... }); } catch (error) { if (error instanceof ChipiAuthError) { // 401 - bearer token invalid or expired // Refresh your auth token and retry const newToken = await refreshAuthToken(); await transferAsync({ ...params, bearerToken: newToken }); } } ``` ## Common Pattern: Unified Error Handler ```typescript theme={null} import { isChipiError, hasHttpStatus, ChipiSessionError, ChipiAuthError } from "@chipi-stack/nextjs"; function handleChipiError(error: unknown): string { if (error instanceof ChipiAuthError) { return "Your session has expired. Please sign in again."; } if (error instanceof ChipiSessionError) { if (error.code === "SESSION_EXPIRED" || error.code === "SESSION_MAX_CALLS_EXCEEDED") { return "Your session has expired. Creating a new one..."; } return `Session error: ${error.message}`; } if (hasHttpStatus(error) && error.status === 429) { return "Too many requests. Please wait a moment."; } if (isChipiError(error)) { return error.message; } return "Something went wrong. Please try again."; } ``` ## Related * [Session Keys Guide](/sdk/guides/session-keys) - Full session key lifecycle * [Passkeys Guide](/sdk/guides/passkeys) - Biometric authentication * [Backend Methods](/sdk/backend/methods/sessions) - Server-side error handling Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support. # Multisig Governance Source: https://docs.chipipay.com/sdk/guides/multisig-governance Propose, approve, and execute any on-chain action through an N-of-M SHHH multisig — from your own app. A SHHH multisig wallet executes a call only after **N of its M owners** sign it. This guide shows how to drive that flow — **propose → approve → execute** — from your own application, using two packages: | Package | What it gives you | | ----------------------------------- | ------------------------------------------------------------------------- | | `@chipi-stack/backend` | `sdk.treasury` — the typed coordination client (the transport). | | `@chipi-stack/chipi-react/multisig` | Action templates + the `useProposeAction` / `useTreasuryProposals` hooks. | The multisig endpoints live in your Chipi backend at `/v1/treasuries/:treasuryId/...`. You drive them with **your own API keys** — no Chipi dashboard required. ## How authorization works Each proposal carries an **OutsideExecution** (the `Call[]` to run) plus, per owner, that owner's signature over the execution hash. The coordination backend splits auth by step: | Step | Auth | Where to call it | | ------------------ | -------------------------------------------------- | ----------------------------------------------------------- | | `propose` / `sign` | **`pk_`** (public key) + the **owner's signature** | browser-safe — the signature *is* the authorization | | `execute` | **`sk_`** (secret key) | a server — it relays through the paymaster and meters usage | | `list` | **`sk_`** | a server — reads are private to your org | Because `propose`/`sign` need no secret, an owner can sign in the browser and the key never leaves their device. `execute`/`list` use your `sk_`, so they belong on a server (a Next.js route handler, your backend) — never in client code. ## 1. Define the actions owners can propose An `ActionTemplate` turns typed form values into the contract `Call[]` and a human title. Use the built-in `voteTemplate`, or define your own with `defineActions`: ```ts lib/actions.ts theme={null} import { defineActions, voteTemplate, toFelt, u256, amountToBase, type ActionTemplate, } from "@chipi-stack/chipi-react/multisig"; // A custom action: borrow a USDC amount (6 decimals, u256) from a lending pool. const borrow: ActionTemplate<{ poolId: string; amount: string }> = { key: "borrow", label: "Borrow from pool", fields: [ { name: "amount", label: "Amount", type: "amount", decimals: 6 }, ], toCalls: (v) => [ { to: "0x0POOL", entrypoint: "borrow", // felt-encode the pool id; convert the human amount to base units and // split into the [low, high] felts a Cairo u256 expects. calldata: [toFelt(v.poolId), ...u256(amountToBase(v.amount, 6))], }, ], title: (v) => `Borrow ${v.amount} from pool ${v.poolId}`, meta: (v) => ({ poolId: v.poolId }), // optional integrator context }; export const actions = defineActions([ voteTemplate({ governanceContract: "0x0DA0" }), borrow, ]); ``` `defineActions` validates that every `key` is unique. `meta` is optional context your approver UI can render (e.g. `{ poolId }`). ## 2. Implement a signer A `MultisigSigner` produces an owner's inner envelope for a given execution hash — the key never leaves it — and resolves that owner's index in the on-chain owner set. The `sign` callback receives `{ messageHash, ownerIndex }` and returns the envelope felts (`bigint[]`). For a **STARK** owner (e.g. a programmatic/agent signer), `@chipi-stack/backend` builds the envelope for you: ```ts lib/signer.ts theme={null} import { buildStarkEnvelope } from "@chipi-stack/backend"; import { ec } from "starknet"; import type { MultisigSigner } from "@chipi-stack/chipi-react/multisig"; const privateKey = process.env.OWNER_STARK_PRIVATE_KEY!; export const signer: MultisigSigner = { signerKind: "STARK", proposerOwnerIndex: 0, // the index this device proposes/signs as sign: ({ messageHash, ownerIndex }) => buildStarkEnvelope({ privateKey, messageHash, ownerIndex }), resolveOwnerIndex: async () => 0, }; // The owner's public key — passed to the transport so the backend can verify // the signature commits to a current on-chain owner. export const ownerPubkey = ec.starkCurve.getStarkKey(privateKey); ``` For a **passkey** owner (`WEBAUTHN_P256`), the device's Face ID / Touch ID produces the assertion in the browser — the private key never leaves the secure enclave. `@chipi-stack/chipi-passkey` prompts for the assertion and `@chipi-stack/backend` packs it into the envelope: ```ts lib/passkey-signer.ts theme={null} "use client"; import { signShhhMessage, getStoredShhhCredential } from "@chipi-stack/chipi-passkey"; import { buildWebAuthnEnvelopeFromAssertion, webauthnOwnerPubkeyParam, } from "@chipi-stack/backend"; import type { MultisigSigner } from "@chipi-stack/chipi-react/multisig"; const hexToBytes = (h: string) => { const s = h.replace(/^0x/, ""); const u = new Uint8Array(s.length / 2); for (let i = 0; i < u.length; i++) u[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16); return u; }; // The passkey registered for this device (see `createShhhPasskey`). Its stored // P-256 public key is what the owner committed on chain. const cred = getStoredShhhCredential(); if (!cred) throw new Error("No SHHH passkey on this device — register one first."); const pubkey = { x: hexToBytes(cred.publicKey.x), y: hexToBytes(cred.publicKey.y) }; export const signer: MultisigSigner = { signerKind: "WEBAUTHN_P256", proposerOwnerIndex: 0, sign: async ({ messageHash, ownerIndex }) => { // Prompts Face ID / Touch ID; the OE hash is bound as the WebAuthn challenge. const a = await signShhhMessage({ messageHash }); return buildWebAuthnEnvelopeFromAssertion({ authenticatorData: a.authenticatorData, clientDataJSON: a.clientDataJSON, signatureDer: a.signatureDer, pubkey, messageHash, ownerIndex, }); }, // Map this device's passkey to its index in the on-chain owner set. resolveOwnerIndex: async () => 0, }; // The P-256 point as `"0x,0x"` — passed to the transport so the backend // can verify the signature commits to a current on-chain owner. export const ownerPubkey = webauthnOwnerPubkeyParam(pubkey); ``` For a **MetaMask** owner (`EIP191_SECP256K1`), the wallet's `personal_sign` produces the signature — MetaMask prefixes and keccak-hashes internally, and the on-chain verifier reproduces the same recipe. The wallet must sign the exact 32 big-endian bytes of the execution hash: ```ts lib/metamask-signer.ts theme={null} "use client"; import { buildEip191EnvelopeFromSignature, eip191OwnerPubkeyParam, type Eip191Pubkey, } from "@chipi-stack/backend"; import type { MultisigSigner } from "@chipi-stack/chipi-react/multisig"; // The wallet's uncompressed secp256k1 pubkey (x, y as 32 BE bytes each) — // recover it once from a signature at onboarding/claim time, or load it from // where you stored it. declare const pubkey: Eip191Pubkey; declare const address: string; // the connected MetaMask account export const signer: MultisigSigner = { signerKind: "EIP191_SECP256K1", proposerOwnerIndex: 0, sign: async ({ messageHash, ownerIndex }) => { // personal_sign over the 32-byte BE encoding of the SNIP-12 hash. const msgHex = "0x" + messageHash.toString(16).padStart(64, "0"); const signatureHex = (await window.ethereum.request({ method: "personal_sign", params: [msgHex, address], })) as string; return buildEip191EnvelopeFromSignature({ signatureHex, pubkey, ownerIndex }); }, resolveOwnerIndex: async () => 0, }; // `"0x,0x"` — the FULL coordinates, not the 4-felt `eip191PubkeyFelts` form. export const ownerPubkey = eip191OwnerPubkeyParam(pubkey); ``` For a **Phantom** owner (`ED25519`), the wallet signs the 64-byte hex-ASCII encoding of the hash (`ed25519SignedBytes`) — that's what the user sees in the popup, and what the on-chain verifier reconstructs: ```ts lib/phantom-signer.ts theme={null} "use client"; import { buildEd25519EnvelopeFromSignature, ed25519OwnerPubkeyParam, ed25519SignedBytes, } from "@chipi-stack/backend"; import type { MultisigSigner } from "@chipi-stack/chipi-react/multisig"; await window.solana.connect(); const publicKey: Uint8Array = window.solana.publicKey.toBytes(); // 32 bytes export const signer: MultisigSigner = { signerKind: "ED25519", proposerOwnerIndex: 0, sign: async ({ messageHash, ownerIndex }) => { // Phantom MUST sign ed25519SignedBytes(messageHash) — the hex-ASCII // encoding — never the raw 32-byte hash. const msg = ed25519SignedBytes(messageHash); const { signature } = await window.solana.signMessage(msg); // Async: initializes the Garaga WASM hint builder on first call. return buildEd25519EnvelopeFromSignature({ signature, publicKey, messageHash, ownerIndex }); }, resolveOwnerIndex: async () => 0, }; // `"0x,0x"` — the LE u128 halves of the 32-byte key. export const ownerPubkey = ed25519OwnerPubkeyParam(publicKey); ``` **Helper names below are wrong for 14.8.** There are no `*OwnerPubkeyParam` exports — the shipped helpers are `webauthnPubkeyFelts` / `eip191PubkeyFelts` / `ed25519PubkeyFelts` / `starkPubkeyFelts` (from `@chipi-stack/backend`), each returning `bigint[]`. For the transport's `ownerPubkey`, STARK is a single 0x-hex felt; coordinate kinds derive from the corresponding `*PubkeyFelts`. This section needs a smoke-tested rewrite (tracked: docs audit 2026-06-09) — don't copy the `*OwnerPubkeyParam` calls verbatim. All four launch signer kinds — **STARK**, **WEBAUTHN\_P256** (passkey), **EIP191\_SECP256K1** (MetaMask), and **ED25519** (Phantom) — are verified live on `propose` / `sign`. ## 3. The transport `sdk.treasury.forTreasury(...)` returns a transport bound to one treasury and one owner's public key. It implements exactly the contract the React hooks expect (`MultisigTransport`), so you can wire it straight in — or call its methods directly from a server. ```ts lib/chipi.ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; export const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, // sk_ — server only }); export const transport = (treasuryId: string, ownerPubkey: string) => sdk.treasury.forTreasury({ treasuryId, ownerPubkey }); ``` ## 4. Drop-in components **Not shipped in 14.8.** ``, ``, and `` are NOT exported by `@chipi-stack/chipi-react/multisig` today — only the hooks (`useProposeAction`, `useTreasuryProposals`) and helpers ship. The component examples below are roadmap; build your UI on the hooks for now. (Tracked: docs audit 2026-06-09.) If you don't want to build UI on the hooks, three embeddable components ship in the same `multisig` subpath — themeable, mobile-first, no CSS imports or Tailwind required (a namespaced stylesheet injects at runtime): ```tsx theme={null} import { ProposeAction, Approvals, ApprovalInbox, } from "@chipi-stack/chipi-react/multisig"; // The propose dialog: typed fields from your templates + a live // "What this does" preview. One signer prompt on submit. console.log("proposed", p.id)} /> // The approvals list: Approve / Execute per row, your business context // injected from the proposal's persisted `meta`. } explorerTxUrl={(tx) => `https://starkscan.co/tx/${tx}`} /> // The founder surface: cross-treasury "what needs me", one-tap approve. ``` Theme them to match your app with CSS-var tokens (defaults are Chipi's neobrutalism): ```tsx theme={null} ``` `` resolves the device's owner index via your signer's `resolveOwnerIndex`; on a device that isn't an owner it degrades to read-only. Stamp `meta` in your template (`meta: (v) => ({ poolId: v.poolId })`) and it round-trips through the backend to every approver's `renderContext`. ## 5. Risk tiers & lifecycle webhooks ### Risk tiers (per-action quorum) Mark a template's tier once and configure per-tier quorums on the treasury — routine actions (frequent, low-stakes) clear with fewer signatures than sensitive ones: ```ts theme={null} { key: "borrow", label: "Borrow", risk: "ROUTINE", /* … */ }, { key: "set_fees", label: "Set fees", risk: "SENSITIVE", /* … */ }, ``` When the treasury has the matching quorum configured (dashboard → treasury policy, or `PATCH /v1/organizations/:id/treasuries/:treasuryId/policy`), the backend **overrides** `requiredApprovals` at propose — the client's value is advisory. Honest scope: this is app-level policy for human treasuries. The wallet's on-chain threshold remains the hard floor at execute, and agent autonomy is never gated on this layer (contract-enforced caps are the agent tier). ### Lifecycle webhooks Register a webhook on the API key your treasury's wallet was created under (the standard Chipi `/webhooks` registration) and subscribe to the governance events — or leave the events list empty to receive everything: | Event | When | | ------------------------------ | ---------------------------------------------------------------------------------- | | `treasury.proposal.created` | a proposal lands | | `treasury.proposal.approved` | each accepted signature | | `treasury.proposal.executable` | the quorum is met — react with `execute` from your server (`sk_`) for auto-execute | | `treasury.proposal.executed` | the relay succeeded (`executedTxHash` set) | Payload: `{ event, data: { treasuryId, proposalId, title, meta, risk, approvals: { have, need }, status, executedTxHash }, ts }` — `meta` is your template's stamped context, so you can route the notification without a read-back. Verify the `chipi-signature` header the same way as every Chipi webhook (HMAC-SHA256 of the raw body with your `whsec-…` signing key): ```ts theme={null} import { createHmac, timingSafeEqual } from "crypto"; function verify(rawBody: string, signature: string, signingKey: string): boolean { const expected = createHmac("sha256", signingKey).update(rawBody).digest("hex"); return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex")); } ``` Delivery is at-least-once with a single retry — make your handler idempotent on `(event, proposalId)`. ## 6. AI agents as constrained proposers An AI agent is just **one more owner of the multisig** — a constrained one. Add its STARK key as an owner, register it with a **daily budget**, and the coordination layer treats that budget as a dynamic approval rule: * An **in-budget USDC payment** the agent proposes auto-approves (quorum 1) → the `treasury.proposal.executable` webhook fires → your server executes it. The agent acts alone, within its allowance. * **Over budget**, a **non-USDC** payment, or **any contract action** escalates to the human threshold and lands in `` as *"Vela wants: Move \$500 to Endur"*. Register the agent (dashboard → treasury → agents, or the API). The default budget is **`0`** — the agent can't move a cent until you raise it: ```bash theme={null} POST /v1/organizations/:orgId/treasuries/:treasuryId/agents { "ownerIndex": 2, "label": "Vela", "dailyBudgetBase": "50000000" } # 50 USDC (6 dp) ``` The agent proposes exactly like any owner — a STARK `MultisigSigner` at its owner index: ```ts agent-loop.ts theme={null} const proposal = await chipi.treasury .forTreasury({ treasuryId, ownerPubkey: agentStarkPubkey }) .propose(await createProposal({ template: payTemplate, values: { to: endur, amount: "5" }, walletAddress: TREASURY, requiredApprovals: humanThreshold, // advisory — the budget decides signer: agentStarkSigner, // owner index 2 })); // In budget → proposal.status === "APPROVED"; your webhook handler executes it. // Over budget → "PROPOSED"; it waits for a human in . ``` In the inbox, mark agent rows so the founder sees whose action it is: ```tsx theme={null} p.meta?.agent === "vela", // you stamp this in the template's meta agentLabel: "Vela", }]} /> ``` Daily budgets are **app-level** policy: they make a misbehaving agent **loud** (escalation), not impossible. A valid agent signature on an in-budget payment executes without a human. For hard, on-chain-enforced spending caps — the real safety boundary for autonomous agents — use session-key policies (coming in a future release). Size the budget to what you'd let the agent move unattended. ## Next.js quickstart The browser can run `propose`/`sign` (pk\_ + owner signature) directly, but `list`/`execute` need `sk_`, so they go through **your** route handlers. The signer always runs client-side — the key never reaches your server. ### Route handlers (server, `sk_`) ```ts app/api/treasury/[id]/proposals/route.ts theme={null} import { NextRequest, NextResponse } from "next/server"; import { sdk } from "@/lib/chipi"; // GET — list proposals (sk_, server-only) export async function GET(_req: NextRequest, { params }: { params: { id: string } }) { // ownerPubkey is irrelevant for reads; any valid value is fine here. const t = sdk.treasury.forTreasury({ treasuryId: params.id, ownerPubkey: "0x0" }); return NextResponse.json(await t.list()); } ``` ```ts app/api/treasury/[id]/transactions/[txId]/execute/route.ts theme={null} import { NextRequest, NextResponse } from "next/server"; import { sdk } from "@/lib/chipi"; // POST — relay the assembled threshold calldata through the paymaster (sk_) export async function POST(req: NextRequest, { params }: { params: { id: string; txId: string } }) { const { calldata } = await req.json(); const t = sdk.treasury.forTreasury({ treasuryId: params.id, ownerPubkey: "0x0" }); return NextResponse.json(await t.execute(params.txId, calldata)); } ``` ### Client component (browser-direct `propose`/`sign`, proxied `list`/`execute`) ```tsx app/treasury/[id]/page.tsx theme={null} "use client"; import { ChipiBrowserSDK } from "@chipi-stack/backend"; import { useProposeAction, useTreasuryProposals, type MultisigTransport, } from "@chipi-stack/chipi-react/multisig"; import { RpcProvider } from "starknet"; import { actions } from "@/lib/actions"; import { signer, ownerPubkey } from "@/lib/signer"; const browser = new ChipiBrowserSDK({ apiPublicKey: process.env.NEXT_PUBLIC_CHIPI_PUBLIC_KEY! }); const rpc = new RpcProvider({ nodeUrl: process.env.NEXT_PUBLIC_STARKNET_RPC! }); export default function TreasuryPage({ params }: { params: { id: string } }) { const treasuryId = params.id; const walletAddress = "0x0WALLET"; // the treasury's on-chain address // propose / sign go straight to Chipi (pk_ + owner sig); list / execute proxy // through our own routes so the sk_ stays on the server. const direct = browser.treasury.forTreasury({ treasuryId, ownerPubkey }); const transport: MultisigTransport = { propose: direct.propose, sign: direct.sign, list: () => fetch(`/api/treasury/${treasuryId}/proposals`).then((r) => r.json()), execute: (txId, calldata) => fetch(`/api/treasury/${treasuryId}/transactions/${txId}/execute`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ calldata }), }).then((r) => r.json()), }; // The wallet's threshold (N) — read from chain so the package stays chain-agnostic. const getRequiredApprovals = async () => { const [n] = await rpc.callContract({ contractAddress: walletAddress, entrypoint: "threshold", calldata: [], }); return Number(BigInt(n)); }; const propose = useProposeAction({ walletAddress, transport, signer, getRequiredApprovals }); const { proposals, approve, execute } = useTreasuryProposals({ walletAddress, transport, signer }); return (
{proposals.data?.items.map((p) => (
{p.title} — {p.signatures.length}/{p.requiredApprovals} — {p.status} {p.status === "APPROVED" && }
))}
); } ``` That's the whole loop: `propose.mutate({ template, values })` builds the calls, signs the OE as the proposer, and submits it; `approve.mutate(proposal)` adds another owner's signature; once `signatures.length >= requiredApprovals` the proposal is `APPROVED` and `execute.mutate(proposal)` relays it on-chain. ## Server-only (programmatic) flow For an agent or backend with no UI, skip the hooks and call the pure helpers with the same transport — every owner is a server-held key: ```ts theme={null} import { createProposal, buildApproval, buildExecuteCalldata } from "@chipi-stack/chipi-react/multisig"; import { sdk } from "./lib/chipi"; import { signer, ownerPubkey } from "./lib/signer"; import { actions } from "./lib/actions"; const t = sdk.treasury.forTreasury({ treasuryId, ownerPubkey }); // sk_ available → all methods work const body = await createProposal({ template: actions[0], values: { contract: "", proposalId: "5", choice: "0x1" }, walletAddress, requiredApprovals: 2, signer, }); const proposal = await t.propose(body); // …collect a second owner's approval (their own signer)… const approval = await buildApproval({ proposal, walletAddress, signer: secondOwnerSigner }); await t.sign(proposal.id, approval); // once enough signatures are collected, re-fetch and execute: const { items } = await t.list(); const ready = items.find((p) => p.id === proposal.id)!; await t.execute(ready.id, buildExecuteCalldata(ready)); ``` ## Notes * **Free executes are metered.** `sdk.treasury.forTreasury(...).getMultisigAllowance()` returns the org's remaining free executes before you build a proposal. * **`amount` for non-payment actions.** Templates that aren't token payments (a vote, a borrow) carry the intent in `calls` + `title`; the payment fields are placeholders. * **The chain is the source of truth.** Off-chain signature verification stops a public `pk_` from spamming forged proposals, but the on-chain dispatcher is the authoritative gate at `execute` — a proposal without a valid N-of-M envelope simply reverts. # On-Chain Verification Source: https://docs.chipipay.com/sdk/guides/on-chain-verification Verify that token transfers actually happened on-chain by checking Transfer events. A transaction can succeed while the inner transfer fails silently. ## Why Verify Events? On StarkNet, a transaction can have status **"SUCCEEDED"** while the token transfer inside it **fails silently**. This happens because: 1. The Chipi paymaster wraps your calls in `execute_from_outside_v2` 2. The forwarder contract executes the wrapped call 3. If the inner call fails (e.g., insufficient balance), the forwarder may still succeed 4. The transaction is marked "SUCCEEDED" on explorers **Always verify Transfer events** to confirm tokens actually moved. ## Checking Transfer Events via RPC After a transfer, query the transaction receipt for Transfer events from the token contract: ```typescript theme={null} const provider = new RpcProvider({ nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY", }); const receipt = await provider.getTransactionReceipt(txHash); if (receipt.execution_status !== "SUCCEEDED") { throw new Error(`Transaction failed: ${receipt.execution_status}`); } // Transfer event key = starknet_keccak("Transfer") const TRANSFER_KEY = "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"; const USDC_ADDRESS = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb"; const transferEvent = receipt.events.find((event) => { const isUsdc = BigInt(event.from_address) === BigInt(USDC_ADDRESS); const isTransfer = event.keys[0] === TRANSFER_KEY; return isUsdc && isTransfer; }); if (!transferEvent) { throw new Error("No USDC Transfer event found — transfer failed silently"); } // StarkNet Transfer events use indexed parameters (keys): // keys[0] = Transfer selector // keys[1] = from address // keys[2] = to address // data[0] = amount (low 128 bits) // data[1] = amount (high 128 bits) const from = transferEvent.keys[1]; const to = transferEvent.keys[2]; const amountLow = BigInt(transferEvent.data[0]); const amountHigh = BigInt(transferEvent.data[1]); const amount = amountLow + (amountHigh << 128n); console.log(`Verified: ${amount} units from ${from} to ${to}`); ``` ## Checking Balance Before and After For additional certainty, query the token balance before and after the transfer: ```typescript theme={null} const balanceBefore = await getBalance(provider, tokenAddress, recipientAddress); const txHash = await serverClient.transfer({ params: { ... } }); // Wait for the tx to be included in a block await provider.waitForTransaction(txHash); const balanceAfter = await getBalance(provider, tokenAddress, recipientAddress); if (balanceAfter <= balanceBefore) { throw new Error("Balance did not increase — transfer may have failed"); } async function getBalance(provider, token, account) { const result = await provider.callContract({ contractAddress: token, entrypoint: "balance_of", calldata: [account], }); return BigInt(result[0]) + (BigInt(result[1]) << 128n); } ``` ## Using getTransactionStatus for Polling The SDK's `getTransactionStatus` method polls the chain for finality, but it only checks **transaction inclusion** — not whether the inner transfer succeeded: ```typescript theme={null} // This tells you the tx is on-chain, NOT that tokens moved const status = await serverClient.getTransactionStatus({ hash: txHash }); // status.onChainStatus: "ACCEPTED_ON_L2" | "ACCEPTED_ON_L1" | "REJECTED" | ... ``` Use `getTransactionStatus` for finality, then verify Transfer events for correctness. ## Common Pitfalls | Pitfall | What Happens | How to Detect | | ----------------------------------------- | ------------------------------ | ------------------------------------------- | | Insufficient token balance | Tx succeeds, no Transfer event | Check events in receipt | | Wrong recipient address | Tokens sent to wrong address | Verify `keys[2]` matches expected recipient | | Amount overflow/underflow | Transfer of 0 tokens | Check `data[0]` and `data[1]` are non-zero | | Tx status "SUCCEEDED" but transfer failed | Forwarder catches inner error | Always check Transfer events | ## Related * [getTransactionStatus](/sdk/backend/methods/get-transaction-status) - Poll transaction finality * [transfer](/sdk/backend/methods/transfer) - Send tokens # Passkeys Source: https://docs.chipipay.com/sdk/guides/passkeys Biometric authentication with automatic PIN backup for wallet security ## What Are Passkeys? Passkeys use biometric authentication (Face ID, Touch ID, Windows Hello) to protect wallets. Instead of a PIN, a secure encryption key is derived from the biometric prompt. No passwords to remember, no PINs to leak. **Dual-key architecture:** Every passkey wallet also has a mandatory PIN backup. If biometrics fail (browser change, device reset), the PIN recovers the wallet. Two independent keys, same private key. ``` Dual-key flow: 1. Biometric prompt → passkey-derived key → encrypts private key (primary) 2. User enters PIN → PIN encrypts same private key (backup) 3. Both stored in backend. Either can decrypt. ``` ## Platform Support | Platform | Technology | Package | | ------------------------ | -------------------------------- | ---------------------------- | | **Web (Next.js, React)** | WebAuthn PRF | `@chipi-stack/chipi-passkey` | | **Mobile (Expo)** | Face ID / Touch ID + SecureStore | `@chipi-stack/chipi-expo` | ## Installation ```bash Web (Next.js / React) theme={null} npm install @chipi-stack/chipi-passkey@latest ``` ```bash Mobile (Expo) theme={null} npx expo install expo-local-authentication expo-secure-store ``` ## Create Wallet with Passkey + PIN The recommended flow — passkey is primary, PIN is backup: ```typescript theme={null} "use client"; import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useCreateWallet, Chain } from "@chipi-stack/nextjs"; export function CreateWallet({ userId }: { userId: string }) { const { getToken } = useAuth(); const { createWalletAsync, isLoading, error } = useCreateWallet(); const [pin, setPin] = useState(""); const handleCreate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; // usePasskey: true triggers biometric prompt automatically. // encryptKey (PIN) becomes the backup — stored as encryptedPrivateKeyBackup. const wallet = await createWalletAsync({ params: { encryptKey: pin, externalUserId: userId, chain: Chain.STARKNET, usePasskey: true, }, bearerToken, }); // wallet.authMethod === "passkey+pin" // wallet.encryptedPrivateKey → encrypted with passkey key // wallet.encryptedPrivateKeyBackup → encrypted with PIN localStorage.setItem("wallet", JSON.stringify(wallet)); }; return (
setPin(e.target.value)} /> {error &&

Error: {error.message}

}
); } ``` ### What happens internally 1. `usePasskey: true` → calls `createWalletPasskey(userId, userId)` → biometric prompt 2. Returns `encryptKey` (passkey-derived) + `credentialId` + `prfSupported` 3. Since `encryptKey` (PIN) was also provided → dual-key mode: * Private key encrypted with passkey key → `encryptedPrivateKey` * Private key encrypted with PIN → `encryptedPrivateKeyBackup` 4. Both sent to backend with `authMethod: "passkey+pin"` **Source:** `chipi-react/src/hooks/useCreateWallet.ts` lines 62-100 ## Transfer with Passkey (Automatic PIN Fallback) ```typescript theme={null} "use client"; import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useTransfer, ChainToken } from "@chipi-stack/nextjs"; export function Transfer({ wallet, userId }: { wallet: any; userId: string }) { const { getToken } = useAuth(); const [recipient, setRecipient] = useState(""); const [amount, setAmount] = useState(""); // onPinRequired: called when passkey fails — show a PIN dialog const { transferAsync, isLoading, error } = useTransfer({ onPinRequired: async () => { return prompt("Passkey failed. Enter your backup PIN:"); }, }); const handleTransfer = async () => { const bearerToken = await getToken(); if (!bearerToken) return; const txHash = await transferAsync({ params: { wallet, token: ChainToken.USDC, recipient, amount: Number(amount), usePasskey: true, externalUserId: userId, }, bearerToken, }); alert(`Transfer complete: ${txHash}`); }; return (
setRecipient(e.target.value)} /> setAmount(e.target.value)} /> {error &&

Error: {error.message}

}
); } ``` ### Transfer flow 1. `usePasskey: true` → tries passkey authentication (biometric prompt) 2. If passkey succeeds → decrypts `encryptedPrivateKey` with passkey key → signs tx 3. If passkey fails (PRF unavailable, user cancelled, localStorage cleared): * Checks if `wallet.encryptedPrivateKeyBackup` exists * If yes → calls `onPinRequired()` → user enters PIN → decrypts backup key → signs tx * If no backup or no `onPinRequired` callback → throws descriptive error **Source:** `chipi-react/src/hooks/useTransfer.ts` lines 65-110 ### UseTransferConfig ```typescript theme={null} interface UseTransferConfig { onPinRequired?: () => Promise; } ``` | Option | Type | Description | | --------------- | ------------------------------- | --------------------------------------------------------------------------------- | | `onPinRequired` | `() => Promise` | Called when passkey fails and backup exists. Return PIN string or null to cancel. | ## PIN-Only Mode (Backward Compatible) Omit `usePasskey: true` for the same PIN-only flow as before: ```typescript theme={null} const wallet = await createWalletAsync({ params: { encryptKey: pin, externalUserId: userId, chain: Chain.STARKNET, // No usePasskey — PIN-only mode }, bearerToken, }); // wallet.authMethod === "pin" ``` ## Migrate from PIN to Passkey For existing PIN-only wallets: ```typescript theme={null} import { useMigrateWalletToPasskey } from "@chipi-stack/nextjs"; const { migrateWalletToPasskeyAsync } = useMigrateWalletToPasskey(); await migrateWalletToPasskeyAsync({ wallet, oldEncryptKey: currentPin, externalUserId: userId, bearerToken, }); // Wallet re-encrypted with passkey key. Backend updated. ``` ## Check Passkey Status ```typescript theme={null} import { usePasskeyStatus } from "@chipi-stack/chipi-passkey/hooks"; import { verifyWalletPasskeyDetailed } from "@chipi-stack/chipi-passkey"; // Quick check const { status } = usePasskeyStatus(); // status.isSupported, status.hasPasskey, status.isValid // Detailed check (distinguishes PRF unavailable from missing passkey) const result = await verifyWalletPasskeyDetailed(); // result: { valid: true } or { valid: false, reason: "prf_unavailable", message: "..." } ``` ## Expo (Mobile) Passkeys On mobile, the same `usePasskey: true` flag works automatically — the Expo `ChipiProvider` injects a native biometric adapter. ```typescript theme={null} import { ChipiProvider } from "@chipi-stack/chipi-expo"; import { useCreateWallet } from "@chipi-stack/chipi-expo"; // Same API as web — usePasskey: true uses Face ID / Touch ID const wallet = await createWalletAsync({ params: { encryptKey: pin, externalUserId: userId, chain: Chain.STARKNET, usePasskey: true, }, bearerToken, }); ``` If biometrics fail (user unenrolled, cancelled), the `onPinRequired` callback in `useTransfer` handles the fallback — same as web. ## Hooks Reference | Hook | Purpose | | ----------------------------- | --------------------------------------------------------------------------------------------------- | | `usePasskeySetup` | Create a new passkey manually (advanced — prefer `usePasskey: true` on `useCreateWallet`) | | `usePasskeyAuth` | Authenticate with existing passkey manually (advanced — prefer `usePasskey: true` on `useTransfer`) | | `usePasskeyStatus` | Check WebAuthn/biometric support and stored passkey validity | | `verifyWalletPasskeyDetailed` | Detailed check with reason: `"prf_unavailable"`, `"no_passkey"`, `"error"` | | `useMigrateWalletToPasskey` | Migrate existing PIN wallet to passkey | ## Security * **Dual-key wrapping**: Same private key encrypted by two independent keys. Either recovers. * **No silent fallback**: If passkey was created with PRF, PBKDF2 fallback is blocked (prevents wrong-key decryption). * **Backend credential recovery**: `credentialId` stored server-side. If localStorage cleared, SDK recovers from backend. * **Hardware-backed**: Keys in Secure Enclave (iOS), Android Keystore, or WebAuthn authenticator. ## Related * [Session Keys Guide](/sdk/guides/session-keys) — Combine passkeys with session keys * [useCreateWallet](/sdk/nextjs/hooks/use-create-wallet) — Wallet creation hook reference * [useTransfer](/sdk/nextjs/hooks/use-transfer) — Transfer hook reference * [useMigrateWalletToPasskey](/sdk/nextjs/hooks/use-migrate-wallet-to-passkey) — Migration hook reference # PIN → Passkey Migration Source: https://docs.chipipay.com/sdk/guides/pin-to-passkey-migration Migrate existing PIN-encrypted wallets to passkey (biometric) authentication for production-grade security. ## Why migrate PIN-encrypted wallets are the shortest path to a working integration, but they are not the right default for production. A 6-digit PIN has roughly 20 bits of entropy — an attacker who obtains the encrypted ciphertext (e.g. via a leaky metadata field, a stolen backup, or any other ciphertext exposure) can brute-force it offline in seconds. Passkey authentication removes this vector: * The encryption key is derived from a hardware-backed WebAuthn credential (Face ID, Touch ID, Windows Hello, security key). * It never leaves the device's secure element. * There is no low-entropy secret to crack. This guide walks through migrating an existing PIN-only wallet to a passkey wallet using `useMigrateWalletToPasskey`. **Recommended for all production wallets.** New wallets should be created with `usePasskey: true` from the start (see the [Passkeys guide](/sdk/guides/passkeys)). This guide covers existing wallets that were created PIN-only. ## Prerequisites * Wallet was created with a PIN (`encryptKey`) and currently has only `encryptedPrivateKey` set. * User is on a device that supports WebAuthn PRF (most modern browsers + Face ID / Touch ID / Windows Hello). * App is using `@chipi-stack/nextjs` ≥ v14.3.0 (which ships dual-key support for new wallets) or `@chipi-stack/chipi-react` ≥ v14.3.0. * `@chipi-stack/chipi-passkey` is installed. ```bash theme={null} npm install @chipi-stack/chipi-passkey@latest ``` ## How migration works `useMigrateWalletToPasskey` performs an atomic key swap: 1. The user's current PIN is used to decrypt `encryptedPrivateKey` locally — if the PIN is wrong, migration aborts before touching anything else. 2. A new passkey is created via WebAuthn (biometric prompt). 3. The decrypted private key is re-encrypted using the passkey-derived key. 4. The new ciphertext is sent to the backend via `PATCH /chipi-wallets/update-encryption-details`. 5. The hook returns the updated wallet and the `credentialId` — **persist both**. The on-chain wallet address does not change. The same private key continues to control the same Starknet account; only the encryption envelope on the backend is replaced. ## Migration component ```tsx theme={null} "use client"; import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useChipiWallet, useMigrateWalletToPasskey } from "@chipi-stack/nextjs"; export function MigrateToPasskey({ userId }: { userId: string }) { const { getToken } = useAuth(); const { wallet } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey(); const [pin, setPin] = useState(""); const [done, setDone] = useState(false); const handleMigrate = async () => { try { if (!wallet) return; const bearerToken = await getToken(); if (!bearerToken) throw new Error("Missing bearer token"); const result = await migrateWalletToPasskeyAsync({ wallet, oldEncryptKey: pin, externalUserId: userId, bearerToken, }); // Persist the new wallet and credentialId — these replace the old PIN-encrypted state. // localStorage is used here for example simplicity. In production, store both // values in a durable backend (your DB, or Clerk privateMetadata via a // server-side route) — see "After migration" below. localStorage.setItem("wallet", JSON.stringify(result.wallet)); localStorage.setItem("credentialId", result.credentialId); setDone(true); } catch (e) { console.error("Migration failed", e); } }; if (done) { return

✓ Migrated to passkey. You can now sign transactions with biometrics.

; } return (

Upgrade your wallet to passkey-based security.

setPin(e.target.value)} /> {error &&

{error.message}

}
); } ``` ## After migration Once migration succeeds: * `wallet.encryptedPrivateKey` is now passkey-encrypted. * All subsequent signing flows must use `usePasskey: true`. For example: ```tsx theme={null} import { useTransfer, ChainToken } from "@chipi-stack/nextjs"; const { transferAsync } = useTransfer(); await transferAsync({ params: { wallet, token: ChainToken.USDC, recipient, amount, usePasskey: true, // required after migration externalUserId: userId, }, bearerToken, }); ``` * Store `credentialId` somewhere durable (your DB, Clerk `privateMetadata` via a server route, etc.). If `localStorage` is cleared, the SDK can recover the credential from the backend via `/chipi-wallets/credential-recovery`. ## A note on PIN backup `useMigrateWalletToPasskey` performs a **single-key swap** — after migration, the backend stores only the passkey-encrypted ciphertext. A wallet that was created PIN-only does not gain a PIN backup column through migration. If you want true dual-key (passkey primary, PIN backup), the path is to create new wallets with `usePasskey: true` from the start (see [Passkeys → Create Wallet with Passkey + PIN](/sdk/guides/passkeys)). This sets both `encryptedPrivateKey` (passkey) and `encryptedPrivateKeyBackup` (PIN) at creation time, and the SDK's `useTransfer` will automatically fall back to PIN if the passkey is unavailable. For migrated wallets, plan for passkey recovery via WebAuthn discoverable credentials and `credentialId` recovery rather than a PIN fallback. ## Rollout strategy A staged rollout reduces risk: 1. **Detect.** On wallet load, check whether the wallet has a `credentialId` / passkey-encrypted state. If not, surface a one-time migration prompt. 2. **Prompt at a low-stakes moment.** Don't block the user mid-transfer. Show the upgrade flow on the next wallet screen visit, or after a successful (non-critical) action. 3. **Make it non-mandatory at first.** Let users skip and re-prompt later. Track adoption. 4. **Mandate for high-value flows.** Once adoption is high, gate sensitive flows (large transfers, treasury actions) behind passkey-only wallets. 5. **Monitor.** Log migration attempts, successes, failures, and PRF-not-supported events so you can size the long tail. ## Errors and edge cases | Error | Cause | What to do | | --------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- | | `Failed to decrypt wallet with provided encryptKey` | User entered the wrong PIN | Re-prompt; do not retry the passkey creation step | | WebAuthn aborted / cancelled | User dismissed the biometric prompt | No state changed — safe to retry | | `Backend rejected wallet encryption update` | Network or auth failure mid-update | The backend write is atomic; retry the full migration | | PRF not supported | Device/browser does not support WebAuthn PRF | Keep the user on PIN; surface the migration option only when PRF is detected | ## Related * [Passkeys guide](/sdk/guides/passkeys) — full passkey + dual-key architecture * [`useMigrateWalletToPasskey`](/sdk/nextjs/hooks/use-migrate-wallet-to-passkey) — hook reference * [`useUpdateWalletEncryption`](/sdk/nextjs/hooks/use-update-wallet-encryption) — lower-level encryption update * [`useChipiSession`](/sdk/nextjs/hooks/use-chipi-session) — session keys with secure server-side storage # Private Balances (shield / unshield) Source: https://docs.chipipay.com/sdk/guides/private-balances Add confidential balances to your app: users shield USDC into the STRK20 privacy pool so only they can see what they keep. Self-custodial, gasless for the end user. **Shielding (deposits) is temporarily paused.** After StarkWare's July 2026 STRK20 pool **v2.0** upgrade, depositing into the shared privacy pool requires a screener attestation issued by StarkWare's hosted proving service — Chipi is finalizing that access. **Unshielding and private-balance reads work today; new shields will revert until access lands.** Don't ship a shield flow to production yet — we'll announce on X ([@hichipipay](https://x.com/hichipipay)) the moment shielding is back on. Production-proven engine (Chipi's consumer wallet "Modo privado", mainnet). Mental model for your users: **checking/savings** — visible money is for spending, private money is what they keep, invisible to everyone but them. ## The five rules 1. **Key handling**: privacy keys derive client-side from the user's anchor (passkey encryptKey / PRF) and are never stored server-side. Cache the companion **address**, never the keys. Note the proving trust model below — it bounds what "private" may honestly mean in your copy. 2. **One passkey gesture per shield/unshield** — that IS the security model. Set the expectation: *"You confirm with your fingerprint or Face ID — the same as when you send money."* 3. **Browsers need a gateway proxy** (`gatewaySubmitUrl`): the Starknet sequencer gateway has no CORS. Install the `private-balance-card` registry item to get the proxy route, or copy it from the example. 4. **Capture the StarkWare pool ToS** at the user's first shield. 5. **Fees**: fixed 4 STRK per op, fronted by Chipi's gas tank (metered per API key) — your user never holds STRK. Show it in USD via `getShieldFee`. ## The trust model, honestly Keys derive client-side and are never stored server-side — but STRK20's **delegated proving requires the pool spending key inside each proof request**, so Chipi's proving service (a private prover Chipi operates; the same model as StarkWare's hosted prover) sees it per operation and must be trusted not to retain or use it. Privacy holds against the **public** — chain observers, other users — not against the proving operator. Do not tell your users "nobody, including Chipi, can see your balance." Local proving is the roadmap item that removes this trust. ## What is the "companion"? Each user gets a lightweight standard account (the **companion**) as their pool identity, derived deterministically from their anchor and deployed automatically on first shield. You only supply a standard SNIP-6 account class hash (`classHash`, e.g. OpenZeppelin `0x061dac03…`); everything else is handled. Why a companion instead of changing the account class: see [our design write-up on the Starknet forum](https://community.starknet.io/t/adding-confidential-payments-to-an-advanced-smart-account-two-ways-to-use-the-starkware-privacy-pool-and-why-we-picked-the-companion/), and the [Core SDK privacy page](/sdk/core/privacy) for the architecture. ## Quickstart ```tsx theme={null} import { useShield, usePrivateBalance, derivePrivacyAccount } from "@chipi-stack/nextjs"; const keys = await derivePrivacyAccount({ anchorSecret, // 32 bytes from your passkey rail seedDomainTag: wallet.address, // per-wallet domain separation classHash: COMPANION_CLASS_HASH, }); const { shieldAsync } = useShield(); await shieldAsync({ config: { apiPublicKey: process.env.NEXT_PUBLIC_CHIPI_API_KEY!, getBearerToken: () => getToken(), // a FUNCTION: tokens die mid-flow gatewaySubmitUrl: "/api/starknet-gateway", // your CORS proxy }, keys, token: USDC_ADDRESS, amount: 1_000_000n, // base units fundDeposit: (companion, amount) => transferGasless(companion, amount), onStep: setPhase, // prepare | fund | deposit | prove | finish — drive a REAL loader }); const { byToken } = usePrivateBalance({ mode: "ledger", events: recordedEvents }); ``` Record every settled op in your own store — your ledger is the display source of truth (the chain won't cleanly surface confidential moves). Failures never strand money: a failed shield parks funds in the companion and the next attempt sweeps them in; say "your money is safe" in error copy. ## Multiple tokens The pool holds any ERC-20, and the engine batches deposits: * **Shielding N tokens costs ONE proof and ONE 4-STRK fee** — batch with the core-level [`execShieldMany`](/sdk/core/privacy#multi-token-operations) (mainnet-proven). The `useShield` hook is per-token by design; for a multi-token shield call the core engine directly. * **Unshielding is per token** (no `unshieldMany` yet): each withdrawal is its own proof and its own 4-STRK fee, and consecutive withdrawals must wait \~5–6 minutes for the prover's view to catch up (they share the note channel). Run them sequentially, surface partial results honestly — what came back IS back, the rest stays private and retryable — and never call it a failure. Full pattern with the wait/refuel details on the [Core SDK privacy page](/sdk/core/privacy#unshielding-several-tokens). Product tip from Chipi's wallet: apply a small per-token dust floor (\~\$0.25) before adding a coin to the batch, and SHOW excluded coins greyed out with the reason — a silently skipped token reads as lost money to the user. Full headless example (also the release smoke): `examples/private-balance` in the SDK repo; multi-token evidence: `scripts/multi-token-smoke.mjs` (S5). Agent skill: `chipi-private-balances`. # Session Keys Source: https://docs.chipipay.com/sdk/guides/session-keys Enable frictionless transactions without requiring user approval for every action ## What Are Session Keys? Session keys are temporary keypairs that let your app execute transactions on behalf of a user without requiring their private key for every action. Instead of prompting users to sign each transaction, you register a session key on-chain with specific permissions (time limit, call limit, allowed functions) and then use it silently. This enables **gasless, popup-free UX** for gaming, DeFi automation, social apps, and subscriptions. Session keys only work with **CHIPI wallets** (SNIP-9 compatible). They are not supported on READY wallets. ## When to Use Session Keys | Use Case | Why Sessions Help | | ------------------- | ------------------------------------------------- | | **Gaming** | Players make moves without wallet popups | | **DeFi automation** | Yield optimizers execute strategies automatically | | **Social dApps** | Post, like, comment without friction | | **Subscriptions** | Recurring payments without manual approval | | **Airdrops** | Zero-friction claiming | ## How It Works ``` 1. Create → Generate a session keypair locally (client-side only) 2. Register → Register the session on the CHIPI wallet contract (one-time, requires owner signature) 3. Execute → Use the session key to sign transactions (no owner key needed) 4. Monitor → Check remaining calls and expiration 5. Revoke → Revoke the session on logout or expiration ``` The SDK handles the signature format difference automatically: * **Owner signature**: `[r, s]` (2 elements) * **Session signature**: `[sessionPubKey, r, s, validUntil]` (4 elements) ## Frontend Implementation ### 1. Create a Session Key ```typescript theme={null} import { useCreateSessionKey } from "@chipi-stack/nextjs"; function StartSession() { const { createSessionKeyAsync, isLoading } = useCreateSessionKey(); const handleStart = async () => { const session = await createSessionKeyAsync({ encryptKey: userPin, durationSeconds: 3600, // 1 hour }); // Store in client-side storage only (NEVER in your database) localStorage.setItem("chipiSession", JSON.stringify(session)); }; } ``` ### 2. Register On-Chain ```typescript theme={null} import { useAddSessionKeyToContract } from "@chipi-stack/nextjs"; function RegisterSession() { const { addSessionKeyToContractAsync } = useAddSessionKeyToContract(); const handleRegister = async () => { const session = JSON.parse(localStorage.getItem("chipiSession")!); const bearerToken = await getBearerToken(); await addSessionKeyToContractAsync({ params: { encryptKey: userPin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI" }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, allowedEntrypoints: [], // empty = all functions allowed }, }, bearerToken, }); }; } ``` ### 3. Execute Transactions ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/nextjs"; const USDC = "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb"; function TransferWithSession() { const { executeWithSessionAsync, isLoading } = useExecuteWithSession(); const handleTransfer = async (recipient: string, amount: string) => { const session = JSON.parse(localStorage.getItem("chipiSession")!); const bearerToken = await getBearerToken(); const txHash = await executeWithSessionAsync({ params: { encryptKey: userPin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI" }, session, calls: [{ contractAddress: USDC, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }], }, bearerToken, }); }; } ``` ### 4. Check Session Status ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/nextjs"; function SessionStatus() { const { data: sessionData } = useGetSessionData({ walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, }); if (sessionData?.isActive) { return

{sessionData.remainingCalls} calls remaining

; } return

Session expired or revoked

; } ``` ### 5. Revoke on Logout ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/nextjs"; function LogoutButton() { const { revokeSessionKeyAsync } = useRevokeSessionKey(); const handleLogout = async () => { const session = JSON.parse(localStorage.getItem("chipiSession")!); try { await revokeSessionKeyAsync({ params: { encryptKey: userPin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI" }, sessionPublicKey: session.publicKey, }, bearerToken: await getBearerToken(), }); } catch { // Continue with logout even if revoke fails } localStorage.removeItem("chipiSession"); await authProvider.signOut(); }; } ``` ## Session Configuration | Parameter | Type | Description | | -------------------- | --------- | ------------------------------------------------------- | | `sessionPublicKey` | string | Public key from `createSessionKey()` | | `validUntil` | number | Unix timestamp (seconds) when session expires | | `maxCalls` | number | Maximum transactions allowed (decrements with each use) | | `allowedEntrypoints` | string\[] | Whitelisted function selectors. Empty = all allowed | ### Restricting Permissions For security, whitelist specific entrypoints: ```typescript theme={null} sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 10, allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve ], } ``` ## Security Best Practices **Never store session keys in your backend database.** This defeats self-custodial security. Store only in client-side secure storage. | Platform | Recommended Storage | | ---------------- | ---------------------------------- | | **Web** | `localStorage` or `sessionStorage` | | **iOS** | Secure Enclave / Keychain | | **Android** | Android Keystore | | **React Native** | `expo-secure-store` | 1. **Always revoke on logout** - Never leave active sessions 2. **Set short durations** - 1-6 hours, not days 3. **Limit maxCalls** - Set realistic limits based on expected usage 4. **Whitelist entrypoints** - Restrict to only the functions your app needs 5. **Monitor remaining calls** - Create new sessions before exhaustion ## Related * [Session Hooks Reference](/sdk/nextjs/hooks/use-create-session-key) - Individual hook API docs * [Backend Sessions Quickstart](/sdk/backend/sessions-quickstart) - Server-side implementation * [Backend Sessions Methods](/sdk/backend/methods/sessions) - Detailed backend API Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support. # Spending Policies Source: https://docs.chipipay.com/sdk/guides/spending-policies Set per-token spending caps on session keys — enforced on-chain by the CHIPI wallet contract ## What Are Spending Policies? Spending policies let wallet owners set dollar-denominated caps on what a session key can spend. The CHIPI wallet contract enforces these limits automatically during transaction execution — no backend or middleware needed. Limits are enforced on ERC-20 operations: `transfer`, `approve`, `increase_allowance`. Spending policies require **CHIPI v33** wallets. v29 wallets do not have these entrypoints. See [Wallet Upgrades](/sdk/guides/wallet-upgrades) to upgrade. ## When to Use Spending Policies | Use Case | Configuration | | --------------------- | ---------------------------------------------- | | **AI agent budgets** | Max $0.01 per API call, $50 per day | | **Game daily limits** | Max 5 USDC per trade, 100 USDC per 24h | | **Employee wallets** | Max 200 USDC per transaction, 1000 per week | | **DeFi automation** | Max position size per swap, rolling window cap | ## How It Works ``` 1. Owner creates a session key and registers it on-chain 2. Owner sets a spending policy: per-call limit + rolling window limit 3. Session key executes transactions freely within limits 4. Contract enforces automatically — reverts if limits exceeded 5. Window resets automatically after the configured duration ``` The policy is per **(session key, token)** pair. You can set different limits for different tokens on the same session. ## Configuration A spending policy has three parameters: | Parameter | Type | Description | | --------------- | ------------- | --------------------------------------------------- | | `maxPerCall` | bigint (u256) | Maximum token amount per single transaction | | `maxPerWindow` | bigint (u256) | Maximum cumulative amount within the rolling window | | `windowSeconds` | number (u64) | Rolling window duration in seconds | The contract also tracks: | Field | Description | | --------------- | ---------------------------------------------- | | `spentInWindow` | Amount spent in the current active window | | `windowStart` | Unix timestamp when the current window started | If no policy is set (maxPerWindow is 0), the session has no spending limits. All ERC-20 operations are allowed without caps. ## TypeScript Backend ```typescript theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); // USDC on Starknet mainnet (6 decimals) const USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb"; // Set policy: max 1 USDC per call, 50 USDC per day const txHash = await sdk.sessions.setSpendingPolicy({ encryptKey: userEncryptKey, wallet: userWallet, spendingPolicyConfig: { sessionPublicKey: session.publicKey, token: USDC, maxPerCall: 1_000_000n, // 1 USDC (6 decimals) maxPerWindow: 50_000_000n, // 50 USDC windowSeconds: 86400, // 24 hours }, }, bearerToken); // Query current policy and spend const policy = await sdk.sessions.getSpendingPolicy({ walletAddress: userWallet.publicKey, sessionPublicKey: session.publicKey, token: USDC, }); console.log(`Spent ${policy.spentInWindow} of ${policy.maxPerWindow} this window`); // Remove policy (session has no limits for this token) const removeTxHash = await sdk.sessions.removeSpendingPolicy({ encryptKey: userEncryptKey, wallet: userWallet, sessionPublicKey: session.publicKey, token: USDC, }, bearerToken); ``` ## React / Next.js The `useChipiSession` hook includes spending policy actions: ```tsx theme={null} import { useChipiSession } from "@chipi-stack/nextjs"; const USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb"; function SessionWithLimits() { const { session, hasActiveSession, createSession, registerSession, setSpendingPolicy, getSpendingPolicy, removeSpendingPolicy, isSettingSpendingPolicy, isRemovingSpendingPolicy, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); const setupWithLimits = async () => { // 1. Create and register session await createSession(); await registerSession({ allowedEntrypoints: ["transfer"] }); // 2. Set spending limits await setSpendingPolicy({ token: USDC, maxPerCall: 1_000_000n, maxPerWindow: 50_000_000n, windowSeconds: 86400, }); }; const checkBudget = async () => { const policy = await getSpendingPolicy(USDC); const remaining = policy.maxPerWindow - policy.spentInWindow; console.log(`${remaining} USDC units remaining in this window`); }; return (
{!hasActiveSession ? ( ) : ( )}
); } ``` The React hook automatically injects the current session's `sessionPublicKey`. You only need to pass `token`, `maxPerCall`, `maxPerWindow`, and `windowSeconds`. ## Python ```python theme={null} from chipi_sdk import ( ChipiSDK, ChipiSDKConfig, SetSpendingPolicyParams, SpendingPolicyConfig, GetSpendingPolicyParams, RemoveSpendingPolicyParams, ) sdk = ChipiSDK(config=ChipiSDKConfig( api_public_key="pk_prod_...", api_secret_key="sk_prod_...", )) USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb" # Set policy tx_hash = sdk.sessions.set_spending_policy( SetSpendingPolicyParams( encrypt_key=user_encrypt_key, wallet=user_wallet, spending_policy_config=SpendingPolicyConfig( session_public_key=session.public_key, token=USDC, max_per_call=1_000_000, # 1 USDC max_per_window=50_000_000, # 50 USDC window_seconds=86400, # 24 hours ), ), bearer_token=bearer_token, ) # Query policy policy = sdk.sessions.get_spending_policy( GetSpendingPolicyParams( wallet_address=user_wallet.public_key, session_public_key=session.public_key, token=USDC, ) ) print(f"Spent {policy.spent_in_window} of {policy.max_per_window}") # Remove policy sdk.sessions.remove_spending_policy( RemoveSpendingPolicyParams( encrypt_key=user_encrypt_key, wallet=user_wallet, session_public_key=session.public_key, token=USDC, ), bearer_token=bearer_token, ) ``` Async variants are available: `aset_spending_policy`, `aget_spending_policy`, `aremove_spending_policy`. ## Validation Rules The SDK validates before submitting to the contract: | Rule | Error | | -------------------------------------------------------------------- | --------------------------------- | | Token address must not be empty | `INVALID_SPENDING_POLICY` | | `windowSeconds` must be a positive integer within u64 range | `INVALID_SPENDING_POLICY` | | `maxPerCall` must be non-negative and fit in u256 | `INVALID_SPENDING_POLICY` | | `maxPerWindow` must be non-negative and fit in u256 | `INVALID_SPENDING_POLICY` | | `maxPerCall` cannot exceed `maxPerWindow` (when maxPerWindow is set) | `INVALID_SPENDING_POLICY` | | Wallet must be CHIPI type | `INVALID_WALLET_TYPE_FOR_SESSION` | ## On-Chain Enforcement The contract enforces spending limits during `__execute__()` for session-signed transactions: 1. **Tracked selectors**: `transfer`, `approve`, `increase_allowance`, `increaseAllowance` 2. **Per-call check**: If `amount > maxPerCall`, transaction reverts with "Spending: exceeds per-call" 3. **Window check**: If `spentInWindow + amount > maxPerWindow`, transaction reverts with "Spending: exceeds window limit" 4. **Auto-reset**: When `now >= windowStart + windowSeconds`, the window resets to zero Owner-signed transactions (2-element signature) bypass spending policy enforcement entirely. # Wallet Upgrades Source: https://docs.chipipay.com/sdk/guides/wallet-upgrades How to upgrade wallets between types and versions — READY to CHIPI, or CHIPI v29 to v33. ## Overview Wallet upgrades change a wallet's on-chain account contract. Common upgrade paths: * **READY to CHIPI**: Argent X wallets upgrading to Chipi's session-enabled account * **CHIPI v29 to v33**: Older Chipi wallets upgrading to the latest version with spending policy and audit fixes Wallet upgrades are server-only operations that require the API secret key. They cannot be performed from frontend hooks. ## How It Works The upgrade is a two-step process: 1. **Prepare**: Call `prepareWalletUpgrade()` to get SNIP-12 typed data describing the upgrade 2. **Sign and Execute**: Have the wallet owner sign the typed data, then call `executeWalletUpgrade()` with the signature The backend handles the actual `upgrade` syscall on StarkNet. ## READY to CHIPI Upgrade Upgrade an Argent X wallet to a Chipi session-enabled wallet: ```typescript theme={null} import { ChipiSDK } from "@chipi-stack/backend"; const sdk = new ChipiSDK({ apiPublicKey: "pk_prod_...", apiSecretKey: "sk_prod_...", }); // Step 1: Prepare const upgrade = await sdk.prepareWalletUpgrade({ walletAddress: "0x04abc...def", }); console.log(upgrade.currentWalletType); // "READY" console.log(upgrade.targetWalletType); // "CHIPI" // Step 2: Sign (with user's private key) const signature = await account.signMessage(upgrade.typedData); // Step 3: Execute const result = await sdk.executeWalletUpgrade({ walletAddress: "0x04abc...def", typedData: upgrade.typedData, signature: [signature.r.toString(), signature.s.toString()], }); console.log(result.transactionHash); console.log(result.newWalletType); // "CHIPI" ``` ## CHIPI Version Upgrade Upgrade an older CHIPI wallet (e.g., v29) to the latest version (v33): ```typescript theme={null} const upgrade = await sdk.prepareWalletUpgrade({ walletAddress: "0x04abc...def", // No targetClassHash needed — defaults to latest CHIPI (v33) }); console.log(upgrade.currentWalletType); // "CHIPI" console.log(upgrade.targetWalletType); // "CHIPI" (same type, newer version) ``` ## Custom Class Hash To upgrade to a specific class hash (e.g., a community-submitted account): ```typescript theme={null} const upgrade = await sdk.prepareWalletUpgrade({ walletAddress: "0x04abc...def", targetClassHash: "0x0484bbd...", // specific version }); ``` ## Python ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, PrepareWalletUpgradeParams, ExecuteWalletUpgradeParams sdk = ChipiSDK(config=ChipiSDKConfig( api_public_key="pk_prod_...", api_secret_key="sk_prod_...", )) # Prepare upgrade = sdk.prepare_wallet_upgrade( PrepareWalletUpgradeParams(wallet_address="0x04abc...def") ) # Sign (with user's key) then execute result = sdk.execute_wallet_upgrade( ExecuteWalletUpgradeParams( wallet_address="0x04abc...def", typed_data=upgrade.typed_data, signature=["0xr", "0xs"], ) ) print(f"Upgraded: {result.transaction_hash}") ``` ## After Upgrade After a successful upgrade: * The wallet's class hash changes on-chain * Session keys become available (if upgrading from READY to CHIPI) * Existing balances and transaction history are preserved * The wallet address stays the same # x402 Client — Paying for APIs Source: https://docs.chipipay.com/sdk/guides/x402-client Use Chipi wallets to pay for x402-protected API endpoints **Using a SHHH V8.4 wallet?** This page covers `X402Client`, which signs payments for legacy **CHIPI v29** and **READY (Argent X)** wallets. SHHH V8.4 wallets — the default for new wallets since v14.5.0 — use [`X402ShhhClient`](/sdk/guides/x402-shhh-buyers) instead, because V8.4 rejects the raw `{r, s}` signature shape `X402Client` produces. ## Overview The x402 client automatically handles HTTP 402 responses. When a server requires payment, the client: 1. Parses the payment requirement from the `PAYMENT-REQUIRED` header 2. Validates the amount against your configured limits 3. Signs the payment using your Starknet account 4. Retries the request with the `X-PAYMENT` header ## React — `useX402Payment` Hook The easiest way to use x402 in a React/Next.js app: ```typescript theme={null} import { useX402Payment } from "@chipi-stack/chipi-react"; function PremiumContent() { const { x402Fetch, isPaying, lastTxHash, paymentCount, error } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "1.00", // Max $1 per request onPaymentComplete: (txHash, amount) => { console.log(`Paid ${amount} USDC — tx: ${txHash}`); }, }); const fetchData = async () => { const response = await x402Fetch("https://api.example.com/premium"); const data = await response.json(); // data is available — payment was handled automatically }; return (
{lastTxHash &&

Last payment: {lastTxHash}

}

Total payments: {paymentCount}

); } ``` ### Hook Configuration | Property | Type | Description | | ------------------- | -------------------------- | --------------------------------------- | | `wallet` | `WalletData` | Wallet data from `useChipiWallet` | | `encryptKey` | `string` | User's PIN/password for signing | | `getBearerToken` | `() => Promise` | Auth token provider | | `maxPaymentAmount` | `string?` | Max USDC per request (e.g. `"1.00"`) | | `allowedRecipients` | `string[]?` | Whitelist of allowed merchant addresses | | `session` | `SessionKeyData?` | Session key for automatic payments | | `onPaymentComplete` | `(txHash, amount) => void` | Callback after successful payment | ### Hook Return Values | Property | Type | Description | | -------------- | ------------------------------------------ | ----------------------------- | | `x402Fetch` | `(url, init?) => Promise` | Drop-in `fetch` replacement | | `signPayment` | `(requirement) => Promise` | Manual payment signing | | `isPaying` | `boolean` | Payment in progress | | `lastTxHash` | `string \| null` | Last payment transaction hash | | `lastAmount` | `string \| null` | Last payment amount (USDC) | | `error` | `Error \| null` | Last error | | `paymentCount` | `number` | Total payments this session | ## Node.js — `X402Client` For server-side or CLI applications: ```typescript theme={null} import { X402Client, DirectSigner, createAccount } from "@chipi-stack/core"; import { RpcProvider } from "starknet"; // Create account from private key (server-side only) const signer = new DirectSigner(process.env.PRIVATE_KEY!); const provider = new RpcProvider({ nodeUrl: "https://starknet-mainnet.infura.io/..." }); const account = await createAccount({ signer, provider }); // Create x402 client with safety limits const client = new X402Client(account, { maxPaymentAmount: "0.50", // Max $0.50 per request allowedRecipients: ["0xTRUSTED_MERCHANT"], // Only pay trusted servers }); // Automatic 402 handling const response = await client.fetch("https://api.example.com/ai-endpoint"); const data = await response.json(); ``` ### Configuration Options | Option | Type | Default | Description | | ------------------- | ----------- | ---------------------- | ------------------------------- | | `maxPaymentAmount` | `string?` | `undefined` (no limit) | Max USDC per request | | `allowedRecipients` | `string[]?` | `[]` (allow all) | Whitelist of merchant addresses | | `headerName` | `string?` | `"X-PAYMENT"` | Custom payment header name | **Always set `maxPaymentAmount` in production.** Without it, a compromised or malicious server could request an arbitrarily large payment. Set it to the maximum amount you expect to pay per request (e.g. `"1.00"` for \$1 USDC). ## Python ```python theme={null} from chipi_sdk import ChipiSDK, X402Client, X402ClientConfig sdk = ChipiSDK(config) client = X402Client(sdk, X402ClientConfig( max_payment_amount="0.10", # Max $0.10 per request )) # Sync response = client.fetch("https://api.example.com/data", bearer_token="...") # Async response = await client.afetch("https://api.example.com/data", bearer_token="...") ``` ## Safety Features ### Amount Limits Always set `maxPaymentAmount` to prevent unexpected charges: ```typescript theme={null} const client = new X402Client(account, { maxPaymentAmount: "0.10", // Never pay more than $0.10 per request }); ``` If a server requests more than your limit, the client throws an error instead of signing. ### Recipient Whitelist Restrict which servers can charge you: ```typescript theme={null} const client = new X402Client(account, { allowedRecipients: [ "0xTRUSTED_API_1", "0xTRUSTED_API_2", ], }); ``` ### Asset Validation The client only accepts payments in native USDC on Starknet mainnet. Requests for any other token are automatically rejected. ## Error Handling ```typescript theme={null} try { const response = await client.fetch("https://api.example.com/data"); } catch (error) { if (error.message.includes("exceeds maximum")) { // Payment too expensive } else if (error.message.includes("not in the allowed")) { // Unknown merchant } else if (error.message.includes("PAYMENT-REQUIRED")) { // Server misconfigured (no payment requirement header) } } ``` # x402 Payment Protocol Source: https://docs.chipipay.com/sdk/guides/x402-introduction Pay-per-request APIs using USDC on Starknet ## What is x402? [x402](https://www.x402.org/) is an open HTTP payment protocol backed by Coinbase, Cloudflare, Google, Visa, Stellar, and Lightning Labs. It enables **pay-per-request APIs** where clients pay with crypto before accessing a resource. The protocol uses HTTP status code **402 Payment Required** — a status code reserved since 1997 for exactly this purpose. ## How it Works ``` Client Server Chipi Paymaster | | | | GET /api/resource | | |------------------------------>| | | 402 + PAYMENT-REQUIRED hdr | | |<------------------------------| | | | | | [sign SNIP-12 payment] | | | | | | GET /api/resource | | | + X-PAYMENT header | | |------------------------------>| | | | verify + settle (gasless) | | |------------------------------->| | | { txHash } | | |<-------------------------------| | 200 + resource | | |<------------------------------| | ``` 1. Client requests a protected resource 2. Server returns **402** with a `PAYMENT-REQUIRED` header containing the price 3. Client signs a USDC payment using SNIP-12 typed data 4. Client retries the request with the signed payment in the `X-PAYMENT` header 5. Server verifies the signature and settles via Chipi's gasless paymaster 6. Server returns the resource ## Why x402 + Chipi? Chipi's existing infrastructure maps perfectly to x402: | x402 Concept | Chipi Infrastructure | | ------------------ | ------------------------------------------- | | Payment signing | `account.signMessage()` (SNIP-12) | | Gasless settlement | `PaymasterAdapter` / sponsored transactions | | Automated payments | Session keys (SNIP-9) | | Client wallets | CHIPI / READY wallets | **Key advantages:** * **Gasless**: Payments are settled through Chipi's paymaster — users never need STRK for gas * **Session keys**: Combined with SNIP-9 sessions, payments happen automatically without user interaction per request * **Native USDC**: All payments use native USDC on Starknet mainnet ## SDK Support | Package | What's Available | | ---------------------- | ------------------------------------------------ | | `@chipi-stack/types` | TypeScript type definitions | | `@chipi-stack/core` | `X402Client` — automatic 402 payment handling | | `@chipi-stack/backend` | `X402Facilitator` + `x402Middleware` for Express | | `chipi-react` | `useX402Payment` hook | | `chipi_sdk` (Python) | `X402Client` + `X402Facilitator` + middleware | ## Quick Examples ### Client: Pay for APIs ```typescript theme={null} import { X402Client } from "@chipi-stack/core"; const client = new X402Client(account, { maxPaymentAmount: "1.00", // Max $1 USDC per request }); // Automatically handles 402 responses const response = await client.fetch("https://api.example.com/premium-data"); const data = await response.json(); ``` ### Server: Monetize APIs ```typescript theme={null} import { x402Middleware } from "@chipi-stack/backend"; const facilitator = sdk.createX402Facilitator(); app.get("/premium", x402Middleware({ facilitator, paymentConfig: { amount: "0.01", payTo: "0xYOUR_ADDRESS" }, }), (req, res) => { res.json({ data: "premium content", txHash: req.x402.txHash }); }); ``` ## Next Steps Pay for APIs from your app Monetize your API endpoints Automated payments with sessions # x402 Server — Monetizing APIs Source: https://docs.chipipay.com/sdk/guides/x402-server Accept x402 payments on your API endpoints with Chipi's paymaster ## Overview The x402 server components let you monetize your API endpoints. Clients pay with USDC per request, and payments are settled gaslessly through Chipi's paymaster. Two main components: * **`X402Facilitator`** — Verifies payment signatures and settles via paymaster * **`x402Middleware`** — Express middleware that handles the full 402 flow (also compatible with Express-like frameworks such as Hono) ## Express / Node.js ### Quick Setup ```typescript theme={null} import express from "express"; import { ChipiServerSDK, x402Middleware } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); const facilitator = sdk.createX402Facilitator(); const app = express(); // Protect an endpoint — requires 0.01 USDC per request app.get("/premium", x402Middleware({ facilitator, paymentConfig: { amount: "0.01", payTo: "0xYOUR_MERCHANT_ADDRESS", description: "Premium API access", }, }), (req, res) => { // Payment was verified and settled res.json({ data: "premium content", txHash: req.x402.txHash, }); } ); app.listen(3000); ``` ### Middleware Behavior 1. **No `X-PAYMENT` header** → Returns 402 with `PAYMENT-REQUIRED` header 2. **Malformed header** → Returns 400 3. **Invalid payment** → Returns 402 with error reason 4. **Valid payment** → Settles via paymaster, attaches `req.x402`, calls `next()` ### Access Payment Info After successful payment, `req.x402` contains: ```typescript theme={null} { txHash: "0x...", // Starknet transaction hash payment: { ... }, // Full PaymentPayload networkId: "starknet-mainnet" } ``` ### Verify-Only Mode To verify payments without auto-settling (for custom settlement logic): ```typescript theme={null} app.get("/custom", x402Middleware({ facilitator, paymentConfig: { amount: "0.01", payTo: "0xADDRESS" }, verifyOnly: true, // Don't auto-settle }), async (req, res) => { // Payment is verified but not settled // Do your own settlement logic here const result = await facilitator.settle(req.x402.payment); res.json({ txHash: result.transactionHash }); } ); ``` ## Custom Nonce Store By default, the facilitator uses an in-memory nonce store (resets on server restart). For production, plug in a persistent store: ### Redis Example ```typescript theme={null} import Redis from "ioredis"; const redis = new Redis(); const facilitator = sdk.createX402Facilitator(undefined, { nonceStore: { has: async (nonce) => (await redis.exists(`x402:nonce:${nonce}`)) === 1, consume: async (nonce) => { // SET NX with 24h expiry — atomic check-and-consume const result = await redis.set(`x402:nonce:${nonce}`, "1", "EX", 86400, "NX"); return result === "OK"; // true if newly set (not replayed) }, }, }); ``` ### PostgreSQL Example ```typescript theme={null} const facilitator = sdk.createX402Facilitator(undefined, { nonceStore: { has: async (nonce) => { const result = await db.query( "SELECT 1 FROM x402_nonces WHERE nonce = $1", [nonce] ); return result.rows.length > 0; }, consume: async (nonce) => { // INSERT … ON CONFLICT DO NOTHING returns rowCount 1 if inserted, 0 if duplicate. // This is atomic at the database level. const result = await db.query( "INSERT INTO x402_nonces (nonce, created_at) VALUES ($1, NOW()) ON CONFLICT DO NOTHING", [nonce] ); return (result.rowCount ?? 0) > 0; }, }, }); ``` The `x402_nonces` table must enforce uniqueness on the `nonce` column: ```sql theme={null} CREATE TABLE x402_nonces ( nonce TEXT PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT NOW() ); ``` For cleanup, run periodically: `DELETE FROM x402_nonces WHERE created_at < NOW() - INTERVAL '24 hours'` ## Python — FastAPI ```python theme={null} from fastapi import FastAPI, Depends, Request from chipi_sdk import ChipiSDK, X402Facilitator, X402PaymentConfig from chipi_sdk.x402_middleware import fastapi_x402_dependency sdk = ChipiSDK(config) facilitator = X402Facilitator(sdk, bearer_token="...") payment_config = X402PaymentConfig(amount="0.01", pay_to="0xADDRESS") app = FastAPI() @app.get("/premium") async def premium( request: Request, x402=Depends(fastapi_x402_dependency(facilitator, payment_config)) ): return { "data": "premium content", "tx_hash": x402.get("tx_hash"), } ``` ## Python — Flask ```python theme={null} from flask import Flask, jsonify from chipi_sdk import ChipiSDK, X402Facilitator, X402PaymentConfig from chipi_sdk.x402_middleware import flask_x402_required sdk = ChipiSDK(config) facilitator = X402Facilitator(sdk, bearer_token="...") payment_config = X402PaymentConfig(amount="0.01", pay_to="0xADDRESS") app = Flask(__name__) @app.route("/premium") @flask_x402_required(facilitator, payment_config) def premium(): return jsonify({"data": "premium content"}) ``` ## Direct Facilitator Usage For custom server frameworks or advanced use cases: ```typescript theme={null} const facilitator = sdk.createX402Facilitator(); // Verify a payment (no settlement) const verification = await facilitator.verify(paymentPayload); if (!verification.isValid) { console.error("Invalid:", verification.invalidReason); } // Settle a payment (execute USDC transfer) const settlement = await facilitator.settle(paymentPayload); if (settlement.success) { console.log("Settled:", settlement.transactionHash); } ``` ## CORS Configuration The middleware automatically skips `OPTIONS` (preflight) requests. Make sure your CORS setup includes the `X-PAYMENT` header: ```typescript theme={null} import cors from "cors"; app.use(cors({ exposedHeaders: ["Payment-Required"], allowedHeaders: ["X-PAYMENT", "Content-Type", "Authorization"], })); ``` # x402 + Session Keys Source: https://docs.chipipay.com/sdk/guides/x402-sessions Automated x402 payments using SNIP-9 session keys ## The Killer Combo x402 payments are USDC transfers. Session keys on CHIPI wallets authorize transfers without requiring the owner key each time. | Mode | User Experience | | ------------------- | --------------------------------------------------------------- | | **Without session** | Each payment requires signing (wallet popup / biometrics / PIN) | | **With session** | Payments happen automatically — zero interaction per request | This makes x402 + sessions ideal for: * AI agent autonomous API consumption * Streaming data feeds (pay-per-query) * Backend automation (server-to-server payments) * Mobile apps with "subscribe for X hours" UX ## React Implementation ```typescript theme={null} import { useChipiSession, useX402Payment } from "@chipi-stack/nextjs"; function AutoPayContent() { // Step 1: Set up session const { session, hasActiveSession, createSession, registerSession, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Step 2: Pass session to x402 const { x402Fetch, isPaying, paymentCount } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, session, // Session key signs payments automatically maxPaymentAmount: "0.50", }); // Step 3: Set up session (one-time) const setupSession = async () => { await createSession(); await registerSession({ allowedEntrypoints: ["transfer"], // Only USDC transfers }); }; // Step 4: Use — payments are fully automatic const fetchData = async () => { // No popup, no PIN, no interaction const response = await x402Fetch("https://api.example.com/ai-query?q=..."); return response.json(); }; return (
{!hasActiveSession ? ( ) : (

{paymentCount} queries paid

)}
); } ``` ## How It Works Under the Hood 1. **Session creation**: Generate a temporary keypair, encrypt private key with user's PIN 2. **Session registration**: Register the session public key on the wallet contract (one owner signature) 3. **Session constraint**: `allowedEntrypoints: ["transfer"]` restricts the session to only USDC transfer calls 4. **x402 payment**: When a 402 is received, the hook uses `executeTransactionWithSession()` instead of `executeTransaction()` — the session key signs automatically ``` User sets up session (one-time PIN entry) | v [Session key registered on wallet contract] | v x402Fetch("https://api.example.com/data") | v Server returns 402 → parse requirement → sign with session key → retry | ^ | | +-- No user interaction! Session key signs automatically ``` ## Node.js Backend Automation For server-side automation where you want to consume paid APIs: ```typescript theme={null} import { X402Client, DirectSigner, createAccount } from "@chipi-stack/core"; import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); // Create session for automated payments const session = sdk.sessions.createSessionKey({ encryptKey: process.env.ENCRYPT_KEY!, durationSeconds: 3600, // 1 hour }); // Register session with transfer-only permissions await sdk.sessions.addSessionKeyToContract({ encryptKey: process.env.ENCRYPT_KEY!, wallet: myWallet, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, allowedEntrypoints: ["transfer"], }, }, bearerToken); // Now use session for automated x402 payments const signer = new DirectSigner(decryptedSessionKey); const account = await createAccount({ signer, provider }); const client = new X402Client(account, { maxPaymentAmount: "0.10" }); // Automated — no human interaction needed for (const query of queries) { const response = await client.fetch(`https://api.example.com/ai?q=${query}`); const result = await response.json(); // Process result... } ``` ## On-Chain Spending Limits Session keys alone don't enforce dollar limits — `maxCalls` caps the number of transactions, not the amount. For real budget control, use **Spending Policies**: ```typescript theme={null} // After registering the session, set on-chain limits await setSpendingPolicy({ token: USDC_ADDRESS, maxPerCall: 100_000n, // Max $0.10 per x402 payment (6 decimals) maxPerWindow: 50_000_000n, // Max $50 per day windowSeconds: 86400, // 24h rolling window }); ``` The contract enforces this automatically — if a payment exceeds the per-call limit or the rolling window total, the transaction reverts on-chain. No middleware or backend enforcement needed. Spending policies require **CHIPI v33** wallets. See the full guide: [Spending Policies](/sdk/guides/spending-policies). ### Three Layers of Protection | Layer | Scope | Enforced By | | ------------------------------- | -------------------------------------- | ----------------------------------- | | `maxPaymentAmount` (X402Client) | Per-request, client-side | SDK (can be bypassed) | | `maxCalls` (Session) | Total transaction count | Smart contract | | **Spending Policy** | Per-call amount + rolling window total | Smart contract (cannot be bypassed) | For production x402 deployments, use all three. ## Security Considerations ### Session Scope Always restrict session keys to the minimum required permissions: ```typescript theme={null} await registerSession({ allowedEntrypoints: ["transfer"], // Only allow transfers maxCalls: 100, // Limit total calls }); // Then add on-chain spending limits await setSpendingPolicy({ token: USDC_ADDRESS, maxPerCall: 100_000n, // $0.10 per call maxPerWindow: 10_000_000n, // $10 total per session window windowSeconds: 21600, // 6h (match session duration) }); ``` This creates a comprehensive spending cap: * **Per-request limit**: `maxPerCall` enforced on-chain (\$0.10 per API call) * **Session window limit**: `maxPerWindow` enforced on-chain (\$10 per 6 hours) * **Transaction count limit**: `maxCalls: 100` enforced on-chain * **Client-side guard**: `maxPaymentAmount: "0.10"` (early rejection, saves gas) ### Session Expiry Fallback If the session expires during a payment, the hook falls back to requiring the owner's signature (wallet popup / biometrics / PIN): ```typescript theme={null} const { x402Fetch } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, session, // May be expired // If session is expired, falls back to standard wallet signing }); ``` ### Best Practices 1. **Set spending limits**: Always configure `maxPaymentAmount` and session `maxCalls` 2. **Whitelist merchants**: Use `allowedRecipients` when possible 3. **Monitor payments**: Use `onPaymentComplete` callback for logging/analytics 4. **Short sessions**: Prefer shorter session durations with renewal over long-lived sessions 5. **Transfer-only**: Always set `allowedEntrypoints: ["transfer"]` for x402 sessions # x402 Buyers — SHHH Wallets Source: https://docs.chipipay.com/sdk/guides/x402-shhh-buyers Pay for x402-protected APIs from a SHHH V8.4 wallet. Uses X402ShhhClient, which handles the V8.4 envelope shape the existing X402Client can't produce. If your wallet is a **SHHH V8.4** wallet — the default since v14.5.0 — use `X402ShhhClient`. The existing [`X402Client`](/sdk/guides/x402-client) only works for CHIPI v29 and Argent X wallets; it signs payments with a raw `{r, s}` pair that V8.4 rejects on chain. `X402ShhhClient` is in `@chipi-stack/backend` (and re-exported from `@chipi-stack/x402` for convenience). Same `fetch(url, init)` surface as the regular client. ## Which buyer client do I need? | Your wallet | Use | | ----------------------------------------------- | --------------------------------------- | | SHHH V8.4 (default for new wallets in v14.5.0+) | `X402ShhhClient` (this page) | | CHIPI v29 (legacy default) | [`X402Client`](/sdk/guides/x402-client) | | READY (Argent X v0.4.0) | [`X402Client`](/sdk/guides/x402-client) | Not sure which? Read `wallet.walletType` off any wallet returned from `getWallet` / `createWallet` / `useChipiWallet`. ## Install ```bash theme={null} npm install @chipi-stack/backend @chipi-stack/x402 # or pnpm / yarn ``` `@chipi-stack/x402` re-exports `X402ShhhClient` so a single import covers facilitator + client surfaces. ## Construct the client ```ts theme={null} import { X402ShhhClient, ChipiClient } from "@chipi-stack/backend"; const client = new X402ShhhClient({ wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, signerKind: "STARK", // defaults to "STARK" if omitted }, encryptKey, chipiClient: new ChipiClient({ apiPublicKey }), bearerToken, }); ``` Required: * **`wallet`** — the wallet's `publicKey` (deployed account address) and `encryptedPrivateKey` (ciphertext). Same `WalletData`-derived shape every other SDK call uses. * **`encryptKey`** — passkey-derived key or PIN that decrypts the wallet's STARK key client-side at sign time. * **`chipiClient`** — Chipi HTTP client (the same one your server uses elsewhere). * **`bearerToken`** — your chipi-back auth token. Server-side only; never crosses the wire to the x402 seller. Optional `config`: ```ts theme={null} new X402ShhhClient({ wallet, encryptKey, chipiClient, bearerToken, config: { maxPaymentAmount: "1.00", // hard cap per request in USDC allowedRecipients: ["0xtrusted-merchant…"], // whitelist of payTo addresses headerName: "X-PAYMENT", // default — override only if the seller wants a different name chainId: undefined, // defaults to Starknet mainnet }, }); ``` **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. ## Pay for a 402-protected request `fetch` is a drop-in replacement for the global `fetch`. If the seller returns `200`, it passes through. If they return `402 Payment Required`, the client signs the payment locally and retries: ```ts theme={null} const response = await client.fetch("https://api.example.com/premium-data"); if (response.status === 200) { const data = await response.json(); // …use the paid response } else { // 402 still after retry, 4xx/5xx, etc. } ``` The client adds the signed `X-PAYMENT` header to the retry. No paymaster call from your side — the facilitator (the seller's side) settles by forwarding the signed calldata to chipi-back's `/transactions/execute-sponsored-raw`. ## What gets signed 1. Build a `USDC.transfer(payTo, amount)` call. 2. Build a SHHH V8.4 `execute_from_outside_v2` outside-execution wrapping that call, with `caller = ANY_CALLER` (paymaster sponsorship). 3. Compute the V8.4 SNIP-12 hash against the wallet's address + chain id. 4. Decrypt the STARK key client-side, sign the hash, wrap into a `V2_SNIP12` envelope. 5. Serialize the full `execute_from_outside_v2` calldata into `payload.shhh.oeCalldata`. 6. Ship that as the `X-PAYMENT` header. The cleartext STARK key never crosses the wire. Chipi-back receives only the pre-signed calldata span; the wallet's owner is the only party that could have produced it. ## Signer kind support | `signerKind` | Status | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `"STARK"` | Shipped — used by 100% of SHHH wallets created today | | `"ED25519"` | Coming — Phantom (Solana) buyers paying x402 sellers | | `WEBAUTHN_P256`, `EIP191_SECP256K1`, etc. | Not in v14.5.0 — the SHHH client throws a clear error for non-STARK kinds so silent on-chain reverts are impossible | For browser-rooted kinds (`WEBAUTHN_P256`, `EIP191_SECP256K1`), the right pattern is to call your wallet adapter's `signMessage` directly and build the envelope via `buildWebAuthnEnvelopeFromAssertion` / `buildEip191EnvelopeFromSignature` from `@chipi-stack/backend`, then submit through the paymaster yourself. The drop-in `fetch(url)` flow is STARK-only today. ## Safety: caps + allowlists Both the [original `X402Client`](/sdk/guides/x402-client#safety-features) and `X402ShhhClient` enforce the same two safety nets at sign time: * `maxPaymentAmount` — hard cap per request in human USDC ("1.00" = 1 USDC). Throws before signing if a 402 asks for more. * `allowedRecipients` — whitelist of seller addresses. Throws before signing if a 402's `payTo` isn't on the list. Both errors surface before any signature is produced, so the user is never asked to sign a payment that would have been rejected anyway. ## Facilitator side (sellers) You don't need a different facilitator for SHHH buyers — `X402Facilitator` from PR #298 onward auto-detects `payload.shhh` on the inbound payment and routes through the SHHH dispatch path. See [x402 server — monetizing APIs](/sdk/guides/x402-server) for the seller-side setup; that page works as-is for SHHH buyers. ## Related * [x402 introduction](/sdk/guides/x402-introduction) — protocol overview * [x402 client (CHIPI v29 / Argent X)](/sdk/guides/x402-client) — the matching page for legacy wallet types * [x402 server](/sdk/guides/x402-server) — accept x402 payments from any buyer type * [x402 + sessions](/sdk/guides/x402-sessions) — automated payments via session keys * [Spending policies](/sdk/guides/spending-policies) — per-token caps on session keys # Server SDK (createChipiServer) Source: https://docs.chipipay.com/sdk/nextjs/create-chipi-server Create a server-side Chipi SDK singleton for use in Next.js Server Components and Route Handlers. ## Overview `createChipiServer` provides a singleton `ChipiServerSDK` instance for server-side operations in Next.js. It ensures only one SDK instance exists across your application, with automatic config conflict detection. Import everything from `@chipi-stack/nextjs/server`: ```typescript theme={null} import { createChipiServer, getChipiServer, resetChipiServer, } from "@chipi-stack/nextjs/server"; ``` ## Setup Create a shared server configuration file (e.g. `lib/chipi-server.ts`): ```typescript theme={null} import { createChipiServer } from "@chipi-stack/nextjs/server"; export const chipiServer = createChipiServer({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); ``` `apiPublicKey` and `apiSecretKey` are both required. The function throws immediately if either is missing. Keep `apiSecretKey` server-only — never expose it in client bundles or `NEXT_PUBLIC_*` environment variables. ## Functions ### `createChipiServer(config)` Creates or returns the singleton `ChipiServerSDK` instance. | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ------------------------------ | | `apiPublicKey` | `string` | Yes | Your Chipi public API key | | `apiSecretKey` | `string` | Yes | Your Chipi secret API key | | `alphaUrl` | `string` | No | Custom API base URL (advanced) | | `nodeUrl` | `string` | No | Custom Starknet RPC node URL | **Returns:** `ChipiServerSDK` #### Config Conflict Detection If `createChipiServer` is called a second time with a different config, it throws an error instead of silently ignoring the new config. This prevents hard-to-debug issues where different parts of your app expect different configurations. ```typescript theme={null} // First call — creates the singleton createChipiServer({ apiPublicKey: "pk_1", apiSecretKey: "sk_1" }); // Second call with same config — returns the existing instance (no-op) createChipiServer({ apiPublicKey: "pk_1", apiSecretKey: "sk_1" }); // Second call with different config — throws an error createChipiServer({ apiPublicKey: "pk_2", apiSecretKey: "sk_2" }); // Error: createChipiServer() was called with a different config than the // existing singleton. Call resetChipiServer() first if you need to reconfigure. ``` ### `getChipiServer()` Returns the existing singleton instance. Throws if `createChipiServer` has not been called yet. **Returns:** `ChipiServerSDK` ```typescript theme={null} import { getChipiServer } from "@chipi-stack/nextjs/server"; // In any server-side code after initial setup const chipi = getChipiServer(); ``` ### `resetChipiServer()` Destroys the existing singleton instance so that a new one can be created with `createChipiServer`. Primarily useful for testing. ```typescript theme={null} import { resetChipiServer, createChipiServer } from "@chipi-stack/nextjs/server"; // In a test setup / teardown resetChipiServer(); const freshInstance = createChipiServer({ /* new config */ }); ``` ## Usage in Server Components ```typescript theme={null} import { chipiServer } from "@/lib/chipi-server"; export default async function WalletPage({ params }: { params: { userId: string } }) { const wallet = await chipiServer.wallets.getWallet({ externalUserId: params.userId, }); return (

Wallet

{/* GetWalletResponse exposes `publicKey` and `normalizedPublicKey`. */} {/* There is no `accountAddress` field — see types/src/wallet.ts. */}

Address: {wallet.normalizedPublicKey}

Public Key: {wallet.publicKey}

); } ``` ## Usage in Route Handlers ```typescript theme={null} import { chipiServer } from "@/lib/chipi-server"; import { NextResponse } from "next/server"; export async function POST(request: Request) { const { externalUserId } = await request.json(); const wallet = await chipiServer.wallets.createWallet({ externalUserId, }); return NextResponse.json(wallet); } ``` ## Usage in Server Actions ```typescript theme={null} "use server"; import { chipiServer } from "@/lib/chipi-server"; import { Chain, ChainToken } from "@chipi-stack/backend"; export async function getWalletBalance(userId: string) { const balance = await chipiServer.getTokenBalance({ externalUserId: userId, chainToken: ChainToken.USDC, chain: Chain.STARKNET, }); return balance; } ``` ## Related * [Backend SDK Quickstart](/sdk/backend/quickstart) — Full backend SDK reference * [Next.js Introduction](/sdk/nextjs/introduction) — Client-side Next.js SDK setup # Next.js with Better Auth Source: https://docs.chipipay.com/sdk/nextjs/gasless-better-auth-setup Learn how to integrate Chipi Pay with your Next.js application using Better Auth for authentication and secure wallet storage. First, follow the complete [Better Auth Next.js Quickstart Guide](https://www.better-auth.com/docs/installation) to set up your Better Auth project and Next.js integration. Then proceed to [Better Auth Basic Usage](https://www.better-auth.com/docs/basic-usage) to learn how to create your login and sign up pages. This will guide you through: * Creating Better Auth Tables and Schemas * Setting up your Next.js app with Better Auth * Configuring environment variables * Creating `auth.ts`, mounting the handler and client instance * Creating login and sign up pages ready to receive new users 🎉 **Come back here once you've completed the Better Auth setup!** 1. In your `auth.ts` file add the following configuration: ```typescript theme={null} import { betterAuth } from "better-auth"; import { Pool } from "pg"; import { jwt } from "better-auth/plugins"; export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL }), emailAndPassword: { enabled: true, }, plugins: [ jwt({ jwks: { keyPairConfig: { alg: "RS256", modulusLength: 2048, } }, jwt: { issuer: process.env.JWT_ISSUER , audience: process.env.JWT_AUDIENCE, expirationTime: "15m" } }) ] }); ``` 2. Migrate or generate your database:
          npx @better-auth/cli migrate
        
          npx @better-auth/cli generate
        
> **Note:** Before proceeding, check the [Better Auth CLI Docs](https://www.better-auth.com/docs/concepts/cli) to determine which CLI commands and setup fit your specific use case best.
You need to create an `auth-client.ts` - required for client side JWT functionality ```typescript theme={null} "use client"; import { createAuthClient } from "better-auth/react"; import { jwtClient } from "better-auth/client/plugins"; export const authClient = createAuthClient({ basePath: "/api/auth", plugins: [jwtClient()], }); ``` > Create a new `route.ts` file under `/api/[...all]/route.ts` with the following content. ```typescript theme={null} // Update the path to your auth file as needed import { toNextJsHandler } from "better-auth/next-js"; import { auth } from "../../../../auth"; export const { POST, GET } = toNextJsHandler(auth); ``` ```typescript theme={null} "use client"; import { authClient } from "./auth-client"; export function useBetterAuth() { const { data: session, isPending } = authClient.useSession(); const getToken = async () => { try { const { data, error } = await authClient.token(); if (error) { console.error("Failed to get JWT token:", error); return null; } return data?.token || null; } catch (error) { console.error("Error fetching JWT token:", error); return null; } }; const userId = session?.user?.id || null; return { getToken, userId, session, isPending, isSignedIn: !!session, }; } ``` 2. Deploy your app, then add your deployed application URL to your environment variables: ```bash theme={null} JWT_ISSUER=https://my-deployed-app-domain.com JWT_AUDIENCE=https://my-deployed-app-domain.com ``` First, install the required packages: ```bash theme={null} # Install Chipi Pay SDK npm install @chipi-stack/nextjs ``` 1. In your [`.env`](https://dashboard.chipipay.com/configure/jwks-endpoint) file, add your Chipi API keys: ```bash theme={null} NEXT_PUBLIC_CHIPI_API_KEY=your_chipi_api_public_key CHIPI_SECRET_KEY=your_chipi_api_secret_key ``` 2. Set up the Chipi Pay provider in your application: ```tsx theme={null} // app/layout.tsx import { ChipiProvider } from "@chipi-stack/nextjs"; export default function RootLayout({children}: { children: React.ReactNode; }) { return ( {children} ); } ``` Visit the [dashboard JWKS and JWT configuration](https://dashboard.chipipay.com/configure/jwks-endpoint), select `Better Auth` as your auth provider and paste your JWKS Endpoint URL. ```bash theme={null} https://my-deployed-app-domain.com/api/auth/jwks ``` Note: You do not need to manually create this /api/auth/jwks endpoint!\ It is automatically provided by the Better Auth JWT plugin and handled internally by the catch-all route handler. Remember to replace `https://my-deployed-app-domain.com` with the actual URL of where your app is deployed. Below you will see a simple example of how you can get and pass your tokens to the different Chipi hooks: ```typescript theme={null} "use client"; import { useGetWallet } from "@chipi-stack/nextjs"; import { useBetterAuth } from "@/hooks/use-better-auth"; export default function Home() { const { getToken, userId, isPending: authPending } = useBetterAuth(); const { data: wallet, isLoading } = useGetWallet({ getBearerToken: async () => { const token = await getToken(); if (!token) { throw new Error("User not authenticated"); } return token; }, params: { externalUserId: userId || "", }, }); if (authPending) { return
Loading authentication...
; } return (

Home page

{isLoading &&

Loading wallet...

} {wallet &&
{JSON.stringify(wallet, null, 2)}
}
); } ``` Note: If you have not yet created a wallet, you may see an error with a message like: message:"Wallet not found for externalUserId: user\_id, apiPublicKey: pk\_prod\_something" This error is expected and shows that your app is correctly contacting Chipi, but there is not yet a wallet for this user. Once you create a wallet, this error will disappear.
That's it! You should now have an initial working version of Chipi Pay integrated into your application. You can now start implementing various features like: * Wallet Creations * Sending tokens * Signing transactions * and more!
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Next.js with Clerk Source: https://docs.chipipay.com/sdk/nextjs/gasless-clerk-setup Learn how to integrate Chipi Pay with your Next.js application using Clerk for authentication and secure wallet storage. To get started with Chipi Pay in your Next.js application ```bash theme={null} npx create-next-app@latest my-chipi-app cd my-chipi-app ``` If you get `ERESOLVE` peer dependency errors when installing Clerk, run `npm install --legacy-peer-deps`. This is a known Clerk + React 19 compatibility issue. With Clerk, you can follow their [Quickstart Guide](https://clerk.com/docs/quickstarts/nextjs) to get started. First, install the required packages: ```bash theme={null} # Install Chipi Pay SDK npm install @chipi-stack/nextjs # Install Clerk npm install @clerk/nextjs ``` 1. Create a `.env` file in your project root and add your API keys: ```bash theme={null} NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key CLERK_SECRET_KEY=your_clerk_secret_key NEXT_PUBLIC_CHIPI_API_KEY=your_chipi_api_public_key CHIPI_SECRET_KEY=your_chipi_api_secret_key ``` Get your Chipi API keys from the [Chipi Dashboard](https://dashboard.chipipay.com) quickstart. Get your Clerk keys from the [Clerk Dashboard](https://dashboard.clerk.com/last-active?path=api-keys). 2. Set up the Chipi Pay provider in your application: ```tsx theme={null} // app/layout.tsx import { ClerkProvider } from "@clerk/nextjs"; import { ChipiProvider } from "@chipi-stack/nextjs"; export default function RootLayout({children}: { children: React.ReactNode; }) { return ( {children} ); } ``` Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Next.js with Custom Auth Source: https://docs.chipipay.com/sdk/nextjs/gasless-custom-auth-setup Integrate Chipi Pay with any OIDC-compliant auth provider (Auth0, Cognito, Okta, Keycloak) in your Next.js application. ## What is JWKS? JSON Web Key Set (JWKS) is a standard that allows Chipi Pay to verify your users' JWT tokens without ever seeing their credentials. Your auth provider exposes a public endpoint with its signing keys, and Chipi uses those keys to verify that tokens are authentic. Any OIDC-compliant provider (Auth0, Cognito, Okta, Keycloak, etc.) exposes a JWKS endpoint. ```bash theme={null} npm install @chipi-stack/nextjs ``` Add your Chipi API keys to `.env.local`: ```bash theme={null} NEXT_PUBLIC_CHIPI_API_KEY=your_chipi_api_public_key CHIPI_SECRET_KEY=your_chipi_api_secret_key ``` You can get your API keys from the [Chipi Dashboard](https://dashboard.chipipay.com/configure/api-keys). Wrap your app with `ChipiProvider` alongside your auth provider: ```tsx theme={null} // app/layout.tsx import { ChipiProvider } from "@chipi-stack/nextjs"; export default function RootLayout({children}: { children: React.ReactNode; }) { return ( {/* Your auth provider wraps ChipiProvider */} {children} ); } ``` Use your auth provider's SDK to get a JWT token, then pass it to Chipi hooks: ```typescript theme={null} // components/CreateWallet.tsx "use client"; import { useState } from "react"; import { useCreateWallet } from "@chipi-stack/nextjs"; // Replace with your auth provider's token method async function getBearerToken(): Promise { // Auth0: const { getAccessTokenSilently } = useAuth0(); // Cognito: const session = await Auth.currentSession(); // Okta: const { authState } = useOktaAuth(); // Keycloak: const { token } = useKeycloak(); return "your-jwt-token"; } export default function CreateWallet() { const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const handleCreateWallet = async () => { const bearerToken = await getBearerToken(); const response = await createWalletAsync({ params: { encryptKey }, bearerToken, }); console.log("Wallet created:", response.publicKey); }; return (
setEncryptKey(e.target.value)} />
); } ```
Register your auth provider's JWKS endpoint in the [Chipi Dashboard](https://dashboard.chipipay.com/configure/jwks-endpoint): 1. Go to Configure > Auth Provider 2. Select **Other** as your provider 3. Paste your provider's URL or JWKS endpoint 4. The dashboard will try OIDC discovery automatically 5. Click **Verify & Save** ### Compatible providers | Provider | JWKS URL pattern | | ----------- | ------------------------------------------------------------------------ | | Auth0 | `https://YOUR_DOMAIN/.well-known/jwks.json` | | AWS Cognito | `https://cognito-idp.REGION.amazonaws.com/POOL_ID/.well-known/jwks.json` | | Okta | `https://YOUR_DOMAIN/oauth2/default/v1/keys` | | Keycloak | `https://YOUR_HOST/realms/REALM/protocol/openid-connect/certs` |
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Next.js with Firebase Source: https://docs.chipipay.com/sdk/nextjs/gasless-firebase-setup Learn how to integrate Chipi Pay with your Next.js application using Firebase Auth for authentication and secure wallet storage. 1. First, in order to create authentication with Firebase you will need two things: first, a [Firebase account](https://firebase.google.com/) and second, a [Next.js](https://nextjs.org/docs/app/getting-started/installation) project. Once you have your account you must: * Go to your [console](https://console.firebase.google.com) and create a new Firebase project. * Once you have a project, proceed to the `Authentication` section. * Click on the `Sign in method` tab and enable the `Email/password` sign-in method and click `save`. * Go to `project settings`, scroll and under your apps section click on the `web app` usually displayed with a `` icon. * Register your app and click save. 2. Install the npm package in your project: ```bash theme={null} npm install firebase ``` On the Firebase Register web app page, you will also be provided something like this. Save it for later. ```typescript theme={null} // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: "apikey", authDomain: "domain", projectId: "your-id", storageBucket: "storage-bucket", messagingSenderId: "000000", appId: "app-id" }; // Initialize Firebase const app = initializeApp(firebaseConfig); ``` 1. Create a file named `firebase.ts` with the following configuration: ```typescript theme={null} import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, }; // Initialize Firebase export const firebaseApp = initializeApp(firebaseConfig) export const auth = getAuth(firebaseApp) ``` 2. Update your environment variables: From the firebaseConfig JSON object provided by Firebase when initializing your project, set up your environment variables in your `.env`. ```typescript theme={null} const firebaseConfig = { apiKey: "apikey", authDomain: "domain", projectId: "your-id", storageBucket: "storage-bucket", messagingSenderId: "000000", appId: "app-id" }; ``` ```typescript theme={null} NEXT_PUBLIC_FIREBASE_API_KEY=apikey NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=domain NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-id NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=storage-bucket NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=000000 NEXT_PUBLIC_FIREBASE_APP_ID=app-id ``` Install the required packages: ```bash theme={null} # Install Chipi Pay SDK npm install @chipi-stack/nextjs ``` 1. In your [`.env`](https://dashboard.chipipay.com/configure/jwks-endpoint) file, add your Chipi API keys: ```bash theme={null} NEXT_PUBLIC_CHIPI_API_KEY=your_chipi_api_public_key CHIPI_SECRET_KEY=your_chipi_api_secret_key ``` 2. Set up the Chipi Pay provider in your application: ```tsx theme={null} // app/layout.tsx import { ChipiProvider } from "@chipi-stack/nextjs"; export default function RootLayout({children}: { children: React.ReactNode; }) { return ( {children} ); } ``` Visit the [dashboard JWKS and JWT configuration](https://dashboard.chipipay.com/configure/jwks-endpoint), select `Firebase` as your auth provider, and paste this under the JWKS Endpoint URL. ```bash theme={null} https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys ``` Visit the [Firebase Authentication guide](https://firebase.google.com/docs/auth/web/password-auth) to learn how to use `createUserWithEmailAndPassword` and create your sign up and login! Below you will see a simple example of how you can get and pass your tokens to the different Chipi hooks: ```typescript theme={null} "use client"; import { useGetWallet } from "@chipi-stack/nextjs"; import { useEffect, useState } from "react"; import { onAuthStateChanged } from "firebase/auth"; import { auth } from "@/utils/firebase"; export default function Home() { const [userId, setUserId] = useState(null); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (user) => { if (user) { setUserId(user.uid); } }); return () => unsubscribe(); }, []); // Function to get Firebase ID token const getToken = async (): Promise => { const user = auth.currentUser; if (!user) { throw new Error("User not authenticated"); } const token = await user.getIdToken(); return token; }; const { data: wallet, isLoading } = useGetWallet({ getBearerToken: getToken, params: { externalUserId: userId || "", }, }); return (

Home page

{isLoading &&

Loading wallet...

} {wallet &&
{JSON.stringify(wallet, null, 2)}
}
); } ``` Note: If you have not yet created a wallet, you may see an error with a message like: message:"Wallet not found for externalUserId: user\_id, apiPublicKey: pk\_prod\_something" This error is expected and shows that your app is correctly contacting Chipi, but there is not yet a wallet for this user. Once you create a wallet, this error will disappear.
That's it! You should now have an initial working version of Chipi Pay integrated into your application. You can now start implementing various features like: * Wallet Creations * Sending tokens * Signing transactions * and more!
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Quickstart Source: https://docs.chipipay.com/sdk/nextjs/gasless-quickstart Quick setup guide for gasless transactions with Chipi SDK in Next.js applications Gasless transactions are only available in Starknet Mainnet. Go to [dashboard.chipipay.com](https://dashboard.chipipay.com) and create an account. The dashboard quickstart will walk you through: 1. **Creating your project** — gives you your API keys (Public Key + Secret Key) 2. **Configuring authentication** — connects your auth provider's JWKS endpoint 3. **Copying your `.env` variables** — the dashboard shows the exact snippet to paste The dashboard detects your auth provider (Clerk, Firebase, etc.) and auto-configures the JWKS endpoint for you. You'll need both keys for the next step: * **Public Key** (`pk_prod_xxxx`) — used client-side * **Secret Key** (`sk_prod_xxxx`) — used by the server-side `ChipiProvider` ```bash npm theme={null} npm install @chipi-stack/nextjs ``` ```bash yarn theme={null} yarn add @chipi-stack/nextjs ``` ```bash pnpm theme={null} pnpm add @chipi-stack/nextjs ``` 1. Add your **Public Key** to your environment variables. ```bash theme={null} NEXT_PUBLIC_CHIPI_API_KEY=pk_prod_xxxx CHIPI_SECRET_KEY=sk_prod_xxxx ``` 2. In your `layout.tsx`, wrap your app with the `ChipiProvider` component as shown below: ```tsx theme={null} // app/layout.tsx import { ChipiProvider } from "@chipi-stack/nextjs"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` You're ready to use the SDK! We recommend starting with our **convenience hooks** that simplify common operations: ### useChipiWallet - Unified Wallet Management ```tsx theme={null} import { useAuth } from "@clerk/nextjs"; import { useChipiWallet } from "@chipi-stack/nextjs"; function WalletComponent() { const { userId, getToken } = useAuth(); const { wallet, hasWallet, formattedBalance, createWallet, isLoadingWallet } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); if (!hasWallet) { return ( ); } return

Balance: ${formattedBalance} USDC

; } ``` Learn about all the options and return values ### useChipiSession - Gasless Session Keys (CHIPI wallets only) ```tsx theme={null} import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs"; function GaslessTransactions() { const { wallet } = useChipiWallet({ ... }); const { hasActiveSession, createSession, registerSession, executeWithSession, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Setup session once const handleSetup = async () => { await createSession(); await registerSession(); }; // Execute transactions without owner signature const handleTransfer = async () => { await executeWithSession([{ contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }]); }; } ``` Learn about session key management
That's it! You should now have an initial working version of Chipi Pay integrated into your application. Explore more features: * [All Available Hooks](/sdk/nextjs/hooks/use-create-wallet) - Complete hook reference * [Session Keys](/sdk/nextjs/hooks/use-chipi-session) - Enable gasless UX * [Buying Services](/sdk/nextjs/buying-services) - Accept crypto payments Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) to ask us anything on your mind.
# Next.js with Supabase Source: https://docs.chipipay.com/sdk/nextjs/gasless-supabase-setup Learn how to integrate Chipi Pay with your Next.js application using Supabase for authentication and secure wallet storage. First, follow the complete [Supabase Next.js Quickstart Guide](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) to set up your Supabase project and Next.js integration. This will guide you through: * Creating a Supabase project * Setting up your Next.js app with the Supabase template * Configuring environment variables * Creating Supabase client files **Come back here once you've completed the Supabase setup!** For enhanced security, migrate your Supabase project to use asymmetric JWT tokens as described in the [Supabase JWT Signing Keys blog post](https://supabase.com/blog/jwt-signing-keys). 1. Go to your Supabase project dashboard 2. Navigate to Settings > API 3. Under "JWT Settings", enable "Use asymmetric JWT signing" 4. Follow the migration steps provided in the dashboard This ensures your JWT tokens are signed with asymmetric keys for better security. Now install the Chipi Pay SDK: ```bash theme={null} npm install @chipi-stack/nextjs ``` Add your Chipi API key to your `.env.local` file (alongside your existing Supabase variables): ```bash theme={null} # Your existing Supabase variables NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=your_supabase_anon_key # Add the Chipi Variables NEXT_PUBLIC_CHIPI_API_KEY=your_chipi_api_public_key CHIPI_SECRET_KEY=your_chipi_api_secret_key ``` Set up the Chipi Pay provider in your application: ```tsx theme={null} // app/layout.tsx "use client"; import { ChipiProvider } from "@chipi-stack/nextjs"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` Create a wallet creation component using Supabase authentication: ```typescript theme={null} // app/components/CreateWallet.tsx "use client"; import { useState } from "react"; import { createClient } from "@/lib/supabase/client"; import { useCreateWallet } from "@chipi-stack/nextjs"; export default function CreateWallet() { const client = createClient(); const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const handleCreateWallet = async () => { if (!encryptKey) { setError("Please enter an encryption key"); return; } try { setError(""); setSuccess(""); const sessionData = (await client.auth.getSession()).data.session; const bearerToken = sessionData?.access_token; const user = sessionData?.user; if (!user || !bearerToken) { setError("Supabase user not found. Please sign in first."); return; } const response = await createWalletAsync({ params: { encryptKey, externalUserId: user.id, }, bearerToken, }); console.log("createWalletResponse", response); setSuccess( `Wallet created successfully! Public Key: ${response.publicKey}` ); setEncryptKey(""); } catch (error) { setError(error.message || "Failed to create wallet"); console.error("Error creating wallet:", error); } }; return (

Create Wallet

{error &&
{error}
} {success &&
{success}
}
setEncryptKey(e.target.value)} className="w-full p-2 border rounded" />
); } ```
That's it! You should now have an initial working version of Chipi Pay integrated into your application. You can now start implementing various features like: * Wallet Creations * Sending tokens * Signing transactions * and more!
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # useAddSessionKeyToContract Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-add-session-key-to-contract Register a session key on the CHIPI wallet smart contract. This one-time operation requires owner signature and enables the session key to execute transactions. Session keys only work with **CHIPI wallets**. This hook requires the owner's `encryptKey` (PIN) to sign the registration transaction. ## Usage ```typescript theme={null} const { addSessionKeyToContract, addSessionKeyToContractAsync, data, isLoading, error, isSuccess, reset } = useAddSessionKeyToContract(); ``` ### Parameters | Parameter | Type | Required | Description | | ---------------------- | ------------- | -------- | ------------------------------------------------------------------- | | `params.encryptKey` | string | Yes | Owner's PIN to decrypt wallet private key for signing | | `params.wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `params.sessionConfig` | SessionConfig | Yes | Session configuration (see below) | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### SessionConfig Structure ```typescript theme={null} { sessionPublicKey: string; // From useCreateSessionKey().data.publicKey validUntil: number; // Unix timestamp (seconds) maxCalls: number; // Maximum transactions allowed allowedEntrypoints: string[]; // Whitelisted function selectors (empty = all allowed) } ``` ### Return Value | Property | Type | Description | | ------------------------------ | ------------- | --------------------------------------------------- | | `addSessionKeyToContract` | function | Trigger registration (fire-and-forget) | | `addSessionKeyToContractAsync` | function | Trigger registration (returns Promise with tx hash) | | `data` | string | Transaction hash of registration | | `isLoading` | boolean | Whether registration is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether registration succeeded | | `reset` | function | Reset mutation state | ## Example Implementation ```typescript theme={null} import { useAddSessionKeyToContract, useCreateSessionKey, useGetWallet } from "@chipi-stack/chipi-react"; export function RegisterSession() { const { createSessionKeyAsync } = useCreateSessionKey(); const { addSessionKeyToContractAsync, isLoading, error } = useAddSessionKeyToContract(); const { data: wallet } = useGetWallet({ params: { externalUserId: "user-123" }, getBearerToken }); const [pin, setPin] = useState(''); const handleRegisterSession = async () => { const bearerToken = await getBearerToken(); // Step 1: Create session key const session = await createSessionKeyAsync({ encryptKey: pin, durationSeconds: 3600, }); // Step 2: Register on contract const txHash = await addSessionKeyToContractAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, // Allow 100 transactions allowedEntrypoints: [], // Empty = all functions allowed }, }, bearerToken, }); // Store session for later use localStorage.setItem('chipiSession', JSON.stringify(session)); console.log('Session registered:', txHash); }; return (

Register Session Key

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded mb-4" /> {error && (
Error: {error.message}
)}
); } ``` ## Restricting Session Permissions For enhanced security, whitelist specific function selectors: ```typescript theme={null} const txHash = await addSessionKeyToContractAsync({ params: { encryptKey: pin, wallet: walletData, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 10, // Only 10 calls allowed allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve ], }, }, bearerToken, }); ``` Limiting `allowedEntrypoints` restricts what functions the session can call. This is recommended for production to minimize attack surface. ## Error Handling Common errors: | Error | Cause | Solution | | ---------------------------------------- | -------------------------- | ---------------------------------------------------------- | | `Session keys require CHIPI wallet type` | Wallet is READY type | Create a new wallet with `walletType: "CHIPI"` | | `Invalid encryptKey` | Wrong PIN entered | Verify the PIN matches the one used during wallet creation | | `Session already exists` | Session already registered | Use `useGetSessionData` to check status | Registration requires a blockchain transaction. The transaction is gas-sponsored but may take 10-30 seconds to confirm. # useApprove Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-approve Grants permission to a smart contract to spend tokens from the user wallet. Required before staking or other delegated actions. ## Usage ```typescript theme={null} const { approve, approveAsync, data, isLoading, isError, error, isSuccess, reset } = useApprove(); ``` ### Parameters The hook accepts an object with: * `params` (`ApproveHookInput`): * `encryptKey` (string): User's decryption PIN * `wallet` (`WalletData`): Wallet public/private key pair * `contractAddress` (string): Token contract to approve * `spender` (string): Contract address to authorize * `amount` (number): Maximum spend allowance (will be converted to string internally) * `decimals` (number, optional): Token decimals (default: 18) * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `approve`: Function to trigger token approval (fire-and-forget) * `approveAsync`: Promise-based function that resolves with the transaction hash * `data`: Transaction hash of the approval (string | undefined) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the approval completed successfully * `reset`: Function to reset the mutation state ## Example Implementation for Approving VESU ```typescript theme={null} const VESU_CONTRACT = "0x037ae3f583c8d644b7556c93a04b83b52fa96159b2b0cbd83c14d3122aef80a2"; // Sample wallet data - replace with your wallet in production. // Keep walletType + signerKind from createWallet / useGetWallet: the SDK // routes by walletType, and omitting it defaults to "READY" (a SHHH wallet // would revert). const sampleWallet: { publicKey: string; encryptedPrivateKey: string; walletType: "SHHH" | "CHIPI" | "READY"; signerKind?: "STARK" | "WEBAUTHN_P256" | "EIP191_SECP256K1" | "ED25519"; } = { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data", // This would be encrypted data in production walletType: "SHHH", signerKind: "STARK", }; export function ApproveForm() { const { approveAsync, data, isLoading, isError, error } = useApprove(); const [wallet] = useState(sampleWallet); const [form, setForm] = useState({ pin: '', amount: '' }); const handleApprove = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken) { console.error('No bearer token available'); return; } try { await approveAsync({ params: { encryptKey: form.pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: wallet.walletType, // routing-critical (SHHH/CHIPI/READY) signerKind: wallet.signerKind, }, contractAddress: VESU_CONTRACT, spender: VESU_CONTRACT, // Authorizing VESU contract amount: Number(form.amount), decimals: 18 }, bearerToken, }); } catch (err) { console.error('Approval failed:', err); } }; return (

Approve Token Access

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, amount: e.target.value})} className="w-full p-2 border rounded-md" required />
{data && (

Approval TX: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always use encrypted private keys * Never store or transmit raw private keys * Implement proper PIN validation * Use secure storage for wallet data * Consider implementing approval limits ## Error Handling * Handle insufficient token balance * Validate contract addresses * Check for existing approvals * Monitor gas fees * Implement retry logic for failed transactions Approvals are specific to each token-contract pair. You'll need to re-approve when interacting with new contracts. ## Related Hooks * **useTransfer** - For moving tokens * **useCallAnyContract** - For custom contract interactions that need prior approval # useCallAnyContract Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-call-any-contract Executes any StarkNet contract method. Handles all contract interactions not covered by specific hooks. ## Usage ```typescript theme={null} const { callAnyContract, callAnyContractAsync, data, isLoading, isError, error, isSuccess, reset } = useCallAnyContract(); ``` ### Parameters The hook accepts an object with: * `params` (`CallAnyContractParams`): * `encryptKey` (string): User's decryption PIN (or passkey-derived key) * `wallet` (`WalletData`): the full wallet object — `publicKey`, `encryptedPrivateKey`, and (routing-critical) **`walletType`** + **`signerKind`** from `createWallet` / `useGetWallet`. Omitting `walletType` defaults to `"READY"` and reverts on a SHHH wallet. * `contractAddress` (string): Target contract address * `calls` (`Call[]`): Array of contract calls with `contractAddress`, `entrypoint`, and `calldata` * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `callAnyContract`: Function to trigger contract call (fire-and-forget) * `callAnyContractAsync`: Promise-based function that resolves with the transaction hash * `data`: Transaction hash of the call (string | undefined) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the call completed successfully * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} export function GenericContractForm() { const { callAnyContractAsync, data, isLoading, isError, error } = useCallAnyContract(); const [form, setForm] = useState({ pin: '', contractAddress: '', entrypoint: '', calldata: '' }); const handleCall = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getBearerToken(); try { await callAnyContractAsync({ params: { encryptKey: form.pin, wallet: walletData, contractAddress: form.contractAddress, calls: [ { contractAddress: form.contractAddress, entrypoint: form.entrypoint, calldata: JSON.parse(form.calldata || '[]') } ], }, bearerToken, }); } catch (err) { console.error('Contract call failed:', err); } }; return (

Contract Interaction

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, contractAddress: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, entrypoint: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, calldata: e.target.value})} className="w-full p-2 border rounded-md" placeholder='Example: [123, "0xabc...", true]' />
{data && (

TX Hash: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Verify contract addresses * Validate calldata format * Use encrypted private keys * Implement proper PIN validation * Review contract ABI before calling ## Error Handling * Handle invalid contract addresses * Validate calldata format * Monitor gas fees * Implement retry logic for failed calls Use with caution. Direct contract calls require deep understanding of the target contract's ABI and security implications. ## Related Hooks * **useApprove** - For token approvals * **useTransfer** - For token transfers # useChipiSession Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-chipi-session Unified session key management for gasless UX - create, register, execute, and revoke sessions with a single hook. ## Overview `useChipiSession` provides a unified API for managing session keys, enabling gasless transactions without requiring the owner's signature for each operation. This hook combines all session-related operations: * **Session creation** (local keypair generation) * **Session registration** (on-chain) * **Transaction execution** (using session key) * **Session revocation** (on-chain) * **Status checking** (remaining calls, expiration) Session keys only work with **CHIPI wallets**. READY wallets do not support session keys. ## Prerequisites Before using `useChipiSession`, you need: 1. A **CHIPI wallet** (check `wallet.walletType === "CHIPI"` or `wallet.supportsSessionKeys`) 2. The user's **encryption key (PIN)** 3. An **authentication token** (from Clerk, Firebase, etc.) ## Session Lifecycle ```mermaid theme={null} stateDiagram-v2 [*] --> none: No session none --> created: createSession() created --> active: registerSession() active --> active: executeWithSession() active --> revoked: revokeSession() active --> expired: Time/calls exhausted ``` | State | Description | | --------- | ---------------------------------------------------- | | `none` | No session exists | | `created` | Session created locally, not yet registered on-chain | | `active` | Session registered and usable | | `expired` | Session has expired (time or call limit) | | `revoked` | Session has been revoked on-chain | ## Quick Start ```tsx theme={null} import { useAuth } from "@clerk/nextjs"; import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs"; function SessionComponent() { const { userId, getToken } = useAuth(); const [pin, setPin] = useState(""); const { wallet } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); const { session, sessionState, hasActiveSession, remainingCalls, createSession, registerSession, executeWithSession, isCreating, isRegistering, isExecuting, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Setup session (create + register) const handleSetup = async () => { await createSession(); await registerSession(); }; // Execute a transfer using the session const handleTransfer = async () => { await executeWithSession([{ contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }]); }; return (

Session: {sessionState}

{hasActiveSession &&

Calls remaining: {remainingCalls}

} {!hasActiveSession ? ( ) : ( )}
); } ``` ## Configuration Options ```typescript theme={null} interface UseChipiSessionConfig { // Required wallet: SessionWallet | null | undefined; encryptKey: string; getBearerToken: () => Promise; // Optional storedSession?: SessionKeyData | null; onSessionCreated?: (session: SessionKeyData) => void | Promise; defaultDurationSeconds?: number; // Default: 21600 (6 hours) defaultMaxCalls?: number; // Default: 1000 autoCheckStatus?: boolean; // Default: true } ``` | Option | Type | Default | Description | | ------------------------ | ----------------------- | -------- | -------------------------------- | | `wallet` | `SessionWallet` | Required | Wallet data (must be CHIPI type) | | `encryptKey` | `string` | Required | User's PIN for signing | | `getBearerToken` | `() => Promise` | Required | Auth token function | | `storedSession` | `SessionKeyData` | `null` | Previously stored session | | `onSessionCreated` | `(session) => void` | - | Callback to persist new session | | `defaultDurationSeconds` | `number` | `21600` | Session duration (6 hours) | | `defaultMaxCalls` | `number` | `1000` | Max calls per session | | `autoCheckStatus` | `boolean` | `true` | Auto-fetch on-chain status | ## Return Values ### Session Data | Property | Type | Description | | ------------------ | ------------------------ | -------------------------------- | | `session` | `SessionKeyData \| null` | Current session data | | `sessionStatus` | `SessionDataResponse` | On-chain session status | | `sessionState` | `SessionState` | Lifecycle state | | `hasActiveSession` | `boolean` | Whether session is usable | | `isSessionExpired` | `boolean` | Whether session has expired | | `remainingCalls` | `number \| undefined` | Calls left in session | | `supportsSession` | `boolean` | Whether wallet supports sessions | ### Actions | Method | Returns | Description | | --------------------------- | ------------------------- | ---------------------------------- | | `createSession(config?)` | `Promise` | Create new session locally | | `registerSession(config?)` | `Promise` | Register on-chain (returns txHash) | | `revokeSession()` | `Promise` | Revoke on-chain (returns txHash) | | `executeWithSession(calls)` | `Promise` | Execute calls (returns txHash) | | `clearSession()` | `void` | Clear local session state | | `refetchStatus()` | `Promise` | Refresh on-chain status | ### Loading States | Property | Description | | ----------------- | ---------------------------- | | `isCreating` | Creating session locally | | `isRegistering` | Registering session on-chain | | `isRevoking` | Revoking session on-chain | | `isExecuting` | Executing transaction | | `isLoadingStatus` | Fetching on-chain status | ## Persisting Sessions `SessionKeyData` contains an `encryptedPrivateKey`. Where you store it matters — the SDK does not persist sessions itself; this is your responsibility. **Do not store `SessionKeyData` in Clerk `unsafeMetadata`.** `unsafeMetadata` is client-writable and embedded in the Clerk session JWT, which means the encrypted session key is exposed to the browser on every session refresh. Combined with a low-entropy user PIN (e.g. 6 digits = 10⁶ combinations), the ciphertext is brute-forceable offline by anyone who obtains a valid session token. Use a server-side route that writes to Clerk **`privateMetadata`** (server-only, not in the JWT), or store the session in your own database. See the recommended pattern below. ### Recommended: server-side route + `privateMetadata` Persist `SessionKeyData` from a server route after verifying the user's auth token. Chipi's backend SDK ships JWKS verification (`iss` + `aud` validation) for Clerk, Firebase, BetterAuth, and generic providers — use it to authenticate the route. See the [Gasless setup guides](/sdk/nextjs/gasless-clerk-setup) for JWKS configuration. **1. Server route — `app/api/chipi/session/route.ts`:** ```ts theme={null} import { auth, clerkClient } from "@clerk/nextjs/server"; import { NextResponse } from "next/server"; import type { SessionKeyData } from "@chipi-stack/types"; // GET — return the stored session (server-only, never exposed in JWT) export async function GET() { const { userId } = await auth(); if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 }); const client = await clerkClient(); const user = await client.users.getUser(userId); const session = user.privateMetadata?.chipiSession as SessionKeyData | undefined; return NextResponse.json({ session: session ?? null }); } // POST — persist a newly created session export async function POST(req: Request) { const { userId } = await auth(); if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 }); const body = await req.json().catch(() => null); if (!body || typeof body !== "object" || !("session" in body)) { return NextResponse.json({ error: "invalid_session_payload" }, { status: 400 }); } const { session } = body as { session: SessionKeyData }; // updateUserMetadata performs a deep merge — using updateUser() here would // replace the entire privateMetadata object and clobber any other keys. const client = await clerkClient(); await client.users.updateUserMetadata(userId, { privateMetadata: { chipiSession: session }, }); return NextResponse.json({ ok: true }); } ``` **2. Client — load and persist via the route:** ```tsx theme={null} "use client"; import { useEffect, useState } from "react"; import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs"; import type { SessionKeyData } from "@chipi-stack/types"; function PersistentSession() { const { wallet } = useChipiWallet({ ... }); const [storedSession, setStoredSession] = useState(null); // Hydrate from server on mount useEffect(() => { let cancelled = false; (async () => { try { const r = await fetch("/api/chipi/session"); if (!r.ok) return; const d = (await r.json()) as { session?: SessionKeyData | null }; if (!cancelled) setStoredSession(d.session ?? null); } catch { if (!cancelled) setStoredSession(null); } })(); return () => { cancelled = true; }; }, []); const { session, hasActiveSession, createSession, registerSession } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, storedSession, onSessionCreated: async (newSession) => { const r = await fetch("/api/chipi/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session: newSession }), }); if (!r.ok) throw new Error(`Failed to persist session: ${r.status}`); }, }); // Session is restored on mount — and never leaks via the JWT } ``` ### Reduce the brute-force surface Even with server-side storage, the encryption strength of `encryptedPrivateKey` depends on the encryption key: * **PIN-only wallets** — a 6-digit PIN has \~20 bits of entropy; an attacker who obtains the ciphertext can crack it offline in seconds. Treat the encrypted session key as sensitive even at rest. * **Passkey wallets (recommended)** — the encryption key is hardware-backed via WebAuthn PRF and never leaves the device's secure element. This is the production-grade default. See the [PIN → passkey migration guide](/sdk/guides/pin-to-passkey-migration). ### Minimal session whitelist When registering a session, restrict `allowedEntrypoints` to the smallest set your flow actually needs. Avoid including broad approval entrypoints (`approve`, `set_approval_for_all`) unless required — a session with these permissions can drain tokens within the validity window if compromised. ## Executing Transactions The `executeWithSession` method accepts an array of Starknet calls: ```typescript theme={null} // Single transfer await executeWithSession([{ contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }]); // Batch multiple calls await executeWithSession([ { contractAddress: USDC_CONTRACT, entrypoint: "approve", calldata: [spender, amount, "0x0"], }, { contractAddress: DEX_CONTRACT, entrypoint: "swap", calldata: [...], }, ]); ``` ## Custom Session Configuration ### Custom Duration ```typescript theme={null} // Create session valid for 1 hour await createSession({ durationSeconds: 3600 }); ``` ### Custom Max Calls ```typescript theme={null} // Register with 500 max calls await registerSession({ maxCalls: 500 }); ``` ### Allowed Entrypoints Restrict which contract methods the session can call: ```typescript theme={null} await registerSession({ allowedEntrypoints: [ "0x..." // Only allow specific function selectors ], }); ``` ## Error Handling ```tsx theme={null} const { error, session } = useChipiSession({...}); // Check for errors if (error) { console.error("Session error:", error.message); } // Handle specific errors const handleSetup = async () => { try { await createSession(); await registerSession(); } catch (err) { if (err.message.includes("does not support sessions")) { alert("Please use a CHIPI wallet for sessions"); } else if (err.message.includes("expired")) { alert("Session has expired, please create a new one"); } else { alert(`Error: ${err.message}`); } } }; ``` ## Complete Example ```tsx theme={null} "use client"; import { useState, useEffect } from "react"; import { useAuth } from "@clerk/nextjs"; import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs"; import type { SessionKeyData } from "@chipi-stack/types"; export function GaslessTransfers() { const { getToken, userId } = useAuth(); const [pin, setPin] = useState(""); const [recipient, setRecipient] = useState(""); const [amount, setAmount] = useState(""); const { wallet, formattedBalance } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); // Hydrate session from a server-side route that reads Clerk privateMetadata // (NOT unsafeMetadata — see the "Persisting Sessions" section above). const [storedSession, setStoredSession] = useState(null); useEffect(() => { let cancelled = false; (async () => { try { const r = await fetch("/api/chipi/session"); if (!r.ok) return; const d = (await r.json()) as { session?: SessionKeyData | null }; if (!cancelled) setStoredSession(d.session ?? null); } catch { if (!cancelled) setStoredSession(null); } })(); return () => { cancelled = true; }; }, []); const { session, sessionState, hasActiveSession, remainingCalls, supportsSession, createSession, registerSession, executeWithSession, revokeSession, isCreating, isRegistering, isExecuting, isRevoking, error, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, storedSession, onSessionCreated: async (newSession) => { const r = await fetch("/api/chipi/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session: newSession }), }); if (!r.ok) throw new Error(`Failed to persist session: ${r.status}`); }, }); if (!wallet?.supportsSessionKeys) { return

Sessions require a CHIPI wallet

; } return (
{/* PIN Input */} setPin(e.target.value)} maxLength={4} /> {/* Session Status */}

State: {sessionState}

{hasActiveSession && (

Remaining calls: {remainingCalls}

)}
{/* Session Setup */} {!hasActiveSession && ( )} {/* Transfer Form */} {hasActiveSession && (
setRecipient(e.target.value)} /> setAmount(e.target.value)} />
)} {/* Revoke Session */} {hasActiveSession && ( )} {/* Error Display */} {error && (

{error.message}

)}
); } ``` # useChipiWallet Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-chipi-wallet All-in-one hook for wallet management - combines wallet fetching, creation, and balance checking into a single unified API. ## Overview `useChipiWallet` is a convenience hook that simplifies wallet management by combining multiple operations into one: * **Wallet fetching** (replaces `useGetWallet`) * **Wallet creation** (replaces `useCreateWallet`) * **Balance checking** (replaces `useGetTokenBalance`) Use this hook when you want a streamlined API. Use the individual hooks (`useGetWallet`, `useCreateWallet`, `useGetTokenBalance`) when you need more granular control. ## Quick Start ```typescript theme={null} import { useAuth } from "@clerk/nextjs"; import { useChipiWallet } from "@chipi-stack/nextjs"; function WalletComponent() { const { userId, getToken } = useAuth(); const { wallet, hasWallet, balance, formattedBalance, createWallet, isCreating, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); if (isLoadingWallet) return
Loading...
; if (!hasWallet) { return ( ); } return (

Address: {wallet?.shortAddress}

Balance: ${formattedBalance} USDC

); } ``` ## Configuration Options ```typescript theme={null} interface UseChipiWalletConfig { // Required externalUserId: string | null | undefined; getBearerToken: () => Promise; // Optional defaultToken?: ChainToken; // Default: "USDC" enabled?: boolean; // Default: true } ``` | Option | Type | Default | Description | | ---------------- | ----------------------- | -------- | ------------------------------------------------------- | | `externalUserId` | `string \| null` | Required | User ID from your auth provider (Clerk, Firebase, etc.) | | `getBearerToken` | `() => Promise` | Required | Function to get the auth token | | `defaultToken` | `ChainToken` | `"USDC"` | Token to fetch balance for | | `enabled` | `boolean` | `true` | Whether to fetch wallet on mount | ## Return Values ### Wallet Data | Property | Type | Description | | ----------------- | -------------------------------------- | ------------------------------------ | | `wallet` | `ChipiWalletData \| null \| undefined` | Wallet data with computed properties | | `hasWallet` | `boolean` | Whether the user has a wallet | | `isLoadingWallet` | `boolean` | Wallet fetch loading state | | `walletError` | `Error \| null` | Any error from fetching | ### Balance Data | Property | Type | Description | | ------------------ | -------------------------------------- | ------------------------------------ | | `balance` | `GetTokenBalanceResponse \| undefined` | Raw balance data | | `formattedBalance` | `string` | Formatted balance (e.g., "1,234.56") | | `isLoadingBalance` | `boolean` | Balance fetch loading state | ### Create Wallet | Property | Type | Description | | --------------- | ------------------------------------------- | ------------------------ | | `createWallet` | `(params) => Promise` | Create a new wallet | | `isCreating` | `boolean` | Creation loading state | | `createdWallet` | `CreateWalletResponse \| undefined` | Last created wallet data | ### Actions | Method | Description | | ------------------ | ------------------------------- | | `refetchWallet()` | Refetch wallet data | | `refetchBalance()` | Refetch balance data | | `refetchAll()` | Refetch both wallet and balance | ## Computed Wallet Properties The `wallet` object includes these computed properties: ```typescript theme={null} interface ChipiWalletData extends GetWalletResponse { supportsSessionKeys: boolean; // true for CHIPI wallets shortAddress: string; // Truncated address for display } ``` ## Example: Full Implementation with Clerk ```tsx theme={null} "use client"; import { useState } from "react"; import { useAuth, SignInButton, SignedIn, SignedOut } from "@clerk/nextjs"; import { useChipiWallet } from "@chipi-stack/nextjs"; export function WalletDashboard() { const { userId, getToken } = useAuth(); const [pin, setPin] = useState(""); const { wallet, hasWallet, formattedBalance, createWallet, isCreating, isLoadingWallet, refetchAll, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); const handleCreateWallet = async (walletType: "CHIPI" | "READY") => { if (!pin || pin.length < 4) { alert("Please enter a 4-digit PIN"); return; } try { const result = await createWallet({ encryptKey: pin, walletType, }); alert(`Wallet created! Address: ${result.publicKey}`); } catch (err) { console.error(err); alert("Failed to create wallet"); } }; return (
{isLoadingWallet ? (

Loading wallet...

) : hasWallet ? (

Wallet Address

{wallet?.shortAddress}

Balance

${formattedBalance} USDC

Session Keys

{wallet?.supportsSessionKeys ? "Supported" : "Not Supported"}

) : (
setPin(e.target.value)} maxLength={4} className="input" />
)}
); } ``` ## Wallet Types When creating a wallet, you can specify the type: | Type | Session Keys | Description | | ------- | ------------ | ---------------------------------------------------- | | `CHIPI` | ✅ Yes | OpenZeppelin account with SNIP-9 session key support | | `READY` | ❌ No | Argent X compatible wallet | ```typescript theme={null} // Create a CHIPI wallet (supports session keys) await createWallet({ encryptKey: pin, walletType: "CHIPI", }); // Create a READY wallet (no session keys) await createWallet({ encryptKey: pin, walletType: "READY", }); ``` ## Migration from Individual Hooks If you're currently using separate hooks, here's how to migrate: ### Before ```typescript theme={null} const { data: wallet, isLoading } = useGetWallet({ params: { externalUserId: userId }, getBearerToken: getToken, enabled: !!userId, }); const { createWalletAsync } = useCreateWallet(); const { data: balance } = useGetTokenBalance({ params: { walletPublicKey: wallet?.publicKey, ... }, getBearerToken: getToken, enabled: !!wallet?.publicKey, }); ``` ### After ```typescript theme={null} const { wallet, hasWallet, balance, formattedBalance, createWallet, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); ``` `useChipiWallet` automatically handles the dependency chain - it only fetches balance after the wallet is loaded. # useCreateSessionKey Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-create-session-key Generate a session keypair locally for temporary, delegated transaction execution. Session keys enable gasless, frictionless UX without requiring the owner private key for each transaction. Session keys only work with **CHIPI wallets** - not READY wallets. Make sure your wallet was created with `walletType: "CHIPI"`. ## Usage ```typescript theme={null} const { createSessionKey, createSessionKeyAsync, data, isLoading, error, isSuccess, reset } = useCreateSessionKey(); ``` ### Parameters | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------ | | `encryptKey` | string | Yes | User's PIN/password to encrypt the session private key | | `durationSeconds` | number | No | Session duration in seconds (default: 21600 = 6 hours) | ### Return Value Returns an object containing: | Property | Type | Description | | ----------------------- | -------------- | ------------------------------------------------ | | `createSessionKey` | function | Trigger session key generation (fire-and-forget) | | `createSessionKeyAsync` | function | Trigger session key generation (returns Promise) | | `data` | SessionKeyData | Generated session key data | | `isLoading` | boolean | Whether generation is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether generation succeeded | | `reset` | function | Reset mutation state | ### SessionKeyData Structure ```typescript theme={null} { publicKey: string; // Session public key (hex) encryptedPrivateKey: string; // AES encrypted with encryptKey validUntil: number; // Unix timestamp (seconds) } ``` ## Example Implementation ```typescript theme={null} import { useCreateSessionKey } from "@chipi-stack/chipi-react"; export function CreateSession() { const { createSessionKeyAsync, data: session, isLoading, error } = useCreateSessionKey(); const [pin, setPin] = useState(''); const handleCreateSession = async () => { try { const sessionData = await createSessionKeyAsync({ encryptKey: pin, durationSeconds: 3600, // 1 hour session }); // Store session in client-side storage (NOT your database!) localStorage.setItem('chipiSession', JSON.stringify(sessionData)); console.log('Session created:', sessionData.publicKey); } catch (err) { console.error('Failed to create session:', err); } }; return (

Create Session Key

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded" />
{error && (
Error: {error.message}
)} {session && (

Session created! Expires: {new Date(session.validUntil * 1000).toLocaleString()}

)}
); } ``` ## Security Considerations **Never store session keys in your backend database.** This defeats the purpose of self-custodial security. Store only in client-side secure storage. | Platform | Recommended Storage | | ---------------- | ---------------------------------------------- | | **Web** | `localStorage` or `sessionStorage` | | **iOS** | Secure Enclave / Keychain | | **Android** | Android Keystore | | **React Native** | `expo-secure-store` or `react-native-keychain` | ## Best Practices 1. **Set short durations** - Use 1-6 hours, not days 2. **Clear on logout** - Always remove session from storage when user logs out 3. **Register on-chain** - After creating, use `useAddSessionKeyToContract` to register it This hook only generates the session keypair locally. You must call `useAddSessionKeyToContract` to register it on-chain before it can be used for transactions. # useCreateWallet Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-create-wallet Creates a new Argent-compatible wallet on StarkNet. This hook deploys the wallet contract behind the scenes and uses Avnus gasless to sponsor gas fees, resulting in a frictionless onboarding experience. ## Usage ```typescript theme={null} const { createWalletAsync, data, isLoading, error } = useCreateWallet(); ``` ### Parameters The mutation accepts an object with: * `params` (`CreateWalletParams`): * `encryptKey` (string): PIN or passkey-derived key used to encrypt the private key * `externalUserId` (string): Your application's unique identifier for the user * `chain` (`Chain`): The blockchain network. Use `Chain.STARKNET` * `walletType` (`"SHHH" | "CHIPI" | "READY"`, optional): **defaults to `"SHHH"`** (the pluggable-signer ShhhAccount — guardian recovery, multisig). `"CHIPI"` is the legacy account that supports session keys; `"READY"` is Argent X compatible. Whatever you create here, persist `walletType` (and `signerKind`) and pass them back on every transfer/approve/call. * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey (via `@chipi-stack/chipi-passkey`) * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `createWallet`: Function to trigger wallet creation (fire-and-forget) * `createWalletAsync`: Promise-based function that resolves with the wallet data * `data`: The created wallet (`CreateWalletResponse`) — flat object with `publicKey`, `encryptedPrivateKey`, `walletType`, `chain`, etc. * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if wallet creation succeeded * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useCreateWallet, Chain } from "@chipi-stack/nextjs"; export function CreateWallet() { const { userId, getToken } = useAuth(); const { createWalletAsync, data, isLoading, error } = useCreateWallet(); const [pin, setPin] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken || !userId) { console.error('No bearer token or user ID available'); return; } try { const wallet = await createWalletAsync({ params: { encryptKey: pin, externalUserId: userId, chain: Chain.STARKNET, // Omit walletType to get the default SHHH account, or set it // explicitly. Use "CHIPI" only if you need legacy session keys. walletType: "SHHH", }, bearerToken, }); alert('Wallet created successfully!'); } catch (err) { console.error('Wallet creation failed:', err); } }; return (

Create New Wallet

setPin(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500" required minLength={4} />
{error && (
Error: {error.message}
)} {data && (

Wallet Details

View Contract

Address: {data.publicKey}

)}
); } ``` ## Security Considerations * PINs should always be collected client-side * Never log or store raw PIN values * Use secure encryption before any transmission * Store encrypted private key securely * Associate with user session (not persistent storage) ## Error Handling * Catch and handle RPC connection errors * Monitor gasless API limits * Implement retry logic for failed deployments Wallet creation is free! Gas fees are covered by our gasless integration. # useExecuteWithSession Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-execute-with-session Execute gasless transactions using a session key instead of the owner private key. Enables frictionless UX for gaming, DeFi automation, and more. This hook uses the session key signature (4-element format) instead of the owner signature. The session must be registered on-chain first via `useAddSessionKeyToContract`. ## Usage ```typescript theme={null} const { executeWithSession, executeWithSessionAsync, data, isLoading, error, isSuccess, reset } = useExecuteWithSession(); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------- | -------------- | -------- | --------------------------------------------------------- | | `params.encryptKey` | string | Yes | PIN to decrypt the **session** private key | | `params.wallet` | WalletData | Yes | Wallet object (session executes on behalf of this wallet) | | `params.session` | SessionKeyData | Yes | Session key data from `useCreateSessionKey` | | `params.calls` | Call\[] | Yes | Array of contract calls to execute | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### Call Structure ```typescript theme={null} { contractAddress: string; // Target contract address entrypoint: string; // Function name to call calldata: string[]; // Function arguments } ``` ### Return Value | Property | Type | Description | | ------------------------- | ------------- | ------------------------------------------------ | | `executeWithSession` | function | Trigger execution (fire-and-forget) | | `executeWithSessionAsync` | function | Trigger execution (returns Promise with tx hash) | | `data` | string | Transaction hash | | `isLoading` | boolean | Whether execution is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether execution succeeded | | `reset` | function | Reset mutation state | ## Example: Token Transfer ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/chipi-react"; // Native USDC on Starknet mainnet (not USDC.e bridged) const USDC_ADDRESS = "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb"; export function SessionTransfer() { const { executeWithSessionAsync, isLoading, error, data: txHash } = useExecuteWithSession(); const [amount, setAmount] = useState(''); const [recipient, setRecipient] = useState(''); const handleTransfer = async () => { const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession')!); const pin = await promptForPin(); // Your PIN input method try { const hash = await executeWithSessionAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, session, calls: [{ contractAddress: USDC_ADDRESS, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }], }, bearerToken, }); console.log('Transfer executed:', hash); } catch (err) { console.error('Transfer failed:', err); } }; return (

Transfer with Session

setRecipient(e.target.value)} placeholder="Recipient address (0x...)" className="w-full p-2 border rounded" /> setAmount(e.target.value)} placeholder="Amount (in smallest units)" className="w-full p-2 border rounded" />
{error && (
Error: {error.message}
)} {txHash && ( )}
); } ``` ## Example: Gaming Actions ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/chipi-react"; const GAME_CONTRACT = "0x..."; export function GameBoard() { const { executeWithSessionAsync, isLoading } = useExecuteWithSession(); const makeMove = async (moveData: string) => { const session = JSON.parse(localStorage.getItem('chipiSession')!); const bearerToken = await getBearerToken(); // Execute instantly - no wallet popup! await executeWithSessionAsync({ params: { encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, session, calls: [{ contractAddress: GAME_CONTRACT, entrypoint: "make_move", calldata: [moveData], }], }, bearerToken, }); }; return (
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((cell) => ( ))}
); } ``` ## Multi-Call Transactions Execute multiple calls in a single transaction: ```typescript theme={null} const txHash = await executeWithSessionAsync({ params: { encryptKey: pin, wallet: walletData, session, calls: [ // Approve spending { contractAddress: USDC_ADDRESS, entrypoint: "approve", calldata: [SWAP_CONTRACT, amount, "0x0"], }, // Execute swap { contractAddress: SWAP_CONTRACT, entrypoint: "swap", calldata: [USDC_ADDRESS, ETH_ADDRESS, amount, minReceived], }, ], }, bearerToken, }); ``` ## Error Handling | Error | Cause | Solution | | ----------------------------------------------- | ----------------------------- | -------------------------------------------------- | | `Session execution only supports CHIPI wallets` | Wallet is READY type | Use a CHIPI wallet | | `Session has expired` | `validUntil` timestamp passed | Create and register a new session | | `Session not found` | Not registered on-chain | Call `useAddSessionKeyToContract` first | | `Max calls exceeded` | `remainingCalls` is 0 | Create a new session with higher `maxCalls` | | `Entrypoint not allowed` | Function not in whitelist | Register session with correct `allowedEntrypoints` | Session transactions decrement `remainingCalls` on the contract. Monitor usage with `useGetSessionData` and create new sessions before exhaustion. For the best UX, pre-create sessions during onboarding and store the PIN securely. This allows instant transactions without prompting for PIN each time. # useGetSessionData Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-get-session-data Query session key status from the CHIPI wallet smart contract. Returns whether the session is active, remaining calls, and allowed entrypoints. ## Usage ```typescript theme={null} const { data, isLoading, isError, error, isSuccess, refetch } = useGetSessionData(params, options); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------- | ------- | -------- | ---------------------------------------- | | `params.walletAddress` | string | Yes | Wallet address to query | | `params.sessionPublicKey` | string | Yes | Session public key to query | | `options.enabled` | boolean | No | Enable/disable the query (default: true) | Pass `null` as params to disable the query. ### Return Value | Property | Type | Description | | ----------- | ------------------- | ----------------------------- | | `data` | SessionDataResponse | Session status from contract | | `isLoading` | boolean | Whether query is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether query succeeded | | `refetch` | function | Manually refetch session data | ### SessionDataResponse Structure ```typescript theme={null} { isActive: boolean; // Whether session is currently valid validUntil: number; // Expiration timestamp (Unix seconds) remainingCalls: number; // Transactions remaining before exhausted allowedEntrypoints: string[]; // Whitelisted function selectors } ``` ## Example Implementation ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; export function SessionStatus() { // Get session from local storage const session = JSON.parse(localStorage.getItem('chipiSession') || 'null'); const { data: sessionData, isLoading, refetch } = useGetSessionData( session ? { walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, } : null ); if (!session) { return
No active session
; } if (isLoading) { return
Loading session status...
; } return (

Session Status

Status: {sessionData?.isActive ? "Active" : "Inactive"}
Expires: {sessionData?.validUntil ? new Date(sessionData.validUntil * 1000).toLocaleString() : "N/A"}
Remaining Calls: {sessionData?.remainingCalls ?? 0}
Restrictions: {sessionData?.allowedEntrypoints?.length ? `${sessionData.allowedEntrypoints.length} functions` : "All functions"}
{!sessionData?.isActive && (
Session is inactive. Create a new session to continue.
)}
); } ``` ## Conditional Rendering Based on Session ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; export function GameInterface() { const session = JSON.parse(localStorage.getItem('chipiSession') || 'null'); const { data: sessionData } = useGetSessionData( session ? { walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, } : null ); // Check if session is valid before allowing actions const canPlay = sessionData?.isActive && sessionData?.remainingCalls > 0; return (
{canPlay ? ( ) : (

Session expired or no calls remaining

)}
); } ``` ## Auto-Refresh Session Status ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; import { useEffect } from "react"; export function useSessionMonitor(walletAddress: string, sessionPublicKey: string) { const { data, refetch } = useGetSessionData({ walletAddress, sessionPublicKey, }); // Refresh every 30 seconds useEffect(() => { const interval = setInterval(() => { refetch(); }, 30000); return () => clearInterval(interval); }, [refetch]); return { isActive: data?.isActive ?? false, remainingCalls: data?.remainingCalls ?? 0, expiresAt: data?.validUntil ? new Date(data.validUntil * 1000) : null, }; } ``` This is a **read-only** query that doesn't require gas or signatures. It directly calls the smart contract's view function. If the session doesn't exist on-chain, `isActive` will be `false` with `remainingCalls: 0`. This is normal for sessions that were never registered or have been revoked. # useGetTokenBalance Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-get-token-balance Fetches the on-chain token balance for a wallet address. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTokenBalance, } = useGetTokenBalance({ params: { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey: "0x...", }, getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ------------------------ | ----------------------- | -------- | --------------------------------------------------- | | `params.chainToken` | `ChainToken` | Yes | Token identifier. Use `ChainToken.USDC` | | `params.chain` | `Chain` | Yes | Blockchain network. Use `Chain.STARKNET` | | `params.walletPublicKey` | `string` | No | Wallet address to check balance for | | `params.externalUserId` | `string` | No | External user ID (alternative to `walletPublicKey`) | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ------------------- | --------------------------------------------- | --------------------------------------------- | | `data` | `GetTokenBalanceResponse \| undefined` | Balance data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => void` | Re-run the query | | `fetchTokenBalance` | `(input) => Promise` | Imperatively fetch balance with custom params | ## Example Implementation ```typescript theme={null} import { useGetTokenBalance, Chain, ChainToken } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function TokenBalance({ walletPublicKey }: { walletPublicKey: string }) { const { getToken } = useAuth(); const { data, isLoading, error } = useGetTokenBalance({ params: { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey, }, getBearerToken: getToken, }); if (isLoading) return

Loading balance...

; if (error) return

Error: {error.message}

; return (

Balance: {data?.balance} USDC

); } ``` Either `walletPublicKey` or `externalUserId` must be provided to identify the wallet. # useGetTransaction Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-get-transaction Fetches a single transaction by its hash or internal ID. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransaction, } = useGetTransaction({ hashOrId: "0x...", getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ---------------- | ----------------------- | -------- | ------------------------------------------------- | | `hashOrId` | `string` | Yes | Transaction hash (`0x...`) or internal ID | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ------------------ | ------------------------------------ | ------------------------------------- | | `data` | `Transaction \| undefined` | Transaction data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => Promise` | Re-run the query | | `fetchTransaction` | `(input) => Promise` | Imperatively fetch with custom params | ## Example Implementation ```typescript theme={null} import { useGetTransaction } from "@chipi-stack/chipi-react"; export function TransactionDetail({ txHash, getBearerToken, }: { txHash: string; getBearerToken: () => Promise; }) { const { data: tx, isLoading, error } = useGetTransaction({ hashOrId: txHash, getBearerToken, }); if (isLoading) return

Loading transaction...

; if (error) return

Error: {error.message}

; return (

Hash: {tx?.transactionHash}

Status: {tx?.status}

From: {tx?.senderAddress}

To: {tx?.destinationAddress}

Amount: {tx?.amount} {tx?.token}

); } ``` # useGetTransactionList Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-get-transaction-list Fetches a paginated list of transactions with optional date and address filters. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransactionList, } = useGetTransactionList({ query: { page: 1, limit: 10, walletAddress: "0x...", }, getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ---------------------- | ----------------------- | -------- | ------------------------------------------------- | | `query.page` | `number` | No | Page number (default: `1`) | | `query.limit` | `number` | No | Results per page (default: `10`) | | `query.walletAddress` | `string` | No | Filter by wallet address | | `query.calledFunction` | `string` | No | Filter by contract function name | | `query.day` | `number` | No | Filter by day (1–31) | | `query.month` | `number` | No | Filter by month (1–12) | | `query.year` | `number` | No | Filter by year | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ---------------------- | ---------------------------------------------------- | ------------------------------------- | | `data` | `PaginatedResponse \| undefined` | Paginated transaction results | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => void` | Re-run the query | | `fetchTransactionList` | `(input) => Promise>` | Imperatively fetch with custom params | ## Example Implementation ```typescript theme={null} import { useGetTransactionList } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; import { useState } from "react"; export function TransactionList({ walletAddress }: { walletAddress: string }) { const { getToken } = useAuth(); const [page, setPage] = useState(1); const { data, isLoading, error } = useGetTransactionList({ query: { page, limit: 10, walletAddress, }, getBearerToken: getToken, }); if (isLoading) return

Loading transactions...

; if (error) return

Error: {error.message}

; return (
    {data?.data.map((tx) => (
  • {tx.transactionHash} — {tx.status}
  • ))}
Page {page}
); } ``` # useGetTransactionStatus Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-get-transaction-status Fetches and polls the on-chain status of a transaction. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransactionStatus, } = useGetTransactionStatus({ hash: "0x...", getBearerToken: getToken, refetchInterval: 3000, // polls every 3s, stops on terminal status }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ----------------- | ----------------------- | -------- | ------------------------------------------------------------- | | `hash` | `string` | Yes | Transaction hash (`0x...`) | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `refetchInterval` | `number \| false` | No | Polling interval in ms (default: `3000`). `false` to disable. | | `queryOptions` | `UseQueryOptions` | No | React Query options | ### Return Value | Property | Type | Description | | ------------------------ | ----------------------------------------------- | ---------------------------- | | `data` | `TransactionStatusResponse \| undefined` | Transaction status data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => Promise` | Re-run the query | | `fetchTransactionStatus` | `(input) => Promise` | Imperatively fetch | Polling automatically stops when the status reaches a terminal state: `ACCEPTED_ON_L1`, `REJECTED`, or `REVERTED`. ## Example Implementation ```typescript theme={null} import { useGetTransactionStatus } from "@chipi-stack/chipi-react"; import { OnChainTxStatus } from "@chipi-stack/types"; export function TransactionTracker({ txHash, getBearerToken, }: { txHash: string; getBearerToken: () => Promise; }) { const { data, isLoading } = useGetTransactionStatus({ hash: txHash, getBearerToken, refetchInterval: 3000, }); if (isLoading) return

Checking status...

; const confirmed = data?.status === OnChainTxStatus.ACCEPTED_ON_L2 || data?.status === OnChainTxStatus.ACCEPTED_ON_L1; return (

Status: {data?.status}

{data?.blockNumber &&

Block: {data.blockNumber}

} {data?.revertReason &&

Revert reason: {data.revertReason}

} {confirmed &&

Transaction confirmed!

}
); } ``` # useGetWallet Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-get-wallet Get an authenticated user Wallet ## Usage ```typescript theme={null} const { fetchWallet, data, isLoading, error } = useGetWallet(); ``` ### Parameters * `params` (`GetWalletParams`): * `externalUserId` (string): User ID from your auth provider * `getBearerToken` (`() => Promise`): Function returning the auth token * `queryOptions` (`UseQueryOptions`, optional): React Query options (e.g. `enabled`) ### Return Value Returns an object containing: * `fetchWallet`: Function to manually fetch wallet data with custom params * `data`: The wallet object (`publicKey`, `encryptedPrivateKey`, `walletType`, etc.) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `isSuccess`: Boolean indicating if the query succeeded * `error`: Any error that occurred during the process * `refetch`: Function to re-run the query ## Example Implementation ```typescript theme={null} import { useGetWallet } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function WalletPage(){ const { userId, getToken } = useAuth(); const { data: wallet, isLoading, error, fetchWallet } = useGetWallet({ params: { externalUserId: userId || "", }, getBearerToken: async () => { const token = await getToken(); if (!token) throw new Error("No token found"); return token; }, queryOptions: { enabled: Boolean(userId), }, }); // Or use fetchWallet manually: const loadWallet = async () => { if (!userId) return; try { const token = await getToken(); if (!token) throw new Error("No token found"); const wallet = await fetchWallet({ params: { externalUserId: userId, }, getBearerToken: async () => token, }); console.log("Wallet loaded:", wallet); } catch (error) { console.error("Error loading wallet:", error); } }; return (
{isLoading &&

Loading wallet...

} {error &&

Error: {error.message}

} {wallet && (

Public Key: {wallet.publicKey}

Normalized Public Key: {wallet.normalizedPublicKey}

)}
); } ``` Chipi never stores your decrypted sensitive data. All private keys and authentication tokens remain secure and are never accessible to Chipi servers. # useMigrateWalletToPasskey Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-migrate-wallet-to-passkey Migrates an existing PIN-encrypted wallet to passkey (biometric) authentication. ## Usage ```typescript theme={null} const { migrateWalletToPasskey, migrateWalletToPasskeyAsync, data, isLoading, isError, isSuccess, error, reset, } = useMigrateWalletToPasskey(); ``` ### Parameters The mutation accepts an object with: | Parameter | Type | Required | Description | | ---------------- | ------------ | -------- | --------------------------------------------------------------- | | `wallet` | `WalletData` | Yes | The current wallet object (`publicKey` + `encryptedPrivateKey`) | | `oldEncryptKey` | `string` | Yes | The user's current PIN used to decrypt the wallet | | `externalUserId` | `string` | Yes | Your app's user ID — used as the passkey username | | `bearerToken` | `string` | Yes | Auth token from your provider | ### Return Value | Property | Type | Description | | ----------------------------- | -------------------------------------------------- | --------------------------- | | `migrateWalletToPasskey` | `(input) => void` | Fire-and-forget migration | | `migrateWalletToPasskeyAsync` | `(input) => Promise` | Promise-based migration | | `data` | `MigrateWalletToPasskeyResult \| undefined` | Migration result | | `isLoading` | `boolean` | True while migrating | | `isError` | `boolean` | True if migration failed | | `isSuccess` | `boolean` | True if migration succeeded | | `error` | `Error \| null` | Error details | | `reset` | `() => void` | Reset mutation state | ### `MigrateWalletToPasskeyResult` | Property | Type | Description | | -------------- | ------------ | ------------------------------------------------------ | | `success` | `boolean` | Whether migration succeeded | | `wallet` | `WalletData` | Updated wallet with new passkey-encrypted private key | | `credentialId` | `string` | The passkey credential ID (store this for future auth) | ## How It Works 1. A new passkey is created in the browser/device (triggers biometric prompt) 2. The wallet's private key is decrypted using `oldEncryptKey` (the PIN) 3. The private key is re-encrypted using the passkey-derived key 4. The updated wallet is returned — **persist the new wallet object and `credentialId`** After migration, the old PIN (`oldEncryptKey`) will no longer work. Store the updated wallet object and `credentialId` securely. ## Example Implementation ```typescript theme={null} import { useMigrateWalletToPasskey } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; import { useState } from "react"; export function MigrateToPasskey({ currentWallet, userId }: { currentWallet: WalletData; userId: string; }) { const { getToken } = useAuth(); const [currentPin, setCurrentPin] = useState(""); const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey(); const handleMigrate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; try { const result = await migrateWalletToPasskeyAsync({ wallet: currentWallet, oldEncryptKey: currentPin, externalUserId: userId, bearerToken, }); // Persist the updated wallet and credentialId localStorage.setItem("wallet", JSON.stringify(result.wallet)); localStorage.setItem("passkeyCredentialId", result.credentialId); alert("Successfully migrated to passkey!"); } catch (err) { console.error("Migration failed:", err); } }; return (
setCurrentPin(e.target.value)} /> {error &&

Error: {error.message}

}
); } ``` ## Related * [Use Passkeys Guide](/sdk/react/use-passkeys) — Full passkey setup walkthrough * **useCreateWallet** — Create a wallet with passkey from the start (set `usePasskey: true`) # useRecordSendTransaction Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-record-transaction Records a send transaction in the Chipi system for tracking and webhook notifications. ## Usage ```typescript theme={null} const { recordSendTransactionAsync, data, isLoading, error } = useRecordSendTransaction(); ``` ### Parameters The hook accepts an object with: * `params` (`RecordSendTransactionParams`): * `transactionHash` (string): The hash of the transaction to record * `chain` (`Chain`): The blockchain network. Use `Chain.STARKNET` * `expectedSender` (string): The expected sender wallet address * `expectedRecipient` (string): The expected recipient wallet address * `expectedToken` (`ChainToken`): The expected token. Use `ChainToken.USDC` * `expectedAmount` (string): The expected transaction amount * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `recordSendTransaction`: Function to record transaction (fire-and-forget) * `recordSendTransactionAsync`: Promise-based function that resolves with the transaction record * `data`: The recorded transaction data (`Transaction | undefined`) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the recording completed successfully * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} import { useRecordSendTransaction } from "@chipi-stack/chipi-react"; import { Chain, ChainToken } from "@chipi-stack/types"; export function RecordTransactionForm() { const { recordSendTransactionAsync, data, isLoading, isError, error } = useRecordSendTransaction(); const { getToken } = useAuth(); const handleRecordTransaction = async () => { try { const token = await getToken(); if (!token) throw new Error("No token found"); // After a transfer, record the transaction const transactionHash = "0x..."; // From your transfer transaction const senderWallet = { publicKey: "0x123..." }; const merchant = "0x456..."; const amountUsd = "100.00"; await recordSendTransactionAsync({ params: { transactionHash, chain: Chain.STARKNET, expectedSender: senderWallet.publicKey, expectedRecipient: merchant, expectedToken: ChainToken.USDC, expectedAmount: amountUsd, }, bearerToken: token, }); } catch (err) { console.error('Recording transaction failed:', err); } }; return (

Record Transaction

{data && (

Transaction Recorded: {data.id}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always verify recipient addresses * Use encrypted private keys * Implement proper PIN validation * Monitor transaction status ## Error Handling * Handle insufficient token balance * Validate wallet addresses * Monitor gas fees * Implement retry logic for failed transactions Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Hooks * **useTransfer** - Execute the transfer before recording it * **useGetTransactionList** - List all recorded transactions # useRevokeSessionKey Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-revoke-session-key Revoke a session key from the CHIPI wallet smart contract. After revocation, the session key can no longer execute transactions. Always revoke active sessions when a user logs out to prevent unauthorized access. ## Usage ```typescript theme={null} const { revokeSessionKey, revokeSessionKeyAsync, data, isLoading, error, isSuccess, reset } = useRevokeSessionKey(); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------- | ---------- | -------- | ------------------------------------------------------------------- | | `params.encryptKey` | string | Yes | Owner's PIN to decrypt wallet private key for signing | | `params.wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `params.sessionPublicKey` | string | Yes | Public key of the session to revoke | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### Return Value | Property | Type | Description | | ----------------------- | ------------- | ------------------------------------------------- | | `revokeSessionKey` | function | Trigger revocation (fire-and-forget) | | `revokeSessionKeyAsync` | function | Trigger revocation (returns Promise with tx hash) | | `data` | string | Transaction hash of revocation | | `isLoading` | boolean | Whether revocation is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether revocation succeeded | | `reset` | function | Reset mutation state | ## Example Implementation ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/chipi-react"; export function RevokeSession() { const { revokeSessionKeyAsync, isLoading, error, isSuccess } = useRevokeSessionKey(); const [pin, setPin] = useState(''); const handleRevoke = async () => { const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession') || '{}'); if (!session.publicKey) { alert('No active session found'); return; } try { const txHash = await revokeSessionKeyAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionPublicKey: session.publicKey, }, bearerToken, }); // Clear from local storage localStorage.removeItem('chipiSession'); console.log('Session revoked:', txHash); } catch (err) { console.error('Failed to revoke session:', err); } }; return (

Revoke Session

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded mb-4" /> {error && (
Error: {error.message}
)} {isSuccess && (
Session revoked successfully!
)}
); } ``` ## Secure Logout Handler Always revoke sessions during logout: ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/chipi-react"; export function useSecureLogout() { const { revokeSessionKeyAsync } = useRevokeSessionKey(); const logout = async (wallet: WalletData, pin: string) => { const sessionStr = localStorage.getItem('chipiSession'); if (sessionStr) { const session = JSON.parse(sessionStr); const bearerToken = await getBearerToken(); try { // Revoke session on-chain before logout await revokeSessionKeyAsync({ params: { encryptKey: pin, wallet: { ...wallet, walletType: "CHIPI" }, sessionPublicKey: session.publicKey, }, bearerToken, }); } catch (error) { console.warn("Failed to revoke session:", error); // Continue with logout even if revoke fails } // Clear from local storage localStorage.removeItem('chipiSession'); } // Proceed with auth logout await authProvider.signOut(); }; return { logout }; } ``` If revocation fails (e.g., network error), the session will still expire at its `validUntil` timestamp. However, it's best practice to always attempt revocation. ## When to Revoke | Scenario | Should Revoke? | | ------------------------- | --------------------------------------------- | | User logs out | ✅ Yes - always | | Session expired naturally | ❌ No - already inactive | | User requests new session | ⚠️ Optional - old session will expire | | Security concern | ✅ Yes - immediately | | App uninstall | N/A - Can't revoke, session expires naturally | Revocation requires a blockchain transaction which may take 10-30 seconds. Don't block the logout flow - fire the revocation and continue. # useSyncOnChainTransfers Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-sync-on-chain-transfers Sync on-chain token transfers into the database. Discovers external receives not made through the SDK. ## Usage ```typescript theme={null} const { syncTransfers, syncTransfersAsync, data, isLoading, isError, error, isSuccess, reset } = useSyncOnChainTransfers(); ``` ### Parameters `syncTransfers` / `syncTransfersAsync` accept an object with: | Parameter | Type | Required | Description | | --------------- | -------- | -------- | --------------------------------------- | | `walletAddress` | `string` | Yes | Wallet public key to sync transfers for | | `bearerToken` | `string` | Yes | Bearer token for authentication | ### Return Value | Property | Type | Description | | -------------------- | -------------------------------------------------- | ------------------------------ | | `syncTransfers` | `(input) => void` | Trigger sync (fire-and-forget) | | `syncTransfersAsync` | `(input) => Promise` | Promise-based sync | | `data` | `SyncOnChainTransfersResponse \| undefined` | Sync result | | `isLoading` | `boolean` | Whether sync is in progress | | `isError` | `boolean` | Whether an error occurred | | `error` | `Error \| null` | Error from last sync attempt | | `isSuccess` | `boolean` | Whether last sync succeeded | | `reset` | `() => void` | Reset mutation state | ### SyncOnChainTransfersResponse | Field | Type | Description | | -------- | -------- | ---------------------------------------------- | | `synced` | `number` | New transactions discovered and saved to DB | | `total` | `number` | Total transfers found on-chain for this wallet | ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useSyncOnChainTransfers, useGetTransactionList, } from "@chipi-stack/nextjs"; export function TransactionHistory({ wallet }: { wallet: { publicKey: string } }) { const { getToken } = useAuth(); const { syncTransfersAsync, isLoading: isSyncing } = useSyncOnChainTransfers(); const { data: txList, refetch } = useGetTransactionList({ query: { walletAddress: wallet.publicKey }, getBearerToken: getToken, }); const handleSync = async () => { const token = await getToken(); if (!token) return; // Discover external transfers (e.g., USDC received from Argent X) const result = await syncTransfersAsync({ walletAddress: wallet.publicKey, bearerToken: token, }); console.log(`Found ${result.synced} new transfers`); // Transaction list cache is automatically invalidated — refetch happens }; return (
{txList?.data.map((tx) => (
{tx.subType === "receive" ? "Received" : "Sent"} {tx.amount} {tx.token} {tx.transactionHash.slice(0, 12)}...
))}
); } ``` ## How It Works 1. Calls `POST /transactions/sync-on-chain` on the backend 2. Backend queries Voyager API for all token transfers for the wallet 3. Compares with existing DB records by transaction hash 4. Saves new transfers to the `Transaction` table (read-through cache) 5. Returns `{ synced, total }` 6. Invalidates `useGetTransactionList` cache so new transfers appear immediately After sync, `useGetTransactionList` returns both SDK-initiated transfers AND external receives. ## When to Use * **On wallet load**: Sync once when user opens their wallet to discover any external receives * **Pull-to-refresh**: Let users manually trigger sync * **After receiving funds**: If user expects incoming funds from an external wallet Synced transfers are cached in the database. Subsequent `useGetTransactionList` calls return them without re-querying Voyager. ## Limitations * Only discovers token transfers (ERC-20). Contract interactions without transfers are not synced. * Voyager API rate limits apply. Avoid calling sync on every render. * First sync for a wallet with many transfers may take a few seconds (paginated, max 500 transfers per sync). ## Related * [useGetTransactionList](/sdk/nextjs/hooks/use-get-transaction-list) — Read the transaction list (includes synced transfers) * [useGetTransaction](/sdk/nextjs/hooks/use-get-transaction) — Get a single transaction by hash * [useGetTransactionStatus](/sdk/nextjs/hooks/use-get-transaction-status) — Poll transaction status # useTransfer Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-transfer Transfers tokens from the user wallet to another address. Uses Avnus gasless to cover gas fees. ## Usage ```typescript theme={null} const { transfer, transferAsync, data, isLoading, isError, error, isSuccess, reset, } = useTransfer(); ``` ### Parameters * `transfer` / `transferAsync` both accept an object with: * `params` (`TransferHookInput`): * `encryptKey` (string): PIN used to decrypt the private key. * `wallet` (`WalletData`): the wallet object. **Pass the full object from `createWallet` / `useGetWallet`** — `publicKey`, `encryptedPrivateKey`, **`walletType`**, and **`signerKind`**. `walletType` is routing-critical: SHHH wallets execute through a different path, and omitting it defaults to `"READY"`, which **reverts** on a SHHH account. `signerKind` is SHHH-only (defaults to `"STARK"`). * `token` (`ChainToken`): Token identifier (e.g. `ChainToken.USDC`). * `usePasskey` (boolean, optional): When `true`, the hook authenticates with passkey internally (requires `externalUserId`). Do not use with external `setupPasskey`/`authenticate` calls. * `otherToken` (optional): Custom token configuration with: * `contractAddress` (string): ERC-20 token contract address. * `decimals` (number): Token decimals. * `recipient` (string): Destination wallet address. * `amount` (number): Transfer amount (will be converted to a string internally). * `bearerToken` (string): Bearer token for authentication (e.g. from your auth provider). ### Return Value Returns an object containing: * `transfer`: Function to trigger the transfer (fire-and-forget style). * `transferAsync`: Promise-based function that resolves with the transaction hash. * `data`: Transaction hash of the transfer (string | undefined). * `isLoading`: Boolean indicating if the operation is in progress. * `isError`: Boolean indicating if an error occurred. * `error`: Error instance when `isError` is true, otherwise `null`. * `isSuccess`: Boolean indicating if the transfer completed successfully. * `reset`: Function to reset the mutation state. ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useTransfer, ChainToken } from "@chipi-stack/chipi-react"; export function TransferForm() { const { getToken } = useAuth(); const { transferAsync, data, isLoading, isError, error } = useTransfer(); const [form, setForm] = useState({ pin: '', recipient: '', amount: '' }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken) { console.error('No bearer token available'); return; } try { await transferAsync({ params: { amount: Number(form.amount), encryptKey: form.pin, // Use the wallet you got from createWallet / useGetWallet, with its // walletType + signerKind. These route the transfer correctly — // omitting walletType defaults to "READY" and a SHHH wallet reverts. wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: wallet.walletType, // "SHHH" | "CHIPI" | "READY" signerKind: wallet.signerKind, // SHHH only; defaults to "STARK" }, token: ChainToken.USDC, recipient: form.recipient }, bearerToken, }); } catch (err) { console.error('Transfer failed:', err); } }; return (

Transfer Tokens

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, recipient: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, amount: e.target.value})} className="w-full p-2 border rounded-md" required />
{data && (

TX Hash: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always verify recipient addresses * Use encrypted private keys * Implement proper PIN validation * Monitor transaction status ## Error Handling * Handle insufficient token balance * Validate wallet addresses * Monitor gas fees * Implement retry logic for failed transactions Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Hooks * **useApprove** - Required before transferring new token types * **useCallAnyContract** - For custom contract interactions # useUpdateWalletEncryption Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-update-wallet-encryption Updates a wallet's encrypted private key after the user changes their passkey or PIN. ## Usage ```typescript theme={null} const { updateWalletEncryption, updateWalletEncryptionAsync, data, isLoading, isError, error, isSuccess, reset, } = useUpdateWalletEncryption(); ``` ### Parameters The mutation accepts an object with: | Parameter | Type | Required | Description | | ------------------------ | -------- | -------- | --------------------------------------------------------------- | | `externalUserId` | `string` | Yes | Your app's user ID | | `newEncryptedPrivateKey` | `string` | Yes | The wallet private key re-encrypted with the new passkey or PIN | | `publicKey` | `string` | No | Wallet public key; required when the user has multiple wallets | | `bearerToken` | `string` | Yes | Auth token from your provider | ### Return Value | Property | Type | Description | | ----------------------------- | ---------------------------------------------------- | ------------------------------------ | | `updateWalletEncryption` | `(input) => void` | Fire-and-forget update | | `updateWalletEncryptionAsync` | `(input) => Promise` | Promise-based update | | `data` | `UpdateWalletEncryptionResponse \| undefined` | Response from the API | | `isLoading` | `boolean` | True while the update is in progress | | `isError` | `boolean` | True if the update failed | | `isSuccess` | `boolean` | True if the update succeeded | | `error` | `Error \| null` | Error details | | `reset` | `() => void` | Reset mutation state | ### `UpdateWalletEncryptionResponse` | Property | Type | Description | | --------- | --------- | ---------------------------- | | `success` | `boolean` | Whether the update succeeded | ## How It Works 1. The client re-encrypts the wallet private key with a new passkey or PIN 2. `updateWalletEncryption` sends the new ciphertext to the Chipi backend 3. The backend replaces the stored encrypted private key 4. The wallet query cache is automatically invalidated so subsequent reads return fresh data After calling this hook, any previous passkey or PIN used to encrypt the private key will no longer work. Make sure the new encryption is persisted before discarding the old key material. ## Example Implementation ```typescript theme={null} import { useUpdateWalletEncryption } from "@chipi-stack/chipi-react"; import { usePasskeySetup } from "@chipi-stack/chipi-passkey/hooks"; import { useAuth } from "@clerk/nextjs"; export function RotatePasskey({ wallet, userId }: { wallet: WalletData; userId: string; }) { const { getToken } = useAuth(); const { setupPasskey } = usePasskeySetup(); const { updateWalletEncryptionAsync, isLoading, isSuccess, error, } = useUpdateWalletEncryption(); const handleRotate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; try { // Create a new passkey (triggers biometric prompt). setupPasskey needs // userId + userName and returns an object — destructure encryptKey. const { encryptKey: newEncryptKey } = await setupPasskey(userId, userName); // Re-encrypt the private key client-side with the new key. There is no // `wallet.privateKey` — decrypt `wallet.encryptedPrivateKey` with the OLD // key first to recover the plaintext, then re-encrypt with the new one. // Keep plaintext key material in-memory only — never log or persist it. const plaintextKey = decrypt(wallet.encryptedPrivateKey, oldEncryptKey); const newEncryptedPrivateKey = encrypt( plaintextKey, newEncryptKey ); await updateWalletEncryptionAsync({ externalUserId: userId, newEncryptedPrivateKey, publicKey: wallet.publicKey, bearerToken, }); alert("Passkey rotated successfully!"); } catch (err) { console.error("Failed to rotate passkey:", err); } }; return (
{isSuccess &&

Encryption updated.

} {error &&

Error: {error.message}

}
); } ``` ## Related * [useMigrateWalletToPasskey](/sdk/react/hooks/use-migrate-wallet-to-passkey) — Migrate from PIN to passkey * [Use Passkeys Guide](/sdk/react/use-passkeys) — Full passkey setup walkthrough # useX402Payment Source: https://docs.chipipay.com/sdk/nextjs/hooks/use-x402-payment React hook for the x402 payment protocol. Wraps fetch with automatic HTTP 402 payment handling using USDC on Starknet. ## Usage ```typescript theme={null} const { x402Fetch, payFetch, signPayment, isPaying, lastTxHash, lastAmount, lastPayment, totalSpent, error, paymentCount, } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "1.00", }); ``` ### Configuration | Parameter | Type | Required | Description | | ------------------- | ------------------------------------------ | -------- | ----------------------------------------------------------------- | | `wallet` | `WalletData \| null` | Yes | Wallet data with `publicKey` and `encryptedPrivateKey` | | `encryptKey` | `string` | Yes | PIN or passkey-derived key for signing | | `getBearerToken` | `() => Promise` | No | Function returning auth token (for session key path) | | `bearerToken` | `string` | No | Static bearer token (alternative to `getBearerToken`) | | `maxPaymentAmount` | `string` | No | Max USDC per payment in human-readable format (e.g. `"1.00"`) | | `maxAmount` | `string` | No | Alias for `maxPaymentAmount` | | `allowedRecipients` | `string[]` | No | Whitelist of recipient addresses. Empty = allow all | | `session` | `SessionKeyData \| null` | No | Active session key for automatic payments without owner signature | | `onPaymentComplete` | `(txHash: string, amount: string) => void` | No | Callback fired after successful payment | ### Return Value | Property | Type | Description | | -------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `x402Fetch` | `(url: string, init?: RequestInit) => Promise` | Drop-in `fetch` replacement — auto-handles 402 | | `payFetch` | Same as `x402Fetch` | Alias | | `signPayment` | `(requirement: PaymentRequirement) => Promise` | Manual payment signing for custom flows | | `isPaying` | `boolean` | Whether a payment is in progress | | `lastTxHash` | `string \| null` | Last payment transaction hash (session payments only — standard wallet returns `null`) | | `lastAmount` | `string \| null` | Last payment amount in human-readable USDC | | `lastPayment` | `LastPaymentInfo \| null` | Last payment details: `{ txHash, amount, timestamp }` | | `totalSpent` | `string` | Total USDC spent this session (e.g. `"0.030000"`) | | `error` | `Error \| null` | Error from last payment attempt | | `paymentCount` | `number` | Total payments made this session | ## Example Implementation ```typescript theme={null} import { useX402Payment, ChainToken } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function PremiumApiClient({ wallet, pin }: { wallet: WalletData; pin: string }) { const { getToken } = useAuth(); const { x402Fetch, isPaying, lastAmount, paymentCount, totalSpent, error, } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "0.10", // Max 0.10 USDC per request onPaymentComplete: (txHash, amount) => { console.log(`Paid ${amount} USDC`); }, }); const handleQuery = async () => { // x402Fetch works like fetch — if the server returns 402, // the hook automatically signs and retries with X-PAYMENT header const response = await x402Fetch("https://api.example.com/premium/data"); const data = await response.json(); console.log("Response:", data); }; return (
{lastAmount &&

Last payment: {lastAmount} USDC

}

Total spent: {totalSpent} USDC ({paymentCount} payments)

{error &&

Error: {error.message}

}
); } ``` ## Payment Flows ### Standard Wallet (no session) 1. `x402Fetch` makes initial request 2. Server returns `402` with `PAYMENT-REQUIRED` header 3. Hook parses requirement, validates against `maxPaymentAmount` and `allowedRecipients` 4. Signs SNIP-12 typed data locally via `signTypedData()` (no on-chain transaction) 5. Retries with `X-PAYMENT` header containing the signed payload 6. Facilitator settles the payment on-chain ### Session Key (automatic payments) 1. Same flow, but signs using session key via `executeTransactionWithSession()` 2. Payment executes on-chain immediately (pre-signed) 3. `lastTxHash` contains the actual transaction hash 4. No owner signature needed — session key handles it ## Security * `maxPaymentAmount` enforced before signing — prevents overpayment * `allowedRecipients` whitelist — prevents payment to unauthorized addresses * Only `"exact"` scheme and `"starknet-mainnet"` network accepted * Only USDC asset accepted (native USDC contract address validated) * Amount validation uses BigInt math (no float precision issues) Standard wallet payments return `lastTxHash: null` because the signature is SNIP-12 off-chain — the facilitator settles on-chain later. Session payments return the actual transaction hash. # NextJS SDK Introduction Source: https://docs.chipipay.com/sdk/nextjs/introduction Learn about the NextJS SDK capabilities and what you can build with it # NextJS SDK Our NextJS SDK allows you to create self-custodial wallets and sign transactions without the need for gas. You can then use the Chipi Dashboard to manage your wallets and transactions, as well as see analytics on your transactions. The Chipi SDK is the only SDK in the Starknet ecosystem designed to work with any auth provider. ## ✨ Key Features ### Complete Wallet Suite * **Wallet Management** - Create and manage wallets * **Transaction Signing** - Sign transactions without the need for gas * **Social Login Integration** - Bring your own auth provider (Clerk, Firebase, etc.) * **Automatic Wallet Creation** - No manual key management ### Advanced Wallet Features * **Sessions** - Allow users to sign once and use their wallet for multiple transactions * **Biometrics** - Use biometrics to sign transactions * **MPC Support** - Coming soon! ### Developer Experience * **TypeScript Support** - Full type safety * **React Hooks** - Hooks for common use cases * **Quickstart** - We provide a quickstart guide to get you started ## 🏗️ Architecture Our Next.js SDK is a simple wrapper around the Chipi API. It is designed to be used with the Chipi Dashboard, which is a web-based dashboard for setting up your project. Simply follow the steps in the [Quickstart Guide](/sdk/nextjs/gasless-quickstart) to get started. If you wish to use an unlisted programming language, you can use the Chipi API directly. Send us a message on [Telegram](https://t.me/+e2qjHEOwImkyZDVh) and we will help you get started. ## 📱 Platform Support * **Web Applications** - Full feature support ## 🚦 Getting Started Ready to build? Start with our [Gasless Quickstart Guide](/sdk/nextjs/gasless-quickstart) to set up gasless transactions, or explore our other guides: * **[Gasless Clerk Setup](/sdk/nextjs/gasless-clerk-setup)** - Learn gasless transactions with Clerk * **[Buying Services](/sdk/nextjs/buying-services)** - Integrate service purchases * **[Pay With Crypto](/sdk/nextjs/pay-with-crypto)** - Add payment buttons ## 🔗 Resources * [Dashboard](https://dashboard.chipipay.com/) - Manage your account * [API Reference](/sdk/api/quickstart) - Complete API documentation * [GitHub](https://github.com/chipi-pay) - Source code and examples * [Support](https://t.me/+e2qjHEOwImkyZDVh) - Community help # Set up Source: https://docs.chipipay.com/sdk/nextjs/pay-with-crypto Add a crypto payment button to your website in minutes Choose how you want to be notified when payments are received. ### Option A: Webhooks (Recommended) Configure a webhook to receive real-time updates on payment status. This is ideal for automated systems and immediate order fulfillment. 1. Go to your [Webhook Configuration](https://dashboard.chipipay.com/configure/webhooks) page 2. Add your endpoint URL 3. Select the events you want to receive 4. Save your configuration ### Option B: Email notifications Set up email notifications for manual order processing and simple setups. 1. Visit the [Notification Settings](https://dashboard.chipipay.com/configure/notifications) page 2. Enter your email address 3. Save your settings Your merchant wallet address is where all payments will be sent. 1. Navigate to the [Merchant Configuration](https://dashboard.chipipay.com/configure/merchant) page in your dashboard and copy your Merchant Wallet Address Keep your wallet address secure and only share it through your payment button implementation. Choose your preferred implementation method: Perfect for Next.js applications. ```typescript theme={null} "use client"; import { useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; import { useAuth } from "@clerk/nextjs"; import { useWalletWithAuth } from "@/hooks/useWalletWithAuth"; import { useTransfer } from "@/hooks/useTransfer"; import { useRecordSendTransaction } from "@/hooks/useRecordSendTransaction"; import type { WalletData, Chain, ChainToken } from "@chipi-stack/types"; export function PayCryptoButton({ costumerWallet, amountUsd, onSuccess, label, }: { costumerWallet: WalletData; amountUsd: number; onSuccess?: (txHash: string) => void; label: string; }) { const { getToken } = useAuth(); const { wallet: senderWallet } = useWalletWithAuth(); const { transferAsync } = useTransfer(); const { recordSendTransactionAsync } = useRecordSendTransaction(); const [pin, setPin] = useState(""); const [busy, setBusy] = useState(false); const runPayment = useCallback( async (pin: string) => { const jwtToken = await getToken(); if (!jwtToken) { toast.error("No token found"); return; } const merchant = process.env.NEXT_PUBLIC_MERCHANT_WALLET || ""; if (!merchant) { toast.error("Merchant wallet is not configured"); return; } try { setBusy(true); const transactionHash = await transferAsync({ params: { amount: amountUsd.toString(), encryptKey: pin, wallet: costumerWallet, token: ChainToken.USDC, recipient: merchant, }, bearerToken: jwtToken, }); const recordSendTransactionParams = { transactionHash, chain: Chain.STARKNET, expectedSender: senderWallet?.publicKey || "", expectedRecipient: merchant, expectedToken: ChainToken.USDC, expectedAmount: amountUsd.toString(), }; const sendTx = await recordSendTransactionAsync({ params: recordSendTransactionParams, bearerToken: jwtToken, }); console.log("successful record send transaction:", sendTx); toast.success("Pago completado ✨", { position: "bottom-center" }); onSuccess?.(transactionHash); } catch (err: unknown) { console.error("Error in pay with chipi button:", err); const msg = (typeof err === "string" && err) || (err instanceof Error ? err.message : "Something went wrong. Please try again."); toast.error(msg, { position: "bottom-center" }); } finally { setBusy(false); } }, [ amountUsd, costumerWallet, getToken, recordSendTransactionAsync, senderWallet?.publicKey, transferAsync, onSuccess, ] ); return (
setPin(e.target.value)} />
); } ``` The `usdAmount` parameter is optional. If you don't include it, customers can choose how much they want to pay, making it perfect for donations or flexible pricing.
When a payment is successful, you'll receive a webhook notification with the transaction details. ### Webhook payload schema The event type. Currently only `"transaction.sent"` is supported. Container object for the transaction data. The transaction details. Unique transaction identifier (e.g., `"transaction-1ad902fe-fb48-434f-af9c-c23c170b47b4"`) The blockchain network. Always `"STARKNET"` for ChipiPay. Your ChipiPay API public key (e.g., `"pk_prod_7726eff1cf6ce2e0291fa61e0c08b8c2"`) The Starknet transaction hash for verification on-chain Block number where the transaction was included. `-1` if pending. The wallet address that sent the payment Your merchant wallet address that received the payment Payment amount in the smallest token unit (e.g., `"100000"` = 0.1 USDC) Token type. Currently always `"USDC"`. The contract function called. Usually `"send"`. USD equivalent of the payment amount Transaction status. `"SUCCESS"` indicates a completed payment. Whether the sender used a ChipiPay managed wallet ISO timestamp when the transaction was first detected ISO timestamp when the transaction was last updated ChipiPay wallet ID if sender used a managed wallet ChipiPay wallet ID if destination is a managed wallet Legacy field for ChipiPay wallet identification ### Webhook signature verification For security, all webhooks include an HMAC signature in the `chipi-signature` header. You should verify this signature to ensure the webhook is from ChipiPay. 1. Get your webhook signing secret from your [webhook configuration](https://dashboard.chipipay.com/configure/webhooks) page (it looks like `whsec_****`) 2. Create a webhook handler in your Next.js app: ```typescript theme={null} // app/api/webhooks/route.ts import { createHmac, timingSafeEqual } from 'crypto'; import { NextRequest, NextResponse } from 'next/server'; function verifyWebhookSignature( payload: string, receivedSignature: string, secretKey: string ): boolean { try { const expectedSignature = createHmac("sha256", secretKey) .update(payload) .digest("hex"); // Use timing-safe comparison to prevent timing attacks return timingSafeEqual( Buffer.from(expectedSignature, "hex"), Buffer.from(receivedSignature, "hex") ); } catch (error) { return false; } } export async function POST(request: NextRequest) { const payload = await request.text(); const signature = request.headers.get('chipi-signature'); const webhookSecret = process.env.CHIPI_WEBHOOK_SECRET!; // Your whsec_**** key if (!signature || !verifyWebhookSignature(payload, signature, webhookSecret)) { return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }); } const event = JSON.parse(payload); if (event.event === 'transaction.sent' && event.data.transaction.status === 'SUCCESS') { // Handle successful payment const transaction = event.data.transaction; // Update your database, send confirmation emails, fulfill orders, etc. console.log(`Payment received: ${transaction.amount} USDC from ${transaction.senderAddress}`); } return NextResponse.json({ received: true }); } ``` Your integration is now ready for production!
# Set Up the Chipi MCP Server Source: https://docs.chipipay.com/sdk/nextjs/set-up-mcp-server This guide walks you through setting up and using the Chipi MCP (Model Component Protocol) server. With it, you can seamlessly integrate Chipi’s UI components, utilities, and APIs into your project via standardized registries. ## Prerequisites * Node.js * A project already initialized with Next.js * An Account at [ChipiPay Dashboard](https://dashboard.chipipay.com/) * A created project in [clerk](https://clerk.com/) * npm or pnpm as a package manager * Tailwind CSS configured The MCP server provides a bridge between your development environment and our component registry, enabling seamless integration of Chipi components into your projects. ## Setup the Chipi MCP Serve Click the button below to install the Chipi MCP server in Cursor. [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/en/install-mcp?name=chipiMcp\&config=eyJjb21tYW5kIjoibnB4IG1jcC1yZW1vdGUgaHR0cHM6Ly9tY3AuY2hpcGlwYXkuY29tL21jcCIsImVudiI6e319) ```bash npm theme={null} npx shadcn@latest init ``` ```bash pnpm theme={null} pnpm dlx shadcn@latest init ``` ```json theme={null} "registries": { "@chipi": "https://mcp.chipipay.com/r/{name}.json" } ``` Once the Chipi MCP server is installed, Cursor acts as your assistant for working with Chipi. You can now: * 🔍 **Discover** what’s in the registry — *“What components are available in chipiMcp list all of them”* * 📥 **Install** all components and hooks into your project — *“Install all components, hooks, and utilities from chipiMcp to set up a fully custom crypto-enabled app. ”* * 🛠️ **Get usage examples** — *“Show me how to use Chipi’s Baseprovider with Next.js.”* * 📚 **Read documentation** — *“What props does PayCryptoButton accept?”* * 🧪 **Debug your setup** — *“Check if my Chipi Pay config is correct, check if I am using and installing all correct dependecies from chipiMcp and chipi registry.json”* This means you don’t have to memorize commands or file paths — just ask Cursor, and it will fetch, install, and explain Chipi components directly in your project. ## Troubleshooting * **Registry not loading**: Check your internet connection and registry URL. * **Components not found**: Ensure the MCP server is running. * **Styling issues**: Make sure Tailwind CSS is properly configured. If you’re using an older version of Node.js or run into issues adding the MCP registry in Cursor settings, you can ask Cursor to fetch the registry manually by pasting this curl command into your prompt ```bash theme={null} curl -s "https://mcp.chipipay.com/r/registry.json" | jq '.items[] | {name, title, type}' ``` For more information about MCP Servers in Cursor, see Cursor MCP documentation. # Use Passkeys as a PIN Source: https://docs.chipipay.com/sdk/nextjs/use-passkeys Secure wallet authentication with biometric passkeys instead of PINs ## Overview WebAuthn passkeys replace PINs with biometric authentication (Face ID, Touch ID, Windows Hello). No PINs to remember or store. ## Prerequisites * Next.js 13+ with App Router * Modern browser with WebAuthn support * Biometric hardware (fingerprint, face recognition) * Chipi Next.js SDK installed ## Installation ```bash theme={null} npm install @chipi-stack/chipi-passkey ``` ## Basic Implementation ```typescript Import theme={null} "use client"; import { useCreateWallet, Chain } from "@chipi-stack/nextjs"; import { usePasskeySetup } from "@chipi-stack/chipi-passkey/hooks"; ``` ```typescript Usage theme={null} ``` ```typescript Import theme={null} "use client"; import { useTransfer, ChainToken } from "@chipi-stack/nextjs"; import { usePasskeyAuth } from "@chipi-stack/chipi-passkey/hooks"; ``` ```typescript Usage theme={null} ``` If users already have a PIN-based wallet, they can migrate to passkey: ```typescript Import theme={null} "use client"; import { useMigrateWalletToPasskey } from "@chipi-stack/nextjs"; ``` ```typescript Usage theme={null} ``` ## Example ```typescript theme={null} "use client"; import { useCreateWallet } from '@chipi-stack/nextjs'; import { usePasskeySetup } from '@chipi-stack/chipi-passkey/hooks'; import { useState } from 'react'; export function CreateWalletWithPasskey({ userId }: { userId: string }) { const { createWalletAsync, isLoading, error } = useCreateWallet(); const { setupPasskey } = usePasskeySetup(); const [wallet, setWallet] = useState(null); const handleCreate = async () => { try { // Triggers biometric prompt — requires userId and display name const { encryptKey, credentialId } = await setupPasskey(userId, userId); // Create wallet with passkey-derived key // Do NOT pass usePasskey: true — setupPasskey already handled it const result = await createWalletAsync({ params: { encryptKey, externalUserId: userId, chain: Chain.STARKNET, }, bearerToken: 'your-jwt-token', }); // Result is flat — publicKey is at top level, not nested under .wallet localStorage.setItem('wallet', JSON.stringify(result)); localStorage.setItem('credentialId', credentialId); setWallet(result); } catch (err) { console.error(err); } }; return (
{error &&

Error: {error.message}

} {wallet && (

Wallet created: {wallet.publicKey}

)}
); } ```
```typescript theme={null} "use client"; import { useTransfer } from '@chipi-stack/nextjs'; import { usePasskeyAuth } from '@chipi-stack/chipi-passkey/hooks'; import { useState } from 'react'; export function TransferWithPasskey() { const { transferAsync, isLoading } = useTransfer(); const { authenticate } = usePasskeyAuth(); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); const handleTransfer = async () => { try { // Triggers biometric prompt — returns encryptKey or null if cancelled const encryptKey = await authenticate(); if (!encryptKey) return; const wallet = JSON.parse(localStorage.getItem('wallet')!); // Transfer with passkey-derived key // Do NOT pass usePasskey: true — authenticate() already handled it const txHash = await transferAsync({ params: { encryptKey, wallet, token: ChainToken.USDC, recipient, amount: Number(amount), }, bearerToken: 'your-jwt-token', }); alert(`Transfer complete: ${txHash}`); } catch (err) { console.error(err); } }; return (
setRecipient(e.target.value)} className="w-full p-2 border rounded" /> setAmount(e.target.value)} type="number" className="w-full p-2 border rounded" />
); } ```
```typescript theme={null} "use client"; import { useMigrateWalletToPasskey } from '@chipi-stack/nextjs'; import { useState } from 'react'; export function MigrateToPasskey({ userId }: { userId: string }) { const { migrateWalletToPasskeyAsync, isLoading } = useMigrateWalletToPasskey(); const [currentPin, setCurrentPin] = useState(''); const handleMigrate = async () => { try { const wallet = JSON.parse(localStorage.getItem('wallet')!); // Validates PIN first, then triggers biometric prompt internally const result = await migrateWalletToPasskeyAsync({ wallet, oldEncryptKey: currentPin, externalUserId: userId, bearerToken: 'your-jwt-token', }); // Store updated wallet and credentialId localStorage.setItem('wallet', JSON.stringify(result.wallet)); localStorage.setItem('credentialId', result.credentialId); alert('Successfully migrated to passkey!'); } catch (err) { console.error(err); } }; return (
setCurrentPin(e.target.value)} className="w-full p-2 border rounded" />
); } ```
## Hooks Reference | Hook | Package | Purpose | | --------------------------- | ---------------------------------- | ---------------------------------- | | `usePasskeySetup` | `@chipi-stack/chipi-passkey/hooks` | Create new passkey | | `usePasskeyAuth` | `@chipi-stack/chipi-passkey/hooks` | Authenticate with existing passkey | | `usePasskeyStatus` | `@chipi-stack/chipi-passkey/hooks` | Check passkey support & status | | `useMigrateWalletToPasskey` | `@chipi-stack/nextjs` | Migrate from PIN to passkey | ## Browser Support * ✅ Chrome 67+ (Desktop & Android) * ✅ Safari 14+ (iOS 14+, macOS) * ✅ Firefox 60+ (Desktop) * ✅ Edge 18+ (Desktop) Passkeys are stored in the browser and synced via platform (iCloud Keychain, Google Password Manager). ## Security Benefits * **No PINs stored** - Keys derived on-demand from biometric * **Platform-level security** - Hardware-backed authentication * **Phishing resistant** - Domain-bound credentials * **User convenience** - One tap authentication ## Next Steps * Check out [useCreateWallet](/sdk/nextjs/hooks/use-create-wallet) for wallet creation options * Learn about [useTransfer](/sdk/nextjs/hooks/use-transfer) for token transfers * Explore [session keys](/sdk/nextjs/hooks/use-create-session-key) for additional security # callAnyContract Source: https://docs.chipipay.com/sdk/other-frontend/methods/call-any-contract Executes any StarkNet contract method. Handles all contract interactions not covered by specific methods, giving you full flexibility to interact with any smart contract. ## Usage ```typescript theme={null} const bearerToken = await getBearerToken(); // Your auth implementation const result = await browserClient.callAnyContract({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data" }, calls: [ { contractAddress: "0x...", // Target contract address entrypoint: "methodName", calldata: ["param1", "param2"] } ], }, bearerToken: bearerToken, }); ``` ### Parameters * `encryptKey` (string): User's decryption PIN * `wallet` (WalletData): Wallet credentials with publicKey and encryptedPrivateKey * `calls` (array): Array of contract calls to execute * `contractAddress` (string): Target contract address * `entrypoint` (string): Contract method name * `calldata` (any\[]): Arguments for the contract method * `bearerToken` (string): Bearer token for authentication ### Return Value Returns a Promise that resolves to a transaction hash string. ## Example Implementation ```typescript theme={null} import { ChipiBrowserSDK } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.VITE_CHIPI_PUBLIC_KEY, // or your framework's env var }); async function executeContractCall( wallet: { publicKey: string; encryptedPrivateKey: string }, contractAddress: string, methodName: string, parameters: any[], userPin: string ) { try { const bearerToken = await getBearerToken(); // Your auth implementation const result = await browserClient.callAnyContract({ params: { encryptKey: userPin, wallet: wallet, calls: [ { contractAddress: contractAddress, entrypoint: methodName, calldata: parameters, } ], }, bearerToken: bearerToken, }); console.log('Contract call successful!'); console.log('Transaction hash:', result); return result; } catch (error) { console.error('Contract call failed:', error); throw error; } } // Usage example - Approve tokens for spending async function approveTokenSpending( userId: string, tokenContract: string, spenderAddress: string, amount: string, pin: string ) { const bearerToken = await getBearerToken(); // Get user's wallet const wallet = await browserClient.getWallet({ externalUserId: userId, bearerToken: bearerToken, }); // Call the approve method on the token contract const txHash = await executeContractCall( wallet, tokenContract, "approve", [spenderAddress, amount, "0"], // StarkNet uses felt252 for amounts pin ); console.log(`Approved ${amount} tokens for ${spenderAddress}`); return txHash; } ``` ## Related Methods * [transfer](/sdk/other-frontend/methods/transfer) - For simple token transfers * [getWallet](/sdk/other-frontend/methods/get-wallet) - Get wallet information before contract calls # createWallet Source: https://docs.chipipay.com/sdk/other-frontend/methods/create-wallet Creates a new Argent-compatible wallet on StarkNet. This method deploys the wallet contract behind the scenes and uses Avnus gasless to sponsor gas fees, resulting in a frictionless onboarding experience. ## Usage ```typescript theme={null} import { ChipiBrowserSDK, Chain } from "@chipi-stack/backend"; const bearerToken = await getBearerToken(); // Your auth implementation const newWallet = await browserClient.createWallet({ params: { encryptKey: "user-secure-pin", externalUserId: "your-user-id-123", chain: Chain.STARKNET, }, bearerToken: bearerToken, }); ``` ### Parameters * `encryptKey` (string): A user-defined code or password used to encrypt the wallet's private key. * `externalUserId` (string): Your application's unique identifier for the user * `chain` (`Chain`): The blockchain network for the wallet. Use `Chain.STARKNET`. * `bearerToken` (string): Bearer token for authentication ### Return Value Returns a Promise that resolves to `CreateWalletResponse` — a flat object with: * `publicKey`: The wallet's public key / address * `encryptedPrivateKey`: The encrypted private key (store securely) * `walletType`: The wallet type (`"CHIPI"` or `"READY"`) * `chain`: The blockchain network * `isDeployed`: Whether the wallet contract is deployed * `normalizedPublicKey`: Normalized form of the public key * `apiPublicKey`: The organization's API public key * `id`, `externalUserId`, `organizationId`, `createdAt`, `updatedAt` ## Example Implementation ```typescript theme={null} import { ChipiBrowserSDK, Chain } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.VITE_CHIPI_PUBLIC_KEY, // or your framework's env var }); async function createUserWallet(userId: string, userPin: string) { try { const bearerToken = await getBearerToken(); // Your auth implementation const newWallet = await browserClient.createWallet({ params: { encryptKey: userPin, externalUserId: userId, chain: Chain.STARKNET, }, bearerToken: bearerToken, }); console.log('Wallet created successfully!'); console.log('Address:', newWallet.publicKey); // Store wallet data in your app state saveWalletToState({ userId, publicKey: newWallet.publicKey, encryptedPrivateKey: newWallet.encryptedPrivateKey, }); return newWallet; } catch (error) { console.error('Wallet creation failed:', error); throw error; } } // Usage example async function onboardNewUser(userId: string, pin: string) { const wallet = await createUserWallet(userId, pin); // Verify deployment on StarkScan const contractUrl = `https://starkscan.co/contract/${wallet.publicKey}`; console.log('View contract:', contractUrl); return wallet; } ``` ## Related Methods * [getWallet](/sdk/other-frontend/methods/get-wallet) - Retrieve existing wallet information * [transfer](/sdk/other-frontend/methods/transfer) - Send tokens from the created wallet # getTransaction Source: https://docs.chipipay.com/sdk/other-frontend/methods/get-transaction Fetches a single transaction by its hash or internal ID. Works with Vue, Angular, Svelte, and vanilla JavaScript. ## Usage ```typescript theme={null} const bearerToken = await getBearerToken(); const tx = await browserClient.getTransaction("0x...", bearerToken); ``` ### Parameters * `hashOrId` (string): Transaction hash (`0x...`) or internal database ID * `bearerToken` (string): Bearer token for authentication ### Return Value Returns a `Promise` with the full transaction record. ## Example Implementation ```typescript theme={null} import { ChipiBrowserSDK } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY, }); async function showTransaction(txHash: string) { const bearerToken = await getBearerToken(); const tx = await browserClient.getTransaction(txHash, bearerToken); console.log(`Status: ${tx.status}`); console.log(`Amount: ${tx.amount} ${tx.token}`); console.log(`From: ${tx.senderAddress}`); console.log(`To: ${tx.destinationAddress}`); } ``` # getTransactionStatus Source: https://docs.chipipay.com/sdk/other-frontend/methods/get-transaction-status Fetches the on-chain status of a transaction from StarkNet. Works with Vue, Angular, Svelte, and vanilla JavaScript. ## Usage ```typescript theme={null} const bearerToken = await getBearerToken(); const status = await browserClient.getTransactionStatus("0x...", bearerToken); ``` ### Parameters * `hash` (string): Transaction hash (`0x...`) * `bearerToken` (string): Bearer token for authentication ### Return Value Returns a `Promise` with: * `transactionHash` (string): Transaction hash * `status` (OnChainTxStatus): `RECEIVED`, `PENDING`, `ACCEPTED_ON_L2`, `ACCEPTED_ON_L1`, `REJECTED`, `REVERTED`, or `NOT_RECEIVED` * `blockNumber` (number, optional): Block number if included in a block * `revertReason` (string, optional): Reason if `REVERTED` ## Example Implementation ```typescript theme={null} import { ChipiBrowserSDK } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY, }); async function pollStatus(txHash: string) { const terminal = ["ACCEPTED_ON_L1", "REJECTED", "REVERTED", "NOT_RECEIVED"]; const bearerToken = await getBearerToken(); const MAX_RETRIES = 60; // 60 retries * 3s = 3 minutes max for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { const { status, revertReason } = await browserClient.getTransactionStatus( txHash, bearerToken, ); console.log(`Status: ${status} (attempt ${attempt + 1}/${MAX_RETRIES})`); if (terminal.includes(status)) { if (status === "REVERTED") console.error("Reverted:", revertReason); return status; } await new Promise((r) => setTimeout(r, 3000)); } throw new Error(`Transaction ${txHash} did not reach terminal status after ${MAX_RETRIES} attempts`); } ``` # getWallet Source: https://docs.chipipay.com/sdk/other-frontend/methods/get-wallet Retrieves wallet information for an existing user by their external user ID. This method allows you to fetch wallet details for users who have already created wallets. ## Usage ```typescript theme={null} const bearerToken = await getBearerToken(); // Your auth implementation const wallet = await browserClient.getWallet({ externalUserId: "your-user-id-123", bearerToken: bearerToken, }); ``` ### Parameters * `externalUserId` (string): Your application's unique identifier for the user * `bearerToken` (string): Bearer token for authentication ### Return Value Returns a Promise that resolves to an object containing: * `publicKey` (string): The wallet's public address on StarkNet * `encryptedPrivateKey` (string): The encrypted private key for the wallet ## Example Implementation ```typescript theme={null} import { ChipiBrowserSDK } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.VITE_CHIPI_PUBLIC_KEY, // or your framework's env var }); async function getUserWallet(userId: string) { try { const bearerToken = await getBearerToken(); // Your auth implementation const wallet = await browserClient.getWallet({ externalUserId: userId, bearerToken: bearerToken, }); console.log('Wallet found:'); console.log('Address:', wallet.publicKey); return wallet; } catch (error) { if (error.message.includes('not found')) { console.log('No wallet found for user:', userId); return null; } console.error('Error retrieving wallet:', error); throw error; } } // Usage example async function checkUserWallet(userId: string) { const wallet = await getUserWallet(userId); if (wallet) { console.log(`User ${userId} has wallet: ${wallet.publicKey}`); return wallet; } else { console.log(`User ${userId} needs to create a wallet`); return null; } } ``` ## Related Methods * [createWallet](/sdk/other-frontend/methods/create-wallet) - Create a new wallet for users who don't have one * [transfer](/sdk/other-frontend/methods/transfer) - Send tokens from the retrieved wallet # Session Keys Source: https://docs.chipipay.com/sdk/other-frontend/methods/sessions Enable temporary, delegated transaction execution using the ChipiBrowserSDK. Session keys allow gasless, frictionless UX without requiring the owner private key for each transaction. Session keys only work with **CHIPI wallets** - not READY wallets. Make sure your wallet was created with `walletType: "CHIPI"`. ## Overview Session keys allow your application to execute transactions on behalf of a user without requiring their owner private key for every action. This enables **gasless, frictionless UX** for gaming, DeFi automation, social apps, and more. | Method | Description | | ------------------------------------ | ---------------------------------- | | `sessions.createSessionKey()` | Generate a session keypair locally | | `sessions.addSessionKeyToContract()` | Register session on-chain | | `sessions.revokeSessionKey()` | Revoke a session key | | `sessions.getSessionData()` | Query session status | | `executeTransactionWithSession()` | Execute tx using session | *** ## createSessionKey Generate a session keypair locally. This is a **synchronous** operation that doesn't make network calls. ```typescript theme={null} const session = browserClient.sessions.createSessionKey({ encryptKey: "user-secure-pin", durationSeconds: 3600, // 1 hour (default: 6 hours) }); console.log(session); // { // publicKey: "0x...", // encryptedPrivateKey: "...", // validUntil: 1702500000 // } // Store in client-side storage (NOT your database!) localStorage.setItem('chipiSession', JSON.stringify(session)); ``` ### Parameters | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------ | | `encryptKey` | string | Yes | PIN to encrypt the session private key | | `durationSeconds` | number | No | Session duration in seconds (default: 21600 = 6 hours) | ### Return Value ```typescript theme={null} { publicKey: string; // Session public key (hex) encryptedPrivateKey: string; // AES encrypted with encryptKey validUntil: number; // Unix timestamp (seconds) } ``` **Never store session keys in your backend database.** Store only in client-side secure storage (localStorage, sessionStorage, or platform-specific secure storage). *** ## addSessionKeyToContract Register a session key on the CHIPI wallet smart contract. This requires the owner's signature (one-time setup per session). ```typescript theme={null} const bearerToken = await getBearerToken(); const txHash = await browserClient.sessions.addSessionKeyToContract({ encryptKey: "user-secure-pin", wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, // Maximum transactions allowed allowedEntrypoints: [], // Empty = all functions allowed }, }, bearerToken); console.log("Session registered:", txHash); ``` ### Parameters | Parameter | Type | Required | Description | | ---------------------------------- | ---------- | -------- | ------------------------------------------------------------------- | | `encryptKey` | string | Yes | Owner's PIN to sign the registration | | `wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `sessionConfig.sessionPublicKey` | string | Yes | From `createSessionKey().publicKey` | | `sessionConfig.validUntil` | number | Yes | Unix timestamp when session expires | | `sessionConfig.maxCalls` | number | Yes | Maximum transactions allowed | | `sessionConfig.allowedEntrypoints` | string\[] | Yes | Whitelisted function selectors (empty = all) | ### Restricting Permissions For enhanced security, whitelist specific function selectors: ```typescript theme={null} const txHash = await browserClient.sessions.addSessionKeyToContract({ encryptKey: pin, wallet: walletData, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 10, allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve ], }, }, bearerToken); ``` *** ## revokeSessionKey Revoke a session key from the smart contract. After revocation, the session can no longer execute transactions. ```typescript theme={null} const bearerToken = await getBearerToken(); const txHash = await browserClient.sessions.revokeSessionKey({ encryptKey: "user-secure-pin", wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionPublicKey: session.publicKey, }, bearerToken); // Clear from local storage localStorage.removeItem('chipiSession'); console.log("Session revoked:", txHash); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------ | ---------- | -------- | ---------------------------------- | | `encryptKey` | string | Yes | Owner's PIN to sign the revocation | | `wallet` | WalletData | Yes | Wallet object | | `sessionPublicKey` | string | Yes | Public key of session to revoke | Always revoke sessions when a user logs out to prevent unauthorized access. *** ## getSessionData Query session status from the smart contract. This is a **read-only** operation that doesn't require gas. ```typescript theme={null} const sessionData = await browserClient.sessions.getSessionData({ walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, }); console.log(sessionData); // { // isActive: true, // validUntil: 1702500000, // remainingCalls: 95, // allowedEntrypoints: [] // } if (sessionData.isActive) { console.log(`Session has ${sessionData.remainingCalls} calls remaining`); } ``` ### Parameters | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------- | | `walletAddress` | string | Yes | Wallet address to query | | `sessionPublicKey` | string | Yes | Session public key to query | ### Return Value ```typescript theme={null} { isActive: boolean; // Whether session is valid validUntil: number; // Expiration timestamp remainingCalls: number; // Transactions remaining allowedEntrypoints: string[]; // Whitelisted functions } ``` *** ## executeTransactionWithSession Execute a transaction using a session key instead of the owner private key. No wallet popup required! ```typescript theme={null} const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession')!); const txHash = await browserClient.executeTransactionWithSession({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, session, calls: [ { contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }, ], }, bearerToken, }); console.log("Transaction executed:", txHash); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------- | -------------- | -------- | ------------------------------------------ | | `params.encryptKey` | string | Yes | PIN to decrypt the **session** private key | | `params.wallet` | WalletData | Yes | Wallet the session acts on behalf of | | `params.session` | SessionKeyData | Yes | Session from `createSessionKey()` | | `params.calls` | Call\[] | Yes | Array of contract calls | | `bearerToken` | string | Yes | Authentication token | ### Call Structure ```typescript theme={null} { contractAddress: string; // Target contract address entrypoint: string; // Function name calldata: string[]; // Function arguments } ``` ### Multi-Call Example Execute multiple operations in a single transaction: ```typescript theme={null} const txHash = await browserClient.executeTransactionWithSession({ params: { encryptKey: pin, wallet: walletData, session, calls: [ // Approve spending { contractAddress: USDC_ADDRESS, entrypoint: "approve", calldata: [SWAP_CONTRACT, amount, "0x0"], }, // Execute swap { contractAddress: SWAP_CONTRACT, entrypoint: "swap", calldata: [USDC_ADDRESS, ETH_ADDRESS, amount, minReceived], }, ], }, bearerToken, }); ``` *** ## Complete Example: Gaming Session ```typescript theme={null} import { ChipiBrowserSDK } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: "pk_prod_your_public_key", }); const GAME_CONTRACT = "0x..."; const MAKE_MOVE_SELECTOR = "0x..."; // Start a gaming session async function startGameSession(userPin: string, bearerToken: string) { // Get player's wallet const wallet = await browserClient.getWallet({ externalUserId: "user-123" }, bearerToken); // Create 24-hour gaming session const session = browserClient.sessions.createSessionKey({ encryptKey: userPin, durationSeconds: 24 * 60 * 60, }); // Register with game-only permissions await browserClient.sessions.addSessionKeyToContract({ encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 1000, allowedEntrypoints: [MAKE_MOVE_SELECTOR], }, }, bearerToken); // Store session localStorage.setItem('gameSession', JSON.stringify(session)); return session; } // Make a move instantly - no wallet popup! async function makeMove(moveData: string, userPin: string, bearerToken: string) { const session = JSON.parse(localStorage.getItem('gameSession')!); const wallet = await browserClient.getWallet({ externalUserId: "user-123" }, bearerToken); return browserClient.executeTransactionWithSession({ params: { encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, session, calls: [{ contractAddress: GAME_CONTRACT, entrypoint: "make_move", calldata: [moveData], }], }, bearerToken, }); } // Secure logout async function logout(userPin: string, bearerToken: string) { const sessionStr = localStorage.getItem('gameSession'); if (sessionStr) { const session = JSON.parse(sessionStr); const wallet = await browserClient.getWallet({ externalUserId: "user-123" }, bearerToken); try { await browserClient.sessions.revokeSessionKey({ encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, sessionPublicKey: session.publicKey, }, bearerToken); } catch (e) { console.warn("Failed to revoke session:", e); } localStorage.removeItem('gameSession'); } } ``` *** ## Error Handling | Error | Cause | Solution | | ---------------------------------------- | ------------------------ | -------------------------------------------------- | | `Session keys require CHIPI wallet type` | Wallet is READY type | Create wallet with `walletType: "CHIPI"` | | `Invalid encryptKey` | Wrong PIN | Verify PIN matches wallet creation | | `Session has expired` | `validUntil` passed | Create and register a new session | | `Max calls exceeded` | `remainingCalls` is 0 | Create new session with higher `maxCalls` | | `Entrypoint not allowed` | Function not whitelisted | Register session with correct `allowedEntrypoints` | ## Security Best Practices 1. **Short durations** - Use 1-6 hours, not days 2. **Limit calls** - Set realistic `maxCalls` based on expected usage 3. **Whitelist functions** - Restrict `allowedEntrypoints` to only what your app needs 4. **Revoke on logout** - Always clean up sessions when user signs out 5. **Client-side storage only** - Never store session keys in your database # transfer Source: https://docs.chipipay.com/sdk/other-frontend/methods/transfer Transfers tokens from a user wallet to another address. Uses Avnus gasless to cover gas fees, making transactions frictionless for your users. ## Usage ```typescript theme={null} const bearerToken = await getBearerToken(); // Your auth implementation const transferResult = await browserClient.transfer({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data" }, amount: "100", token: ChainToken.USDC, recipient: "0x1234567890abcdef...", }, bearerToken: bearerToken, }); ``` ### Parameters * `encryptKey` (string): PIN used to decrypt the private key * `wallet` (WalletData): Object with publicKey and encryptedPrivateKey * `amount` (string | number): Transfer amount * `token` (`ChainToken`): Token identifier. Use `ChainToken.USDC` * `recipient` (string): Destination wallet address * `decimals` (number, optional): Token decimals (default: 18) * `bearerToken` (string): Bearer token for authentication ### Return Value Returns a Promise that resolves to a transaction hash string. ## Example Implementation ```typescript theme={null} import { ChipiBrowserSDK, ChainToken } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.VITE_CHIPI_PUBLIC_KEY, // or your framework's env var }); async function transferTokens( senderWallet: { publicKey: string; encryptedPrivateKey: string }, recipientAddress: string, amount: string, userPin: string ) { try { const bearerToken = await getBearerToken(); // Your auth implementation const transferResult = await browserClient.transfer({ params: { encryptKey: userPin, wallet: senderWallet, amount: amount, token: ChainToken.USDC, recipient: recipientAddress, }, bearerToken: bearerToken, }); console.log('Transfer completed successfully!'); console.log('Transaction hash:', transferResult); return transferResult; } catch (error) { console.error('Transfer failed:', error); throw error; } } // Usage example async function sendUSDC(userId: string, recipientAddress: string, amount: string, pin: string) { // First, get the user's wallet const bearerToken = await getBearerToken(); const wallet = await browserClient.getWallet({ externalUserId: userId, bearerToken: bearerToken, }); // Then perform the transfer const txHash = await transferTokens( wallet, recipientAddress, amount, pin ); console.log(`Sent ${amount} USDC to ${recipientAddress}`); console.log(`Transaction: https://starkscan.co/tx/${txHash}`); return txHash; } ``` ## Related Methods * [getWallet](/sdk/other-frontend/methods/get-wallet) - Get wallet information before transfers * [callAnyContract](/sdk/other-frontend/methods/call-any-contract) - For custom contract interactions # Not using React? Quickstart Source: https://docs.chipipay.com/sdk/other-frontend/quickstart Quick setup guide for the Chipi Browser SDK - client-side wallet management for Vue, Angular, Svelte, and vanilla JavaScript The Browser SDK is designed for client-side use only. Never expose your secret API key in frontend code - only use your public key. ```bash npm theme={null} npm install @chipi-stack/backend ``` ```bash yarn theme={null} yarn add @chipi-stack/backend ``` ```bash pnpm theme={null} pnpm add @chipi-stack/backend ``` 1. Go to your [API Keys](https://dashboard.chipipay.com/configure/api-keys) in the Chipi Dashboard 2. Copy your **Public Key** (`pk_prod_xxxx`) You only need the Public Key for frontend applications. Never use your Secret Key in client-side code. Create a new instance of the ChipiBrowserSDK with your public API key: ```typescript theme={null} import { ChipiBrowserSDK, Chain, ChainToken } from "@chipi-stack/backend"; const browserClient = new ChipiBrowserSDK({ apiPublicKey: "pk_prod_your_public_key", }); ``` The Browser SDK requires a bearer token for authentication. You'll need to obtain this from your authentication provider: ```typescript theme={null} // Example with your auth system async function getBearerToken() { // Replace with your actual auth implementation const token = await yourAuthProvider.getToken(); return token; } ``` Now you can create a wallet for your users: ```typescript theme={null} const bearerToken = await getBearerToken(); const newWallet = await browserClient.createWallet({ params: { encryptKey: "user-secure-pin", externalUserId: "your-user-id-123", chain: Chain.STARKNET, }, bearerToken: bearerToken, }); console.log('New wallet created:', newWallet); // Output: { publicKey: "0x...", encryptedPrivateKey: "...", walletType: "CHIPI", ... } ``` Transfer tokens between wallets: ```typescript theme={null} const bearerToken = await getBearerToken(); const transferResult = await browserClient.transfer({ params: { encryptKey: "user-secure-pin", wallet: { publicKey: newWallet.publicKey, encryptedPrivateKey: newWallet.encryptedPrivateKey, }, amount: "100", token: ChainToken.USDC, recipient: "0x1234567890abcdef...", }, bearerToken: bearerToken, }); console.log('Transfer completed:', transferResult); ``` For production applications, store your API key as an environment variable: ```bash theme={null} # .env VITE_CHIPI_PUBLIC_KEY=pk_prod_your_public_key # For Vite NEXT_PUBLIC_CHIPI_API_KEY=pk_prod_your_public_key # For Next.js REACT_APP_CHIPI_API_KEY=pk_prod_your_public_key # For Create React App ``` Then initialize the SDK: ```typescript theme={null} const browserClient = new ChipiBrowserSDK({ apiPublicKey: process.env.VITE_CHIPI_PUBLIC_KEY, // or your framework's env var }); ``` ## Next Steps Now that you have the basic setup working, explore more advanced features: * **[Session Keys](/sdk/other-frontend/methods/sessions)** - Enable delegated transaction execution * **[API Reference](/sdk/api/introduction)** - Full API documentation ## Security Best Practices * Secure your bearer tokens properly * Validate user inputs before making API calls Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Signature Validation Source: https://docs.chipipay.com/sdk/other-frontend/signature-validation Learn how to verify webhook signatures to ensure secure communication with Chipi Pay # Signature Validation For security, all webhooks from Chipi Pay include an HMAC signature in the `chipi-signature` header. You should verify this signature to ensure the webhook is from Chipi Pay and hasn't been tampered with. ## Why Verify Signatures? Always verify webhook signatures in production. This prevents unauthorized requests and ensures data integrity. Webhook signature verification: * **Prevents spoofing** - Ensures requests are from Chipi Pay * **Protects data integrity** - Detects if payloads have been modified * **Security best practice** - Industry standard for webhook security ## Getting Your Webhook Secret 1. Go to your [webhook configuration](https://dashboard.chipipay.com/configure/webhooks) page in the Chipi Dashboard 2. Copy your **Webhook Signing Secret** (it looks like `whsec_****`) 3. Store it securely as an environment variable ## Implementation ### Step 1: Install Required Dependencies ```bash theme={null} npm install crypto ``` ### Step 2: Create a Signature Verification Function ```typescript theme={null} import { createHmac, timingSafeEqual } from 'crypto'; function verifyWebhookSignature( payload: string, receivedSignature: string, secretKey: string ): boolean { try { const expectedSignature = createHmac("sha256", secretKey) .update(payload) .digest("hex"); // Use timing-safe comparison to prevent timing attacks return timingSafeEqual( Buffer.from(expectedSignature, "hex"), Buffer.from(receivedSignature, "hex") ); } catch (error) { return false; } } ``` ### Step 3: Verify in Your Webhook Handler ```typescript theme={null} // Example webhook handler (adjust based on your framework) export async function POST(request: Request) { const payload = await request.text(); const signature = request.headers.get('chipi-signature'); const webhookSecret = process.env.CHIPI_WEBHOOK_SECRET!; // Your whsec_**** key if (!signature || !verifyWebhookSignature(payload, signature, webhookSecret)) { return new Response( JSON.stringify({ error: 'Invalid signature' }), { status: 401 } ); } const event = JSON.parse(payload); // Handle the webhook event if (event.event === 'transaction.sent' && event.data.transaction.status === 'SUCCESS') { // Handle successful payment const transaction = event.data.transaction; console.log(`Payment received: ${transaction.amount} USDC from ${transaction.senderAddress}`); // Update your database, send confirmation emails, fulfill orders, etc. } return new Response(JSON.stringify({ received: true }), { status: 200 }); } ``` ## Security Best Practices * Always use timing-safe comparison functions to prevent timing attacks * Store your webhook secret in environment variables, never in code * Verify signatures before processing any webhook data * Log failed signature verifications for security monitoring ## Common Issues ### Signature Mismatch If you're getting signature mismatches: 1. Ensure you're using the correct webhook secret from your dashboard 2. Verify you're reading the raw request body (not parsed JSON) 3. Check that the `chipi-signature` header is being read correctly 4. Make sure you're using SHA-256 HMAC ### Environment Variables Store your webhook secret securely: ```bash theme={null} # .env CHIPI_WEBHOOK_SECRET=whsec_your_secret_key_here ``` ## Next Steps * Learn more about [webhook events](/sdk/api/introduction) * Set up your [webhook configuration](https://dashboard.chipipay.com/configure/webhooks) * Explore [React SDK hooks](/sdk/react/hooks/use-create-wallet) # API Reference Source: https://docs.chipipay.com/sdk/passkeys/api Every function and React hook exported from @chipi-stack/chipi-passkey. Grouped by which wallet type they apply to. The package ships two parallel surfaces. Pick the one that matches your wallet type. Mixing them is fine — both can live on the same device under different localStorage keys. | Wallet type | Surface | Use when | | ---------------------------- | ----------------------------------------------------------- | --------------------------------------------------- | | SHHH (default since v14.5.0) | `createShhhPasskey`, `signShhhMessage`, `…ShhhCredential` | New integrations, any SHHH wallet | | Legacy CHIPI v29 | `createWalletPasskey`, `getWalletEncryptKey`, `…Credential` | Existing CHIPI v29 wallets you haven't migrated yet | The React hooks (`usePasskeySetup`, `usePasskeyAuth`, `usePasskeyStatus`) wrap the **legacy** surface today. SHHH equivalents ship in a follow-on release; until then, call `createShhhPasskey` / `signShhhMessage` directly inside your own effects. ## SHHH passkey (modern path) ### createShhhPasskey Registers a new P-256 platform passkey. No PRF extension, no PIN derivation. The private key stays inside the platform authenticator. ```ts theme={null} import { createShhhPasskey } from "@chipi-stack/chipi-passkey"; const result = await createShhhPasskey( userId: string, userName: string, options?: { rpName?: string; rpId?: string; displayName?: string }, ); // result: { // credentialId: string, // publicKey: { x: Uint8Array(32), y: Uint8Array(32) }, // publicKeyHex: { x: string, y: string }, // rpId: string, // } ``` Defaults: `rpId = window.location.hostname`, `rpName = "Chipi Wallet"`. Persists `{ credentialId, publicKey, userId, rpId, transports }` to localStorage under the key `chipi_wallet_shhh_passkey_credential`. Throws on `NotAllowedError` (user cancelled) or `InvalidStateError` (passkey already exists for this account on this device). ### signShhhMessage Runs `navigator.credentials.get()` with `messageHash` encoded as the 32-byte big-endian WebAuthn challenge. Returns the raw assertion bytes ready to feed into `buildWebAuthnEnvelopeFromAssertion` from `@chipi-stack/backend`. ```ts theme={null} import { signShhhMessage } from "@chipi-stack/chipi-passkey"; const result = await signShhhMessage({ messageHash: bigint, // SHHH SNIP-12 hash credentialId?: string, // defaults to the stored credential rpId?: string, // defaults to the registered rpId }); // result: { // authenticatorData: Uint8Array, // clientDataJSON: Uint8Array, // signatureDer: Uint8Array, // credentialId: string, // } ``` ### hasShhhPasskey Synchronous check for a stored SHHH credential. Use it to branch between "Sign up" and "Sign in" UI without prompting. ```ts theme={null} import { hasShhhPasskey } from "@chipi-stack/chipi-passkey"; const ready = hasShhhPasskey(); // boolean ``` ### getStoredShhhCredential Reads the persisted credential metadata. ```ts theme={null} import { getStoredShhhCredential } from "@chipi-stack/chipi-passkey"; const stored = getStoredShhhCredential(); // stored | null: // { // credentialId: string, // publicKey: { x: string, y: string }, // hex // createdAt: string, // ISO timestamp // userId: string, // rpId: string, // transports?: AuthenticatorTransport[], // } ``` ### removeStoredShhhCredential Clears the stored credential id from localStorage. Use at logout, account reset, or when the user opts to re-register. The platform authenticator's stored credential is NOT deleted — only your app's reference to it. ```ts theme={null} import { removeStoredShhhCredential } from "@chipi-stack/chipi-passkey"; removeStoredShhhCredential(); ``` ### Extraction utilities Exported for tests and for advanced callers who want to verify a passkey's pubkey against a separately-stored copy. ```ts theme={null} import { bigintToBe32, extractP256PubkeyFromRegistration, extractP256PubkeyFromSpki, extractP256PubkeyFromAttestationObject, } from "@chipi-stack/chipi-passkey"; ``` `extractP256PubkeyFromRegistration` is the dispatch helper — it tries the SPKI fast path first (Chrome, Safari, modern Edge), then falls back to decoding the CBOR `attestationObject` for older browsers. You only need these utilities if you're writing your own registration-response handler. ## Legacy PRF passkey \[#legacy-prf-passkey] The PRF passkey path encrypts a STARK private key with a key derived from the WebAuthn PRF extension. It backs **CHIPI v29 wallets** — both legacy on-chain accounts and the dual-key (passkey primary + PIN backup) flow. New SHHH integrations don't need this surface; existing CHIPI v29 integrations still do. ### createWalletPasskey Registers a passkey with the PRF extension enabled, derives an encryption key from the PRF output, and stores the credential metadata. ```ts theme={null} import { createWalletPasskey } from "@chipi-stack/chipi-passkey"; const result = await createWalletPasskey(userId: string, userName: string); // result: { // encryptKey: string, // hex — use to encrypt the wallet's STARK key // credentialId: string, // prfSupported: boolean, // false on Firefox without PRF // } ``` Persists `{ credentialId, createdAt, userId, transports, prfSupported }` to localStorage under `chipi_wallet_passkey_credential`. **Separate** storage key from the SHHH path — both surfaces coexist on the same device. ### getWalletEncryptKey Authenticates with an existing PRF passkey and returns the same `encryptKey` derived at registration time. Use it before every wallet operation that needs the STARK key (transfer, session-key registration, encryption rotation). ```ts theme={null} import { getWalletEncryptKey } from "@chipi-stack/chipi-passkey"; const encryptKey = await getWalletEncryptKey( credentialId?: string, options?: { prfSupported?: boolean }, ); // encryptKey: string | null // null if user cancels the biometric prompt ``` The PRF output is deterministic — same passkey, same PRF salt, same encryption key. If PRF is unavailable (Firefox before 122, or a browser switch after registering on Chrome), the function falls back to PBKDF2 derivation **only if** the credential was originally registered without PRF. Otherwise it throws — see [When passkeys fail](/sdk/passkeys/when-passkeys-fail). ### Status checks ```ts theme={null} import { isWebAuthnSupported, isWebAuthnPRFSupported, hasWalletPasskey, verifyWalletPasskey, verifyWalletPasskeyDetailed, } from "@chipi-stack/chipi-passkey"; isWebAuthnSupported(); // boolean isWebAuthnPRFSupported(); // alias for isWebAuthnSupported (kept for backcompat) hasWalletPasskey(); // boolean — is a credential stored locally? await verifyWalletPasskey(); // boolean await verifyWalletPasskeyDetailed(); // { valid, reason?, message? } ``` ### Credential management ```ts theme={null} import { getStoredCredential, removeStoredCredential, } from "@chipi-stack/chipi-passkey"; const stored = getStoredCredential(); // stored | null: // { // credentialId: string, // createdAt: string, // userId: string, // transports?: AuthenticatorTransport[], // prfSupported?: boolean, // } removeStoredCredential(); ``` ## React hooks (legacy passkey path) The hooks today wrap the **legacy PRF** surface — `usePasskeySetup` calls `createWalletPasskey`, `usePasskeyAuth` calls `getWalletEncryptKey`. Import from the `/hooks` subpath so Next.js bundles get the right `"use client"` directives. ```ts theme={null} import { usePasskeySetup, usePasskeyAuth, usePasskeyStatus, } from "@chipi-stack/chipi-passkey/hooks"; ``` ### usePasskeySetup ```tsx theme={null} function CreateWalletButton({ userId, userName }: { userId: string; userName: string }) { const { setupPasskey, isLoading, error, data } = usePasskeySetup(); async function onClick() { const result = await setupPasskey(userId, userName); // result: { encryptKey, credentialId, prfSupported } } return ( ); } ``` ### usePasskeyAuth ```tsx theme={null} function SignButton() { const { authenticate, isLoading, error, data } = usePasskeyAuth(); async function onClick() { const encryptKey = await authenticate(); // null if user cancelled if (encryptKey) { // use encryptKey for the next wallet operation } } } ``` ### usePasskeyStatus ```tsx theme={null} function PasskeyStatusBanner() { const { status, hasPasskey, refresh } = usePasskeyStatus(); // status: "loading" | "ready" | "not_supported" | "no_passkey" | "error" if (status === "no_passkey") return ; if (status === "not_supported") return ; return null; } ``` ## TypeScript types ```ts theme={null} import type { ShhhPasskeyCredential, CreateShhhPasskeyResult, ShhhAssertion, CreateWalletPasskeyResult, WalletCredentialMetadata, PasskeyVerifyResult, UsePasskeySetupResult, UsePasskeyAuthResult, UsePasskeyStatusResult, PasskeyStatus, } from "@chipi-stack/chipi-passkey"; ``` ## Related * [Quickstart](/sdk/passkeys/quickstart) — end-to-end SHHH passkey flow * [When passkeys fail](/sdk/passkeys/when-passkeys-fail) — recovery, PIN fallback, browser switching # Passkeys Source: https://docs.chipipay.com/sdk/passkeys/overview Sign Starknet transactions with Touch ID, Face ID, or Windows Hello. The user's private key never leaves their device, and they never type a password. The `@chipi-stack/chipi-passkey` package handles the browser side of passkey authentication for Chipi wallets. The user authenticates with biometrics — Touch ID, Face ID, Windows Hello, Android biometrics — and the wallet's private key stays on their device. ## What you build with this * **Sign-up without a password.** The user taps Touch ID once; you get back a public key + credential id and create their wallet. * **Sign every transaction with biometrics.** No PIN prompt, no password modal, no copy-pasted seed phrase. * **Recover lost passkeys.** A second passkey on a different device, or a PIN fallback at the SDK boundary, both keep the wallet recoverable. ## Install ```bash theme={null} npm install @chipi-stack/chipi-passkey # or pnpm / yarn ``` Browser-only. The package depends on `navigator.credentials` and `crypto.subtle`; it does not run on Node or Bun server entry points. ## Pick your path There are two wallet types — the SDK supports passkeys for both, but the entry point differs. Use `createShhhPasskey` + `signShhhMessage`. The P-256 key stays inside the platform authenticator. No PIN. This is the path new integrations should take. Use `createWalletPasskey` + `getWalletEncryptKey`. The WebAuthn PRF extension derives an encryption key that unlocks a stored STARK key. Still supported. ## Browser support The platform authenticator must be available — that's iOS 15+ Safari, modern Chrome / Edge, Firefox 122+ on macOS / Windows, Android 9+ Chrome. The package exposes `isWebAuthnSupported()` so you can branch your UI: ```ts theme={null} import { isWebAuthnSupported } from "@chipi-stack/chipi-passkey"; if (!isWebAuthnSupported()) { // Render the PIN fallback flow instead of a "Sign up with passkey" button. } ``` ## Related * [Quickstart](/sdk/passkeys/quickstart) — register a passkey and sign a transaction, end to end * [API reference](/sdk/passkeys/api) — every exported function * [When passkeys fail](/sdk/passkeys/when-passkeys-fail) — what to do when the browser changes, the user loses their device, or biometrics break # Quickstart Source: https://docs.chipipay.com/sdk/passkeys/quickstart Register a passkey, create a Chipi wallet protected by it, and sign a transaction. End-to-end React example you can paste into a Next.js page. This page walks the modern path — SHHH wallets with a P-256 passkey. The user's private key lives inside their platform authenticator (Secure Enclave on Apple, TPM on Windows, Android Keystore on Android). It cannot leave the device. The page also points at the legacy PRF path if you're integrating against existing CHIPI v29 wallets. ## 1. Register a passkey `createShhhPasskey` triggers the browser's biometric prompt and returns the new credential's id + public key. Call it from a button click, never on page load. ```tsx theme={null} import { createShhhPasskey } from "@chipi-stack/chipi-passkey"; async function onSignUp(userId: string, userName: string) { const { credentialId, publicKeyHex } = await createShhhPasskey( userId, userName, ); // credentialId → save with your wallet record // publicKeyHex → { x: "…", y: "…" }, both 32-byte hex strings return { credentialId, publicKeyHex }; } ``` The returned `publicKeyHex.x` and `publicKeyHex.y` are the two 32-byte coordinates of the user's new P-256 key. Pass both into your `createWallet` call as `signerKind: "WEBAUTHN_P256"` so the SHHH wallet's first owner is this passkey. ## 2. Create a SHHH wallet bound to the passkey The passkey package never talks to chipi-back directly. Hand the credential to your existing wallet-creation flow — typically `useCreateWallet` from `@chipi-stack/chipi-react` — with the right `walletType` + `signerKind`: ```tsx theme={null} import { useCreateWallet } from "@chipi-stack/chipi-react"; import { webauthnPubkeyFelts } from "@chipi-stack/backend"; function SignUpButton() { const { createWalletAsync } = useCreateWallet(); async function onClick() { // Step 1 returns these. In a real app you'd carry them across components. const { credentialId, publicKeyHex } = await onSignUp(userId, userName); const wallet = await createWalletAsync({ params: { externalUserId: userId, chain: "STARKNET", walletType: "SHHH", signerKind: "WEBAUTHN_P256", // SHHH WEBAUTHN_P256 expects the pubkey as 4 felts // [x_low, x_high, y_low, y_high]. Derive them from the registered // passkey pubkey with webauthnPubkeyFelts (from @chipi-stack/backend). ownerPubkeyFelts: webauthnPubkeyFelts(publicKeyHex).map( (f) => "0x" + f.toString(16), ), }, bearerToken, }); return wallet; } } ``` ## 3. Sign a transaction with the passkey `signShhhMessage` prompts the user with biometrics again, runs the WebAuthn ceremony, and returns the raw signature pieces. The backend SDK accepts these as-is — you don't need to assemble the SHHH envelope yourself. ```tsx theme={null} import { signShhhMessage } from "@chipi-stack/chipi-passkey"; import { buildWebAuthnEnvelopeFromAssertion } from "@chipi-stack/backend"; async function onSendTransfer(messageHash: bigint, pubkey: { x: Uint8Array; y: Uint8Array }) { // 1. Biometric prompt + WebAuthn ceremony. const { authenticatorData, clientDataJSON, signatureDer, credentialId } = await signShhhMessage({ messageHash }); // 2. Wrap into the SHHH V2_SNIP12 envelope. const envelope = buildWebAuthnEnvelopeFromAssertion({ authenticatorData, clientDataJSON, signatureDer, pubkey, messageHash, }); // 3. Ship the envelope as the `signature` field of your paymaster call. return envelope; } ``` The wallet's address never appears in this flow — the verifier reproduces the assertion check on chain using the `WEBAUTHN_P256` verifier class registered against the wallet. ## Full Next.js / React example ```tsx theme={null} "use client"; import { useState } from "react"; import { createShhhPasskey, signShhhMessage, hasShhhPasskey, } from "@chipi-stack/chipi-passkey"; import { useCreateWallet } from "@chipi-stack/chipi-react"; import { webauthnPubkeyFelts } from "@chipi-stack/backend"; export function PasskeySignUp() { const { createWalletAsync, isLoading } = useCreateWallet(); const [wallet, setWallet] = useState(null); async function onSignUp() { const { credentialId, publicKeyHex } = await createShhhPasskey( "user-42", "Alice", ); const w = await createWalletAsync({ params: { externalUserId: "user-42", chain: "STARKNET", walletType: "SHHH", signerKind: "WEBAUTHN_P256", ownerPubkey: publicKeyHex, }, bearerToken, }); setWallet(w); } async function onSignTx() { // Compute the SHHH OE hash for the transfer you want to send — your // existing wallet flow already builds this. Replace `0n` with the // real `computeOeHash` output for your call. const messageHash = 0n; const { authenticatorData, clientDataJSON, signatureDer } = await signShhhMessage({ messageHash }); // Forward the assertion bytes to your paymaster flow — see step 3 // above for the envelope-wrapping detail. console.log({ authenticatorData, clientDataJSON, signatureDer }); } if (hasShhhPasskey()) { return ; } return ( ); } ``` ## What happens if the user has a passkey already? `hasShhhPasskey()` checks `localStorage` for a stored credential id, so you can branch your UI between "Sign up" and "Sign in" without prompting. If the user wipes their browser storage, the credential id is lost but the passkey itself still lives in the platform authenticator — you'll prompt them once to re-discover it. ## When biometrics aren't available If `isWebAuthnSupported()` returns `false`, fall back to the PIN flow described in [When passkeys fail](/sdk/passkeys/when-passkeys-fail). That flow is also the right one for **legacy CHIPI v29 wallets**, which use a PRF-derived encryption key instead of a P-256 signature. ## Related * [Overview](/sdk/passkeys/overview) * [API reference](/sdk/passkeys/api) * [When passkeys fail](/sdk/passkeys/when-passkeys-fail) # When Passkeys Fail Source: https://docs.chipipay.com/sdk/passkeys/when-passkeys-fail Real failure modes — lost device, browser switch, broken biometrics — and what your app should show the user. Plus when to add a PIN backup. Passkeys cover the happy path beautifully. The failure modes — the user buys a new laptop, switches from Chrome to Firefox, drops their phone — need explicit handling. This page is the playbook. ## The four failure modes | What happened | What the SDK throws | What the user sees | What you do | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | User cancelled the prompt | `signShhhMessage` rejects with `NotAllowedError` translated to `Error("Passkey authentication was cancelled")` | "Authentication cancelled" | Show the prompt button again. No data loss. | | New device, no passkey synced | `hasShhhPasskey()` returns `false` | Sign-up page | If the user already has a wallet, route them to recovery (see below). | | PRF unavailable after a browser switch (legacy CHIPI v29) | `getWalletEncryptKey` throws `"Passkey PRF unavailable…"` | "Use PIN to sign in" | Show PIN field. The PRF-encrypted wallet can be re-encrypted later via [useUpdateWalletEncryption](/sdk/react/hooks/use-update-wallet-encryption). | | Lost device, no other passkey | nothing throws — user is locked out | "Recover your wallet" UI | SHHH: use the guardian recovery flow. CHIPI v29: only recoverable via the PIN backup. | ## SHHH wallets — true recovery A SHHH wallet's owner set lives on chain. Recovery is a real on-chain primitive, not a workaround. **Two paths:** 1. **The user still has access** (e.g. moved from old phone to new) — `propose_add_owner` adds the new passkey as a second owner with a 48-hour timelock, then `execute_add_owner` activates it. See the recovery story in [`/services/gasless/recovery`](/services/gasless/recovery). 2. **The user lost all access** — a pre-registered guardian (a second device, a recovery email's WebAuthn credential, a "Guardian-as-a-Service" partner) calls `initiate_recovery`. After a 7-day timelock the guardian can finalize a wallet rotation onto a fresh passkey. The React side ships [``](/services/gasless/recovery) which drives both flows. The recipe at a glance: ```tsx theme={null} import { Recover, useGuardianRecovery } from "@chipi-stack/chipi-react"; import { createShhhPasskey } from "@chipi-stack/chipi-passkey"; // 1. User on the new device registers a fresh passkey. const { credentialId, publicKeyHex } = await createShhhPasskey(userId, userName); // 2. Drop into — see the gasless recovery docs for full wiring. ``` ## CHIPI v29 wallets — dual-key fallback A CHIPI v29 wallet's passkey only encrypts a STARK key; if the passkey is gone, the encryption key is gone with it. The mitigation is the **dual-key architecture**: store the same private key TWICE, once encrypted by the passkey, once encrypted by a user-chosen PIN. The wallet shape carries two ciphertexts: ```ts theme={null} type Wallet = { publicKey: string; encryptedPrivateKey: string; // passkey-derived key encrypts this encryptedPrivateKeyBackup: string; // PIN encrypts this authMethod: "passkey+pin"; // marker for the dual-key path }; ``` Pass both at creation time and at every wallet read: ```ts theme={null} import { useCreateWallet } from "@chipi-stack/chipi-react"; import { createWalletPasskey } from "@chipi-stack/chipi-passkey"; const { encryptKey: passkeyKey } = await createWalletPasskey(userId, userName); const pin = await promptForPin(); await createWalletAsync({ params: { externalUserId: userId, chain: "STARKNET", walletType: "CHIPI", encryptKey: passkeyKey, // primary — encrypts encryptedPrivateKey encryptKeyBackup: pin, // backup — encrypts encryptedPrivateKeyBackup authMethod: "passkey+pin", }, bearerToken, }); ``` Day-to-day signing uses the passkey. If the passkey ever fails (`verifyWalletPasskeyDetailed()` returns `reason: "prf_unavailable"` or `"no_passkey"`), prompt for the PIN and use the `encryptedPrivateKeyBackup` ciphertext instead. The SDK handles which ciphertext to pick when you pass the right `encryptKey`. ## PIN-only mode is a fallback, not a default A PIN-only wallet (no passkey) is supported, but it's the weakest path — see the PIN warning we use across every onboarding doc. Default to passkey; show PIN only when `isWebAuthnSupported()` returns `false` or the user explicitly opts in. ```tsx theme={null} import { isWebAuthnSupported } from "@chipi-stack/chipi-passkey"; function SignUpFlow() { if (isWebAuthnSupported()) { return ; } // Old browser, very old Android, or a corporate-managed device that // disables WebAuthn. Fall back to PIN. return ; } ``` ## Browser switching (the common case) Even fully-supported browsers don't always share PRF state. The user registers on Chrome, then opens your app in Firefox a week later. The Firefox WebAuthn ceremony succeeds — the credential is discoverable — but the PRF output is unavailable in that browser. For a SHHH wallet, this is **not a problem**: the passkey itself is the signer, no encryption key derivation. `signShhhMessage` works on any browser that supports WebAuthn. For a legacy CHIPI v29 wallet, this **is** the problem `verifyWalletPasskeyDetailed()` catches. Surface the PIN fallback UI, decrypt with the PIN, and offer to re-register the passkey on the new browser via the [migration hook](/sdk/react/hooks/use-migrate-wallet-to-passkey). ## Related * [Overview](/sdk/passkeys/overview) * [Quickstart](/sdk/passkeys/quickstart) * [API reference](/sdk/passkeys/api) * [Recovery on SHHH wallets](/services/gasless/recovery) # create_wallet Source: https://docs.chipipay.com/sdk/python/methods/create-wallet Creates a new wallet on Starknet. This method deploys the wallet contract and uses Chipi gasless paymaster to sponsor gas fees, resulting in a frictionless onboarding experience. **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. ## Usage ```python theme={null} from chipi_sdk import CreateWalletParams, WalletType, Chain wallet_response = sdk.create_wallet( params=CreateWalletParams( encrypt_key="user-secure-pin", external_user_id="your-user-id-123", wallet_type=WalletType.CHIPI, # Optional, defaults to CHIPI ) ) ``` ### Parameters * `encrypt_key` (str): A user-defined code or password used to encrypt the wallet's private key * `external_user_id` (str): Your application's unique identifier for the user * `user_id` (str, optional): Internal user ID (optional) * `wallet_type` (WalletType, optional): Type of wallet to create (`CHIPI` or `READY`). Defaults to `CHIPI` * `use_passkey` (bool, optional): Whether to use passkey authentication. Defaults to `False` ### Return Value Returns a `CreateWalletResponse` (same shape as `GetWalletResponse`) — a flat object with: * `public_key`: Wallet's public address * `encrypted_private_key`: AES encrypted private key * `wallet_type`: Type of wallet created * `normalized_public_key`: Normalized public key * `chain`: Blockchain network * `is_deployed`: Whether the wallet contract is deployed * `id`, `external_user_id`, `organization_id`, `api_public_key`, `created_at`, `updated_at` ## Example Implementation ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, CreateWalletParams, WalletType, Chain # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) def create_user_wallet(user_id: str, user_pin: str): """Create a new wallet for a user.""" try: wallet_response = sdk.create_wallet( params=CreateWalletParams( encrypt_key=user_pin, external_user_id=user_id, wallet_type=WalletType.CHIPI, ) ) print(f"✅ Wallet created successfully!") print(f" Address: {wallet_response.public_key}") print(f" View on Starkscan: https://starkscan.co/contract/{wallet_response.public_key}") # Store wallet data securely in your database save_wallet_to_database( user_id=user_id, public_key=wallet_response.public_key, encrypted_private_key=wallet_response.encrypted_private_key, ) return wallet_response except Exception as error: print(f"❌ Wallet creation failed: {error}") raise # Usage def onboard_new_user(user_id: str, pin: str): """Onboard a new user by creating their wallet.""" wallet = create_user_wallet(user_id, pin) # Verify deployment on StarkScan contract_url = f"https://starkscan.co/contract/{wallet.public_key}" print(f"View contract: {contract_url}") return wallet # Create wallet wallet = onboard_new_user("user-123", "secure-pin-1234") ``` ## Async Version For async applications, use `acreate_wallet`: ```python theme={null} import asyncio async def create_user_wallet_async(user_id: str, user_pin: str): """Create a new wallet asynchronously.""" wallet_response = await sdk.acreate_wallet( params=CreateWalletParams( encrypt_key=user_pin, external_user_id=user_id, wallet_type=WalletType.CHIPI, ) ) return wallet_response # Run async wallet = asyncio.run(create_user_wallet_async("user-123", "secure-pin-1234")) ``` ## Wallet Types The SDK supports two wallet types: **OpenZeppelin account with SNIP-9 session keys support** * Full session key support for delegated transactions * Compatible with Chipi session management * Recommended for most applications ```python theme={null} wallet_type=WalletType.CHIPI # Default ``` **Argent X Account v0.4.0** * Compatible with Argent X wallet standard * Use when Argent X compatibility is required ```python theme={null} wallet_type=WalletType.READY ``` ## Security Considerations **Encryption Key Security** The `encrypt_key` is critical for wallet security: * Never store it in plaintext * Use a secure method to derive it (e.g., from user password with PBKDF2) * Consider using hardware security modules (HSM) for production **External User ID** The `external_user_id` should be: * Unique per user in your system * Stable (doesn't change over time) * Not personally identifiable information (use a UUID instead of email) ## Error Handling ```python theme={null} from chipi_sdk import ChipiTransactionError, ChipiApiError try: wallet = sdk.create_wallet( params=CreateWalletParams( encrypt_key="user-pin", external_user_id="user-123", ) ) except ChipiTransactionError as e: # Handle transaction-specific errors print(f"Transaction error: {e.message}") print(f"Error code: {e.code}") except ChipiApiError as e: # Handle API errors print(f"API error: {e.message}") print(f"Status: {e.status}") except Exception as e: # Handle other errors print(f"Unexpected error: {e}") ``` ## Complete Example with Database Integration ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, CreateWalletParams, WalletType import sqlite3 from datetime import datetime # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) def save_wallet_to_db(user_id: str, wallet_data: dict): """Save wallet data to database.""" conn = sqlite3.connect('app.db') cursor = conn.cursor() cursor.execute(''' INSERT INTO wallets (user_id, public_key, encrypted_private_key, created_at) VALUES (?, ?, ?, ?) ''', ( user_id, wallet_data['public_key'], wallet_data['encrypted_private_key'], datetime.now().isoformat() )) conn.commit() conn.close() def create_and_save_wallet(user_id: str, user_pin: str): """Create wallet and save to database.""" # Create wallet wallet_response = sdk.create_wallet( params=CreateWalletParams( encrypt_key=user_pin, external_user_id=user_id, wallet_type=WalletType.CHIPI, ) ) # Save to database save_wallet_to_db(user_id, { 'public_key': wallet_response.public_key, 'encrypted_private_key': wallet_response.encrypted_private_key, }) print(f"✅ Wallet created and saved for user: {user_id}") print(f" Address: {wallet_response.public_key}") return wallet_response # Usage wallet = create_and_save_wallet("user-123", "secure-pin-1234") ``` ## Related Methods * [get\_wallet](/sdk/python/methods/get-wallet) - Retrieve existing wallet information * [transfer](/sdk/python/methods/transfer) - Send tokens from the created wallet # get_token_balance Source: https://docs.chipipay.com/sdk/python/methods/get-token-balance Fetches the on-chain token balance for a wallet on Starknet. ## Usage ```python theme={null} from chipi_sdk import GetTokenBalanceParams, ChainToken, Chain balance = sdk.get_token_balance( params=GetTokenBalanceParams( chain_token=ChainToken.USDC, chain=Chain.STARKNET, wallet_public_key="0x04...abc", ) ) ``` ### Parameters * `chain_token` (ChainToken): Token to query (e.g., `ChainToken.USDC`, `ChainToken.ETH`) * `chain` (Chain): Blockchain network (e.g., `Chain.STARKNET`) * `wallet_public_key` (str, optional): Wallet address. Use this or `external_user_id` * `external_user_id` (str, optional): External user ID. Use this or `wallet_public_key` ### Return Value Returns a `GetTokenBalanceResponse` object containing: * `chain`: Blockchain network * `chain_token`: Token type * `chain_token_address`: On-chain contract address for the token * `decimals`: Token decimal places * `balance`: Token balance as a string ## Example Implementation ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, GetTokenBalanceParams, ChainToken, Chain # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) def check_balance(wallet_address: str): """Check USDC balance for a wallet.""" try: balance = sdk.get_token_balance( params=GetTokenBalanceParams( chain_token=ChainToken.USDC, chain=Chain.STARKNET, wallet_public_key=wallet_address, ) ) print(f"Token: {balance.chain_token}") print(f"Balance: {balance.balance}") print(f"Decimals: {balance.decimals}") print(f"Contract: {balance.chain_token_address}") return balance except Exception as error: print(f"Failed to get balance: {error}") raise # Usage balance = check_balance("0x04...abc") ``` ## Async Version For async applications, use `aget_token_balance`: ```python theme={null} import asyncio async def check_balance_async(wallet_address: str): """Check USDC balance asynchronously.""" balance = await sdk.aget_token_balance( params=GetTokenBalanceParams( chain_token=ChainToken.USDC, chain=Chain.STARKNET, wallet_public_key=wallet_address, ) ) return balance # Run async balance = asyncio.run(check_balance_async("0x04...abc")) ``` ## Query by External User ID You can query balance using the external user ID instead of the wallet address: ```python theme={null} balance = sdk.get_token_balance( params=GetTokenBalanceParams( chain_token=ChainToken.USDC, chain=Chain.STARKNET, external_user_id="user-123", ) ) ``` ## Checking Multiple Token Balances ```python theme={null} from chipi_sdk import GetTokenBalanceParams, ChainToken, Chain, ChipiApiError def get_all_balances(wallet_address: str): """Get balances for all supported tokens.""" tokens = [ ChainToken.USDC, ChainToken.ETH, ChainToken.STRK, ChainToken.USDT, ChainToken.DAI, ChainToken.WBTC, ] balances = {} for token in tokens: try: result = sdk.get_token_balance( params=GetTokenBalanceParams( chain_token=token, chain=Chain.STARKNET, wallet_public_key=wallet_address, ) ) balances[token.value] = result.balance except ChipiApiError as e: print(f"Failed to fetch {token.value}: {e.message}") balances[token.value] = None return balances # Usage all_balances = get_all_balances("0x04...abc") for token, amount in all_balances.items(): print(f"{token}: {amount}") ``` ## Balance Check Before Transfer A common pattern is checking the balance before initiating a transfer: ```python theme={null} from chipi_sdk import ( GetTokenBalanceParams, GetWalletParams, TransferParams, WalletData, ChainToken, Chain, ) def transfer_with_balance_check( user_id: str, recipient: str, amount: str, pin: str, ): """Transfer USDC after verifying sufficient balance.""" # Get wallet wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=user_id) ) if not wallet_response: raise ValueError(f"Wallet not found for user: {user_id}") # Check balance balance = sdk.get_token_balance( params=GetTokenBalanceParams( chain_token=ChainToken.USDC, chain=Chain.STARKNET, wallet_public_key=wallet_response.public_key, ) ) from decimal import Decimal if Decimal(balance.balance) < Decimal(amount): raise ValueError( f"Insufficient balance: {balance.balance} USDC available, " f"{amount} USDC required" ) # Execute transfer tx_hash = sdk.transfer( params=TransferParams( encrypt_key=pin, wallet=WalletData( public_key=wallet_response.public_key, encrypted_private_key=wallet_response.encrypted_private_key, ), token=ChainToken.USDC, recipient=recipient, amount=amount, ) ) print(f"Transfer successful: {tx_hash}") return tx_hash # Usage tx = transfer_with_balance_check( user_id="user-123", recipient="0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", amount="25.0", pin="secure-pin-1234", ) ``` ## Error Handling ```python theme={null} from chipi_sdk import ChipiApiError, GetTokenBalanceParams, ChainToken, Chain def safe_get_balance(wallet_address: str, token: ChainToken = ChainToken.USDC): """Get token balance with error handling.""" try: balance = sdk.get_token_balance( params=GetTokenBalanceParams( chain_token=token, chain=Chain.STARKNET, wallet_public_key=wallet_address, ) ) return { "success": True, "balance": balance.balance, "token": balance.chain_token, "decimals": balance.decimals, } except ChipiApiError as e: print(f"API error: {e.message}") return { "success": False, "error": e.message, "status": e.status, } except Exception as e: print(f"Unexpected error: {e}") return { "success": False, "error": str(e), } # Usage result = safe_get_balance("0x04...abc") if result["success"]: print(f"Balance: {result['balance']}") else: print(f"Error: {result['error']}") ``` ## Related Methods * [get\_wallet](/sdk/python/methods/get-wallet) - Get wallet information * [transfer](/sdk/python/methods/transfer) - Send tokens from the wallet * [create\_wallet](/sdk/python/methods/create-wallet) - Create a new wallet # get_transaction Source: https://docs.chipipay.com/sdk/python/methods/get-transaction Fetches a single transaction by its hash or internal ID. ## Usage ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK(config=ChipiSDKConfig( api_public_key="pk_prod_YOUR_KEY", api_secret_key="sk_prod_YOUR_KEY", )) tx = sdk.get_transaction(hash_or_id="0x...") ``` ### Parameters * `hash_or_id` (str): Transaction hash (`0x...`) or internal database ID * `bearer_token` (str, optional): JWT token. Falls back to `api_secret_key` if not provided ### Return Value Returns a `Transaction` object with `id`, `transaction_hash`, `status`, `sender_address`, `destination_address`, `amount`, `token`, and timestamps. ## Example Implementation ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_YOUR_KEY", api_secret_key="sk_prod_YOUR_KEY", ) ) # Fetch by hash tx = sdk.get_transaction("0x04abc...def") print(f"Status: {tx.status}, Amount: {tx.amount}") # Fetch by internal ID tx = sdk.get_transaction("tx-123") ``` ## Async Version ```python theme={null} import asyncio async def main(): tx = await sdk.aget_transaction("0x04abc...def") print(f"Status: {tx.status}") asyncio.run(main()) ``` # get_transaction_status Source: https://docs.chipipay.com/sdk/python/methods/get-transaction-status Fetches the on-chain status of a transaction from StarkNet. ## Usage ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK(config=ChipiSDKConfig( api_public_key="pk_prod_YOUR_KEY", api_secret_key="sk_prod_YOUR_KEY", )) status = sdk.get_transaction_status(hash="0x...") ``` ### Parameters * `hash` (str): Transaction hash (`0x...`) * `bearer_token` (str, optional): JWT token. Falls back to `api_secret_key` ### Return Value Returns a `TransactionStatusResponse` with: * `transaction_hash` (str): Transaction hash * `status` (OnChainTxStatus): `RECEIVED`, `PENDING`, `ACCEPTED_ON_L2`, `ACCEPTED_ON_L1`, `REJECTED`, `REVERTED`, or `NOT_RECEIVED` * `block_number` (int, optional): Block number if included * `revert_reason` (str, optional): Reason if `REVERTED` ## Example Implementation ```python theme={null} import time from chipi_sdk import ChipiSDK, ChipiSDKConfig, OnChainTxStatus sdk = ChipiSDK(config=ChipiSDKConfig( api_public_key="pk_prod_YOUR_KEY", api_secret_key="sk_prod_YOUR_KEY", )) terminal = {OnChainTxStatus.ACCEPTED_ON_L1, OnChainTxStatus.REJECTED, OnChainTxStatus.REVERTED} def wait_for_confirmation(tx_hash: str) -> OnChainTxStatus: while True: result = sdk.get_transaction_status(tx_hash) if result.status in terminal: if result.status == OnChainTxStatus.REVERTED: print(f"Reverted: {result.revert_reason}") return result.status time.sleep(3) ``` ## Async Version ```python theme={null} import asyncio async def main(): status = await sdk.aget_transaction_status("0x04abc...def") print(f"Status: {status.status}") asyncio.run(main()) ``` # get_wallet Source: https://docs.chipipay.com/sdk/python/methods/get-wallet Retrieves wallet information for a user by their external user ID. ## Usage ```python theme={null} from chipi_sdk import GetWalletParams wallet_response = sdk.get_wallet( params=GetWalletParams( external_user_id="your-user-id-123" ) ) ``` ### Parameters * `external_user_id` (str): Your application's unique identifier for the user ### Return Value Returns a `GetWalletResponse` object containing: * `id`: Internal wallet ID * `public_key`: Wallet's public address * `encrypted_private_key`: AES encrypted private key * `wallet_type`: Type of wallet * `normalized_public_key`: Normalized public key * `chain`: Blockchain network * `is_deployed`: Whether the wallet contract is deployed * `external_user_id`: Your application's user ID * `organization_id`: Organization ID (optional) * `api_public_key`: Organization API public key * `created_at`: Wallet creation timestamp * `updated_at`: Last update timestamp Returns `None` if wallet is not found. ## Example Implementation ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, GetWalletParams # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) def get_user_wallet(user_id: str): """Get wallet for a specific user.""" try: wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=user_id) ) if wallet_response is None: print(f"No wallet found for user: {user_id}") return None print(f"✅ Wallet found!") print(f" Address: {wallet_response.public_key}") print(f" Created: {wallet_response.created_at}") return wallet_response except Exception as error: print(f"❌ Failed to get wallet: {error}") raise # Usage wallet = get_user_wallet("user-123") if wallet: print(f"User wallet address: {wallet.public_key}") ``` ## Async Version For async applications, use `aget_wallet`: ```python theme={null} import asyncio async def get_user_wallet_async(user_id: str): """Get wallet asynchronously.""" wallet_response = await sdk.aget_wallet( params=GetWalletParams(external_user_id=user_id) ) return wallet_response # Run async wallet = asyncio.run(get_user_wallet_async("user-123")) ``` ## Handling Missing Wallets ```python theme={null} def get_or_create_wallet(user_id: str, user_pin: str): """Get existing wallet or create a new one.""" from chipi_sdk import CreateWalletParams, WalletType # Try to get existing wallet wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=user_id) ) if wallet_response: print(f"Found existing wallet for {user_id}") return wallet_response # Create new wallet if not found print(f"Creating new wallet for {user_id}") new_wallet = sdk.create_wallet( params=CreateWalletParams( encrypt_key=user_pin, external_user_id=user_id, wallet_type=WalletType.CHIPI, ) ) return new_wallet # Usage wallet_data = get_or_create_wallet("user-123", "secure-pin-1234") ``` ## Complete Example with Caching ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, GetWalletParams from functools import lru_cache from datetime import datetime, timedelta # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) # Simple in-memory cache wallet_cache = {} cache_ttl = timedelta(minutes=5) def get_wallet_cached(user_id: str): """Get wallet with caching to reduce API calls.""" # Check cache if user_id in wallet_cache: cached_data, timestamp = wallet_cache[user_id] if datetime.now() - timestamp < cache_ttl: print(f"📦 Returning cached wallet for {user_id}") return cached_data # Fetch from API print(f"🌐 Fetching wallet from API for {user_id}") wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=user_id) ) # Update cache if wallet_response: wallet_cache[user_id] = (wallet_response, datetime.now()) return wallet_response # Usage wallet1 = get_wallet_cached("user-123") # API call wallet2 = get_wallet_cached("user-123") # From cache ``` ## Using Wallet Data for Transactions After retrieving a wallet, you can use it for transactions: ```python theme={null} from chipi_sdk import GetWalletParams, TransferParams, ChainToken, WalletData # Get wallet wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id="user-123") ) if wallet_response: # Use wallet for transfer tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-secure-pin", wallet=WalletData( public_key=wallet_response.public_key, encrypted_private_key=wallet_response.encrypted_private_key, ), token=ChainToken.USDC, recipient="0x...", amount="10.0", ) ) print(f"Transfer completed: {tx_hash}") ``` ## Error Handling ```python theme={null} from chipi_sdk import ChipiApiError, GetWalletParams def safe_get_wallet(user_id: str): """Get wallet with error handling.""" try: wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=user_id) ) if wallet_response is None: return { "success": False, "error": "Wallet not found", "user_id": user_id } return { "success": True, "public_key": wallet_response.public_key, "encrypted_private_key": wallet_response.encrypted_private_key, "created_at": wallet_response.created_at } except ChipiApiError as e: print(f"❌ API error: {e.message}") return { "success": False, "error": e.message, "status": e.status } except Exception as e: print(f"❌ Unexpected error: {e}") return { "success": False, "error": str(e) } # Usage result = safe_get_wallet("user-123") if result["success"]: print(f"Wallet: {result['public_key']}") else: print(f"Error: {result['error']}") ``` ## Related Methods * [create\_wallet](/sdk/python/methods/create-wallet) - Create a new wallet * [transfer](/sdk/python/methods/transfer) - Send tokens from the wallet * [get\_token\_balance](/sdk/backend/methods/get-wallet) - Check wallet balance # transfer Source: https://docs.chipipay.com/sdk/python/methods/transfer Transfers tokens from a wallet to another address. Uses Chipi gasless paymaster to cover gas fees, making transactions frictionless for your users. ## Usage ```python theme={null} from chipi_sdk import TransferParams, ChainToken tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-secure-pin", wallet=wallet_data, # WalletData object token=ChainToken.USDC, recipient="0x1234567890abcdef...", amount="1.5", # Amount in USDC ) ) ``` ### Parameters * `encrypt_key` (str): PIN used to decrypt the private key * `wallet` (WalletData): Wallet object with `public_key` and `encrypted_private_key` * `token` (ChainToken): Token type to transfer (e.g., `ChainToken.USDC`, `ChainToken.ETH`) * `recipient` (str): Destination wallet address (must start with `0x`) * `amount` (str): Transfer amount (in token units, not wei) * `other_token` (dict, optional): Custom token info for `ChainToken.OTHER` type * `use_passkey` (bool, optional): Whether to use passkey authentication. Defaults to `False` ### Return Value Returns a transaction hash string. ## Example Implementation ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig, TransferParams, ChainToken, WalletData # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) def transfer_usdc( wallet: WalletData, recipient_address: str, amount: str, user_pin: str ): """Transfer USDC tokens.""" try: tx_hash = sdk.transfer( params=TransferParams( encrypt_key=user_pin, wallet=wallet, token=ChainToken.USDC, recipient=recipient_address, amount=amount, ) ) print(f"✅ Transfer completed successfully!") print(f" Transaction hash: {tx_hash}") print(f" View on Starkscan: https://starkscan.co/tx/{tx_hash}") return tx_hash except Exception as error: print(f"❌ Transfer failed: {error}") raise # Usage def send_payment(sender_user_id: str, recipient_address: str, amount: str, pin: str): """Send USDC payment from user wallet.""" # Get sender's wallet wallet = sdk.get_wallet( params=GetWalletParams(external_user_id=sender_user_id) ) if not wallet: raise ValueError(f"Wallet not found for user: {sender_user_id}") # Perform transfer tx_hash = transfer_usdc( wallet=wallet.wallet, recipient_address=recipient_address, amount=amount, user_pin=pin ) print(f"Sent {amount} USDC to {recipient_address}") return tx_hash # Send 10 USDC tx = send_payment( sender_user_id="user-123", recipient_address="0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", amount="10.0", pin="secure-pin-1234" ) ``` ## Async Version For async applications, use `atransfer`: ```python theme={null} import asyncio async def transfer_usdc_async(wallet: WalletData, recipient: str, amount: str, pin: str): """Transfer USDC asynchronously.""" tx_hash = await sdk.atransfer( params=TransferParams( encrypt_key=pin, wallet=wallet, token=ChainToken.USDC, recipient=recipient, amount=amount, ) ) return tx_hash # Run async tx_hash = asyncio.run(transfer_usdc_async(wallet, recipient, "5.0", "pin")) ``` ## Supported Tokens The SDK supports multiple token types: ```python theme={null} from chipi_sdk import ChainToken # Available tokens ChainToken.USDC # USDC (Native) ChainToken.USDC_E # USDC (Bridged) ChainToken.USDT # Tether USD ChainToken.ETH # Ethereum ChainToken.STRK # Starknet token ChainToken.DAI # DAI stablecoin ChainToken.WBTC # Wrapped Bitcoin ChainToken.OTHER # Custom token (requires other_token param) ``` ### Transferring Different Tokens ```python theme={null} tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-pin", wallet=wallet_data, token=ChainToken.USDC, recipient="0x...", amount="100.5", # 100.5 USDC ) ) ``` ```python theme={null} tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-pin", wallet=wallet_data, token=ChainToken.ETH, recipient="0x...", amount="0.01", # 0.01 ETH ) ) ``` ```python theme={null} tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-pin", wallet=wallet_data, token=ChainToken.STRK, recipient="0x...", amount="50", # 50 STRK ) ) ``` ```python theme={null} tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-pin", wallet=wallet_data, token=ChainToken.OTHER, other_token={ "contractAddress": "0x...", "decimals": 18, }, recipient="0x...", amount="1000", ) ) ``` ## Complete Example with Error Handling ```python theme={null} from chipi_sdk import ( ChipiSDK, ChipiSDKConfig, TransferParams, ChainToken, GetWalletParams, ChipiTransactionError, ChipiApiError ) # Initialize SDK sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) def safe_transfer( sender_user_id: str, recipient_address: str, amount: str, user_pin: str ): """Safely transfer USDC with comprehensive error handling.""" try: # Validate recipient address if not recipient_address.startswith("0x") or len(recipient_address) != 66: raise ValueError("Invalid recipient address format") # Get sender wallet wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=sender_user_id) ) if not wallet_response: raise ValueError(f"Wallet not found for user: {sender_user_id}") # Check balance (optional - add your balance check logic) # balance = sdk.get_token_balance(...) # Execute transfer print(f"Initiating transfer of {amount} USDC...") tx_hash = sdk.transfer( params=TransferParams( encrypt_key=user_pin, wallet=wallet_response.wallet, token=ChainToken.USDC, recipient=recipient_address, amount=amount, ) ) print(f"✅ Transfer successful!") print(f" TX Hash: {tx_hash}") print(f" Starkscan: https://starkscan.co/tx/{tx_hash}") # Log transaction to database log_transaction(sender_user_id, recipient_address, amount, tx_hash) return { "success": True, "tx_hash": tx_hash, "amount": amount, "token": "USDC", "recipient": recipient_address } except ChipiTransactionError as e: print(f"❌ Transaction error: {e.message}") print(f" Error code: {e.code}") return {"success": False, "error": str(e)} except ChipiApiError as e: print(f"❌ API error: {e.message}") print(f" Status: {e.status}") return {"success": False, "error": str(e)} except ValueError as e: print(f"❌ Validation error: {e}") return {"success": False, "error": str(e)} except Exception as e: print(f"❌ Unexpected error: {e}") return {"success": False, "error": str(e)} # Usage result = safe_transfer( sender_user_id="user-123", recipient_address="0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", amount="25.50", user_pin="secure-pin-1234" ) if result["success"]: print(f"Payment of {result['amount']} USDC sent successfully!") else: print(f"Payment failed: {result['error']}") ``` ## Batch Transfers Transfer to multiple recipients: ```python theme={null} def batch_transfer(sender_user_id: str, recipients: list, amount_per_recipient: str, pin: str): """Send USDC to multiple recipients.""" # Get sender wallet once wallet_response = sdk.get_wallet( params=GetWalletParams(external_user_id=sender_user_id) ) results = [] for recipient in recipients: try: tx_hash = sdk.transfer( params=TransferParams( encrypt_key=pin, wallet=wallet_response.wallet, token=ChainToken.USDC, recipient=recipient, amount=amount_per_recipient, ) ) results.append({"recipient": recipient, "tx_hash": tx_hash, "success": True}) print(f"✅ Sent {amount_per_recipient} USDC to {recipient[:10]}...") except Exception as e: results.append({"recipient": recipient, "error": str(e), "success": False}) print(f"❌ Failed to send to {recipient[:10]}...: {e}") return results # Send 10 USDC to 3 recipients results = batch_transfer( sender_user_id="user-123", recipients=[ "0x1111111111111111111111111111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222222222222222222222222222", "0x3333333333333333333333333333333333333333333333333333333333333333", ], amount_per_recipient="10.0", pin="secure-pin-1234" ) # Summary successful = sum(1 for r in results if r["success"]) print(f"\n📊 Batch transfer complete: {successful}/{len(results)} successful") ``` ## Security Considerations **Transaction Security** * Always validate recipient addresses before transferring * Implement transfer limits for security * Log all transactions for audit purposes * Use rate limiting to prevent abuse * Never expose encryption keys in logs or error messages **Gas Fees** All transfers use Chipi's gasless paymaster - users don't need ETH for gas! ## Related Methods * [get\_wallet](/sdk/python/methods/get-wallet) - Get wallet information before transfers * [call\_any\_contract](/sdk/python/methods/call-any-contract) - For custom contract interactions * [execute\_transaction](/sdk/backend/methods/transfer) - Backend SDK equivalent # Python SDK Quickstart Source: https://docs.chipipay.com/sdk/python/quickstart Quick setup guide for the Chipi Python SDK - server-side wallet management and gasless transactions for Python applications The Python SDK is designed for server-side use. You must use both your public and **secret** API keys. Never expose your secret key in client-side code. **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. The Python SDK depends on `starknet.py`, which requires certain system libraries to be installed first. The following instructions assume [Homebrew](https://brew.sh) is installed. First, install `cmake` (required for building crypto dependencies): ```bash theme={null} brew install cmake gmp ``` If you're using an Apple Silicon Mac (M1/M2/M3), you may need to set additional flags during installation. See the [installation section](#installing-the-sdk) below. For more details on macOS installation, see the [starknet.py installation guide](https://starknetpy.readthedocs.io/en/latest/installation.html). ```bash theme={null} sudo apt install -y libgmp3-dev ``` For more details, see the [starknet.py installation guide](https://starknetpy.readthedocs.io/en/latest/installation.html). Windows installation requires MinGW. The recommended way to install is through [chocolatey](https://chocolatey.org/). After installing chocolatey, run: ```bash theme={null} choco install mingw ``` Make sure MinGW is in your PATH (e.g., `C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin`). For more details and troubleshooting, see the [starknet.py installation guide](https://starknetpy.readthedocs.io/en/latest/installation.html). ```bash pip theme={null} pip install chipi-stack ``` ```bash poetry theme={null} poetry add chipi-stack ``` ```bash uv theme={null} uv add chipi-stack ``` **Install:** `chipi-stack` (PyPI). **Import:** `from chipi_sdk import ...`. The PyPI package name and the Python module name differ — `pip install chipi-stack` succeeds, but `import chipi_stack` will fail. Always import from `chipi_sdk`. If you're on an Apple Silicon Mac (M1/M2/M3) and encounter issues, use these flags: ```bash theme={null} CFLAGS=-I`brew --prefix gmp`/include LDFLAGS=-L`brew --prefix gmp`/lib pip install chipi-stack ``` The Python SDK requires **both** your public key AND secret key for authentication. 1. Go to your [API Keys](https://dashboard.chipipay.com/configure/api-keys) in the Chipi Dashboard 2. Copy your **Public Key** (`pk_prod_xxxx`) 3. Copy your **Secret Key** (`sk_prod_xxxx`) Your secret key is used for backend authentication. Keep it secure and never commit it to version control. Create a new instance of the ChipiSDK with your API keys: ```python theme={null} from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) ``` Now you can create a wallet for your users: ```python theme={null} from chipi_sdk import CreateWalletParams, WalletType # Create a new wallet wallet_response = sdk.create_wallet( params=CreateWalletParams( encrypt_key="user-secure-pin", external_user_id="your-user-id-123", wallet_type=WalletType.CHIPI, # Optional, defaults to CHIPI ) ) print(f"Wallet created: {wallet_response.public_key}") print(f"Deployed: {wallet_response.is_deployed}") # wallet_response is a flat object — use it directly # wallet_response.public_key # wallet_response.encrypted_private_key ``` Transfer USDC between wallets: ```python theme={null} from chipi_sdk import TransferParams, ChainToken, WalletData # Transfer USDC tx_hash = sdk.transfer( params=TransferParams( encrypt_key="user-secure-pin", wallet=WalletData( public_key=wallet_response.public_key, encrypted_private_key=wallet_response.encrypted_private_key, ), token=ChainToken.USDC, recipient="0x1234567890abcdef...", # Recipient address amount="1.5", # Amount in USDC ) ) print(f"Transfer completed! TX: {tx_hash}") print(f"View on Starkscan: https://starkscan.co/tx/{tx_hash}") ``` For production applications, store your API keys as environment variables: ```bash theme={null} # .env CHIPI_PUBLIC_KEY=pk_prod_your_public_key CHIPI_SECRET_KEY=sk_prod_your_secret_key ``` Then initialize the SDK: ```python theme={null} import os from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key=os.getenv("CHIPI_PUBLIC_KEY"), api_secret_key=os.getenv("CHIPI_SECRET_KEY"), ) ) ``` Consider using [python-dotenv](https://pypi.org/project/python-dotenv/) to load environment variables from `.env` files. ## Async Support The Python SDK supports both sync and async operations. For better performance in async applications, use the async methods: ```python theme={null} import asyncio from chipi_sdk import ChipiSDK, ChipiSDKConfig, CreateWalletParams sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key="pk_prod_your_public_key", api_secret_key="sk_prod_your_secret_key", ) ) async def create_wallet_async(): wallet_response = await sdk.acreate_wallet( params=CreateWalletParams( encrypt_key="user-secure-pin", external_user_id="your-user-id-123", ) ) return wallet_response # Run async function wallet = asyncio.run(create_wallet_async()) ``` All SDK methods have async equivalents prefixed with `a` (e.g., `create_wallet` → `acreate_wallet`, `transfer` → `atransfer`). ## Next Steps Now that you have the basic setup working, explore more advanced features: * **[Create Wallet](/sdk/python/methods/create-wallet)** - Detailed wallet creation guide * **[Transfer Tokens](/sdk/python/methods/transfer)** - Send USDC and other tokens * **[Backend API Reference](/sdk/backend/quickstart)** - Full SDK method documentation ## Security Best Practices * Never commit your secret API key to version control * Use environment variables for all sensitive credentials * Validate user inputs before making API calls * Securely store wallet encryption keys (never in plaintext) Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # React with Better Auth Source: https://docs.chipipay.com/sdk/react/gasless-better-auth-setup Learn how to integrate Chipi Pay with your React application using Better Auth for authentication and secure wallet storage. First, follow the complete [Better Auth Installation Guide](https://www.better-auth.com/docs/installation) to set up authentication in your React app. Then proceed to [Better Auth Basic Usage](https://www.better-auth.com/docs/basic-usage) to create login and sign-up pages. This will guide you through: * Creating Better Auth tables and schemas * Setting up your React app with Better Auth * Configuring environment variables * Creating `auth.ts`, mounting the handler, and client instance **Come back here once you've completed the Better Auth setup!** In your `auth.ts` file, add the JWT plugin: ```typescript theme={null} import { betterAuth } from "better-auth"; import { Pool } from "pg"; import { jwt } from "better-auth/plugins"; export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL }), emailAndPassword: { enabled: true, }, plugins: [ jwt({ jwks: { keyPairConfig: { alg: "RS256", modulusLength: 2048, } }, jwt: { issuer: process.env.JWT_ISSUER, audience: process.env.JWT_AUDIENCE, expirationTime: "15m" } }) ] }); ``` Then migrate or generate your database to add the JWT tables. Install the Chipi Pay SDK for React: ```bash theme={null} npm install @chipi-stack/chipi-react ``` Add your API keys to your `.env` file: ```bash theme={null} # Your existing Better Auth variables DATABASE_URL=your_database_url JWT_ISSUER=your_jwt_issuer JWT_AUDIENCE=your_jwt_audience # Add the Chipi Variables VITE_CHIPI_API_KEY=your_chipi_api_public_key ``` You can get your Chipi API keys from the [Chipi Dashboard](https://dashboard.chipipay.com/configure/api-keys). ```tsx theme={null} // App.tsx import { ChipiProvider } from "@chipi-stack/chipi-react"; export default function App() { return ( {/* Your app components */} ); } ``` Create a wallet using Better Auth's JWT token: ```typescript theme={null} // components/CreateWallet.tsx import { useState } from "react"; import { useCreateWallet } from "@chipi-stack/chipi-react"; import { authClient } from "../lib/auth-client"; export default function CreateWallet() { const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const handleCreateWallet = async () => { // Get JWT from Better Auth (returned in set-auth-jwt header) const { data, response: sessionResponse } = await authClient.getSession({ fetchOptions: { onSuccess: (ctx) => ctx }, }); const bearerToken = sessionResponse?.headers?.get("set-auth-jwt") ?? data?.session?.token; const userId = data?.user?.id; if (!bearerToken || !userId) { console.error("Please sign in first"); return; } const walletResponse = await createWalletAsync({ params: { encryptKey, externalUserId: userId, }, bearerToken, }); console.log("Wallet created:", walletResponse.publicKey); }; return (
setEncryptKey(e.target.value)} />
); } ```
Register your Better Auth JWKS endpoint in the [Chipi Dashboard](https://dashboard.chipipay.com/configure/jwks-endpoint): 1. Go to Configure > Auth Provider 2. Select **Better Auth** as your provider 3. Enter your application URL (the dashboard will auto-detect `/api/auth/jwks`) 4. Click **Verify & Save**
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # React with Clerk Source: https://docs.chipipay.com/sdk/react/gasless-clerk-setup Learn how to integrate Chipi Pay with your React application using Clerk for authentication and secure wallet storage. To get started with Chipi Pay in your React application ```bash theme={null} npm create vite@latest my-chipi-app -- --template react-ts cd my-chipi-app npm install ``` With Clerk, you can follow their [Quickstart Guide](https://clerk.com/docs/quickstarts/react) to get started with React + Vite. First, install the required packages: ```bash theme={null} # Install Chipi Pay SDK npm install @chipi-stack/chipi-react # Install Clerk npm install @clerk/clerk-react ``` 1. Create a `.env` file in your project root and add your API keys: ```bash theme={null} # Vite uses VITE_ prefix for environment variables VITE_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key VITE_CHIPI_API_KEY=your_chipi_api_public_key ``` 2. Set up the Chipi Pay provider in your application : ```tsx theme={null} // App.tsx import { ClerkProvider } from "@clerk/clerk-react"; import { ChipiProvider } from "@chipi-stack/chipi-react"; export default function App() { return ( {/* Your app components */} ); } ``` Create a wallet creation component: ```typescript theme={null} // components/CreateWallet.tsx import { useState } from "react"; import { useUser, useAuth } from "@clerk/clerk-react"; import { useCreateWallet } from "@chipi-stack/chipi-react"; export default function CreateWallet() { const { user } = useUser(); const { getToken } = useAuth(); const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const handleCreateWallet = async () => { if (!encryptKey) { setError("Please enter an encryption key"); return; } try { setError(""); setSuccess(""); const token = await getToken(); const response = await createWalletAsync({ params: { encryptKey, externalUserId: user.id, }, bearerToken: token || "", }); console.log('createWalletResponse', response); setSuccess("Wallet created successfully!"); setEncryptKey(""); } catch (error) { setError(error.message || "Failed to create wallet"); console.error("Error creating wallet:", error); } }; return (

Create Wallet

{error &&
{error}
} {success &&
{success}
}
setEncryptKey(e.target.value)} className="w-full p-2 border rounded" />
); } ```
Here's an example of implementing a transfer flow with Clerk authentication: ```typescript theme={null} // components/Transfer.tsx import { useState } from "react"; import { useUser, useAuth } from "@clerk/clerk-react"; import { useTransfer, useGetWallet, ChainToken } from "@chipi-stack/chipi-react"; // Native USDC on Starknet mainnet (not USDC.e bridged) const USDC_CONTRACT = "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb"; export default function Transfer() { const { user } = useUser(); const { getToken } = useAuth(); const [amount, setAmount] = useState(""); const [recipientAddress, setRecipientAddress] = useState(""); const [encryptKey, setEncryptKey] = useState(""); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const { getWalletAsync } = useGetWallet(); const { transferAsync, isLoading: isLoadingTransfer } = useTransfer(); const handleTransfer = async () => { if (!amount || !recipientAddress || !encryptKey) { setError("Please fill in all fields"); return; } try { setError(""); setSuccess(""); const token = await getToken(); if (!token) { setError("No bearer token found"); return; } const wallet = await getWalletAsync({ externalUserId: user.id, bearerToken: token, }); // make the transfer const transferResponse = await transferAsync({ bearerToken: token, params: { encryptKey, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, }, amount: String(amount), token: ChainToken.USDC, recipient: recipientAddress, }, }); console.log("transfer response", transferResponse); setSuccess("Transfer completed successfully!"); // Clear form setAmount(""); setRecipientAddress(""); setEncryptKey(""); } catch (error) { setError(error.message || "Transfer failed"); console.error("Transfer error:", error); } }; return (

Transfer USDC

{error &&
{error}
} {success &&
{success}
}
setRecipientAddress(e.target.value)} className="w-full p-2 border rounded" /> setAmount(e.target.value)} className="w-full p-2 border rounded" /> setEncryptKey(e.target.value)} className="w-full p-2 border rounded" />
); } ```
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # React with Custom Auth Source: https://docs.chipipay.com/sdk/react/gasless-custom-auth-setup Integrate Chipi Pay with any OIDC-compliant auth provider (Auth0, Cognito, Okta, Keycloak) in your React application. ## What is JWKS? JSON Web Key Set (JWKS) is a standard that allows Chipi Pay to verify your users' JWT tokens without ever seeing their credentials. Your auth provider exposes a public endpoint with its signing keys, and Chipi uses those keys to verify that tokens are authentic. Any OIDC-compliant provider (Auth0, Cognito, Okta, Keycloak, etc.) exposes a JWKS endpoint. ```bash theme={null} npm install @chipi-stack/chipi-react ``` Add your Chipi API key to your `.env` file: ```bash theme={null} VITE_CHIPI_API_KEY=your_chipi_api_public_key ``` You can get your API keys from the [Chipi Dashboard](https://dashboard.chipipay.com/configure/api-keys). Wrap your app with `ChipiProvider` alongside your auth provider: ```tsx theme={null} // App.tsx import { ChipiProvider } from "@chipi-stack/chipi-react"; export default function App() { return ( {/* Your app components */} ); } ``` Use your auth provider's SDK to get a JWT token, then pass it to Chipi hooks: ```typescript theme={null} // components/CreateWallet.tsx import { useState } from "react"; import { useCreateWallet, Chain } from "@chipi-stack/chipi-react"; // Replace with your auth provider's token method async function getBearerToken(): Promise { // Auth0: const { getAccessTokenSilently } = useAuth0(); // Cognito: const session = await Auth.currentSession(); // Okta: const { authState } = useOktaAuth(); // Keycloak: const { token } = useKeycloak(); return "your-jwt-token"; } export default function CreateWallet() { const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const handleCreateWallet = async () => { const bearerToken = await getBearerToken(); const response = await createWalletAsync({ params: { encryptKey, externalUserId: "your-user-id", chain: Chain.STARKNET }, bearerToken, }); console.log("Wallet created:", response.publicKey); }; return (
setEncryptKey(e.target.value)} />
); } ```
Register your auth provider's JWKS endpoint in the [Chipi Dashboard](https://dashboard.chipipay.com/configure/jwks-endpoint): 1. Go to Configure > Auth Provider 2. Select **Other** as your provider 3. Paste your provider's URL or JWKS endpoint 4. The dashboard will try OIDC discovery automatically 5. Click **Verify & Save** ### Compatible providers | Provider | JWKS URL pattern | | ----------- | ------------------------------------------------------------------------ | | Auth0 | `https://YOUR_DOMAIN/.well-known/jwks.json` | | AWS Cognito | `https://cognito-idp.REGION.amazonaws.com/POOL_ID/.well-known/jwks.json` | | Okta | `https://YOUR_DOMAIN/oauth2/default/v1/keys` | | Keycloak | `https://YOUR_HOST/realms/REALM/protocol/openid-connect/certs` |
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # React with Firebase Source: https://docs.chipipay.com/sdk/react/gasless-firebase-setup Learn how to integrate Chipi Pay with your React application using Firebase Auth for authentication and secure wallet storage. 1. First, in order to create authentication with Firebase you will need two things: first, a [Firebase account](https://firebase.google.com/) and second, a [React](https://es.react.dev/learn/installation) project. Once you have your account you must: * Go to your [console](https://console.firebase.google.com) and create a new Firebase project. * Once you have a project, proceed to the `Authentication` section. * Click on the `Sign in method` tab and enable the `Email/password` sign-in method and click `save`. * Go to `project settings`, scroll and under your apps section click on the `web app` usually displayed with a `` icon. * Register your app and click save. 2. Install the npm package in your project: ```bash theme={null} npm install firebase ``` On the Firebase Register web app page, you will also be provided something like this. Save it for later. ```typescript theme={null} // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: "apikey", authDomain: "domain", projectId: "your-id", storageBucket: "storage-bucket", messagingSenderId: "000000", appId: "app-id" }; // Initialize Firebase const app = initializeApp(firebaseConfig); ``` 1. Create a file named `firebase.ts` with the following configuration: ```typescript theme={null} // src/firebase.ts import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, appId: import.meta.env.VITE_FIREBASE_APP_ID, }; export const firebaseApp = initializeApp(firebaseConfig); export const auth = getAuth(firebaseApp); ``` Note:
❗ React apps don’t have environment variable safety – use VITE\_ prefix (for Vite) or .env.local for created local React app.
For more information about Vite environment variables, see the Vite documentation.
and for more on Create React App's environment variables, see the React environment variables docs.
2. Update your environment variables: From the firebaseConfig JSON object provided by Firebase when initializing your project, set up your environment variables. ```typescript theme={null} const firebaseConfig = { apiKey: "apikey", authDomain: "domain", projectId: "your-id", storageBucket: "storage-bucket", messagingSenderId: "000000", appId: "app-id" }; ``` ```typescript theme={null} VITE_FIREBASE_API_KEY=apikey VITE_FIREBASE_AUTH_DOMAIN=domain VITE_FIREBASE_PROJECT_ID=your-id VITE_FIREBASE_STORAGE_BUCKET=storage-bucket VITE_FIREBASE_MESSAGING_SENDER_ID=000000 VITE_FIREBASE_APP_ID=app-id ```
Install the required packages: ```bash theme={null} # Install Chipi Pay SDK npm install @chipi-stack/chipi-react ``` 1. In your `.env` file, add your Chipi **publishable** API key: ```bash theme={null} VITE_CHIPI_API_KEY=your_chipi_public_key ``` **Never put a Chipi secret key in a Vite/React client app.** Any environment variable prefixed with `VITE_` is bundled into the browser JavaScript and exposed via `import.meta.env`, which means anyone inspecting your shipped app can read it. The `@chipi-stack/chipi-react` SDK uses only `apiPublicKey` — the type `ChipiBrowserSDKConfig` explicitly sets `apiSecretKey?: never` (see `types/src/core.ts`). Secret keys belong on your backend only. 2. Set up the Chipi Pay provider in your application: ```tsx theme={null} // src/main.tsx import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { ChipiProvider } from "@chipi-stack/chipi-react"; const API_PUBLIC_KEY = import.meta.env.VITE_CHIPI_API_KEY; if (!API_PUBLIC_KEY) throw new Error("VITE_CHIPI_API_KEY is not set"); ReactDOM.createRoot(document.getElementById("root")!).render( ); ``` Visit the [dashboard JWKS and JWT configuration](https://dashboard.chipipay.com/configure/jwks-endpoint), select `Firebase` as your auth provider, and paste this under the JWKS Endpoint URL. ```bash theme={null} https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys ``` Visit the [Firebase Authentication guide](https://firebase.google.com/docs/auth/web/password-auth) to learn how to use `createUserWithEmailAndPassword` and create your sign up and login! Below you will see a simple example of how you can get and pass your tokens to the different Chipi hooks: ```typescript theme={null} // src/App.tsx import { useEffect, useState } from "react"; import { useGetWallet } from "@chipi-stack/chipi-react"; import { onAuthStateChanged } from "firebase/auth"; import { auth } from "./firebase"; export default function App() { const [userId, setUserId] = useState(null); useEffect(() => { return onAuthStateChanged(auth, (user) => { setUserId(user ? user.uid : null); }); }, []); // Get Firebase token const getToken = async () => { const user = auth.currentUser; if (!user) throw new Error("User not authenticated"); return user.getIdToken(); }; const { data: wallet, isLoading } = useGetWallet({ getBearerToken: getToken, params: { externalUserId: userId || "", }, }); return (

Chipi + Firebase + React

{isLoading &&

Loading wallet...

} {wallet &&
{JSON.stringify(wallet, null, 2)}
}
); } ``` Note: If you have not yet created a wallet, you may see an error with a message like: message:"Wallet not found for externalUserId: user\_id, apiPublicKey: pk\_prod\_something" This error is expected and shows that your app is correctly contacting Chipi, but there is not yet a wallet for this user. Once you create a wallet, this error will disappear.
That's it! You should now have an initial working version of Chipi Pay integrated into your application. You can now start implementing various features like: * Wallet Creations * Sending tokens * Signing transactions * and more!
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # Quickstart Source: https://docs.chipipay.com/sdk/react/gasless-quickstart Quick setup guide for gasless transactions with Chipi SDK in React applications Gasless transactions are only available in Starknet Mainnet. You'll need to [add the JWKS Endpoint](https://dashboard.chipipay.com/configure/jwks-endpoint) from your auth provider to authenticate gasless transactions. With this JWKS Endpoint setup, you can now run secure code in your frontend using a rotating JWT token. ### Getting your JWKS Endpoint For these common auth providers, you can get your JWKS Endpoint like this: 1. Go to your [API Keys](https://dashboard.clerk.com/last-active?path=api-keys) 2. Copy your **Jwks Url** and paste it into the JWKS Endpoint field in your Chipi Dashboard. See our tutorial on how to set up Chipi with [Firebase](/sdk/react/gasless-firebase-setup). Now go to your [Api Keys](https://dashboard.chipipay.com/configure/api-keys) and click on your project. Which will give you access to your **Public Key** (`pk_prod_xxxx`). Keep this handy as you'll need it to initialize the SDK. You won't need the Secret Key (`sk_prod_xxxx`) for now. ```bash npm theme={null} npm install @chipi-stack/chipi-react ``` ```bash yarn theme={null} yarn add @chipi-stack/chipi-react ``` ```bash pnpm theme={null} pnpm add @chipi-stack/chipi-react ``` 1. Add your **Public Key** to your environment variables. ```bash theme={null} # Vite uses VITE_ prefix for environment variables VITE_CHIPI_API_KEY=pk_prod_xxxx ``` 2. In your main `App.tsx` or `index.tsx`, wrap your app with the `ChipiProvider` component as shown below: ```tsx theme={null} // App.tsx import { ChipiProvider } from "@chipi-stack/chipi-react"; const API_PUBLIC_KEY = process.env.VITE_CHIPI_API_KEY; if (!API_PUBLIC_KEY) throw new Error("API_PUBLIC_KEY is not set"); export default function App() { // ... return ( ; ); } ``` You're ready to use the SDK! We recommend starting with our **convenience hooks** that simplify common operations: ### useChipiWallet - Unified Wallet Management ```tsx theme={null} import { useChipiWallet } from "@chipi-stack/chipi-react"; function WalletComponent() { const { userId, getToken } = useYourAuthProvider(); const { wallet, hasWallet, formattedBalance, createWallet, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); if (!hasWallet) { return ( ); } return

Balance: ${formattedBalance} USDC

; } ``` Learn about all the options and return values ### useChipiSession - Gasless Session Keys (CHIPI wallets only) ```tsx theme={null} import { useChipiWallet, useChipiSession } from "@chipi-stack/chipi-react"; function GaslessTransactions() { const { wallet } = useChipiWallet({ ... }); const { hasActiveSession, createSession, registerSession, executeWithSession, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Setup session once const handleSetup = async () => { await createSession(); await registerSession(); }; // Execute transactions without owner signature const handleTransfer = async () => { await executeWithSession([{ contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }]); }; } ``` Learn about session key management
That's it! You should now have an initial working version of Chipi Pay integrated into your application. Explore more features: * [All Available Hooks](/sdk/react/hooks/use-create-wallet) - Complete hook reference * [Session Keys](/sdk/react/hooks/use-chipi-session) - Enable gasless UX * [Buying Services](/sdk/react/buying-services) - Accept crypto payments Need help? Join our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) to ask us anything on your mind.
# React with Supabase Source: https://docs.chipipay.com/sdk/react/gasless-supabase-setup Learn how to integrate Chipi Pay with your React application using Supabase for authentication and secure wallet storage. First, follow the complete [Supabase React Quickstart Guide](https://supabase.com/docs/guides/getting-started/quickstarts/reactjs) to set up your Supabase project and React integration. This will guide you through: * Creating a Supabase project * Setting up your React app with Supabase * Configuring environment variables * Creating a Supabase client **Come back here once you've completed the Supabase setup!** For Chipi Pay to verify your users' JWT tokens, your Supabase project must use asymmetric JWT signing. 1. Go to your Supabase project dashboard 2. Navigate to Settings > API 3. Under "JWT Settings", enable "Use asymmetric JWT signing" 4. Follow the migration steps provided in the dashboard See the [Supabase JWT Signing Keys blog post](https://supabase.com/blog/jwt-signing-keys) for details. Install the Chipi Pay SDK for React: ```bash theme={null} npm install @chipi-stack/chipi-react ``` Add your API keys to your `.env` file: ```bash theme={null} # Your existing Supabase variables VITE_SUPABASE_URL=your_supabase_project_url VITE_SUPABASE_ANON_KEY=your_supabase_anon_key # Add the Chipi Variables VITE_CHIPI_API_KEY=your_chipi_api_public_key ``` You can get your Chipi API keys from the [Chipi Dashboard](https://dashboard.chipipay.com/configure/api-keys). Set up the Chipi Pay provider in your application: ```tsx theme={null} // App.tsx import { ChipiProvider } from "@chipi-stack/chipi-react"; export default function App() { return ( {/* Your app components */} ); } ``` Create a wallet using Supabase authentication: ```typescript theme={null} // components/CreateWallet.tsx import { useState } from "react"; import { supabase } from "../lib/supabase"; import { useCreateWallet } from "@chipi-stack/chipi-react"; export default function CreateWallet() { const { createWalletAsync, isLoading } = useCreateWallet(); const [encryptKey, setEncryptKey] = useState(""); const handleCreateWallet = async () => { const { data: { session } } = await supabase.auth.getSession(); const bearerToken = session?.access_token; const userId = session?.user?.id; if (!bearerToken || !userId) { console.error("Please sign in first"); return; } const response = await createWalletAsync({ params: { encryptKey, externalUserId: userId, }, bearerToken, }); console.log("Wallet created:", response.publicKey); }; return (
setEncryptKey(e.target.value)} />
); } ```
Register your Supabase JWKS endpoint in the [Chipi Dashboard](https://dashboard.chipipay.com/configure/jwks-endpoint): 1. Go to Configure > Auth Provider 2. Select **Supabase** as your provider 3. Enter your Supabase project URL 4. Click **Verify & Save**
Need help? Check out our [Telegram Community](https://t.me/+e2qjHEOwImkyZDVh) for support and to connect with other developers. # useAddSessionKeyToContract Source: https://docs.chipipay.com/sdk/react/hooks/use-add-session-key-to-contract Register a session key on the CHIPI wallet smart contract. This one-time operation requires owner signature and enables the session key to execute transactions. Session keys only work with **CHIPI wallets**. This hook requires the owner's `encryptKey` (PIN) to sign the registration transaction. ## Usage ```typescript theme={null} const { addSessionKeyToContract, addSessionKeyToContractAsync, data, isLoading, error, isSuccess, reset } = useAddSessionKeyToContract(); ``` ### Parameters | Parameter | Type | Required | Description | | ---------------------- | ------------- | -------- | ------------------------------------------------------------------- | | `params.encryptKey` | string | Yes | Owner's PIN to decrypt wallet private key for signing | | `params.wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `params.sessionConfig` | SessionConfig | Yes | Session configuration (see below) | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### SessionConfig Structure ```typescript theme={null} { sessionPublicKey: string; // From useCreateSessionKey().data.publicKey validUntil: number; // Unix timestamp (seconds) maxCalls: number; // Maximum transactions allowed allowedEntrypoints: string[]; // Whitelisted function selectors (empty = all allowed) } ``` ### Return Value | Property | Type | Description | | ------------------------------ | ------------- | --------------------------------------------------- | | `addSessionKeyToContract` | function | Trigger registration (fire-and-forget) | | `addSessionKeyToContractAsync` | function | Trigger registration (returns Promise with tx hash) | | `data` | string | Transaction hash of registration | | `isLoading` | boolean | Whether registration is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether registration succeeded | | `reset` | function | Reset mutation state | ## Example Implementation ```typescript theme={null} import { useAddSessionKeyToContract, useCreateSessionKey, useGetWallet } from "@chipi-stack/chipi-react"; export function RegisterSession() { const { createSessionKeyAsync } = useCreateSessionKey(); const { addSessionKeyToContractAsync, isLoading, error } = useAddSessionKeyToContract(); const { data: wallet } = useGetWallet({ params: { externalUserId: "user-123" }, getBearerToken }); const [pin, setPin] = useState(''); const handleRegisterSession = async () => { const bearerToken = await getBearerToken(); // Step 1: Create session key const session = await createSessionKeyAsync({ encryptKey: pin, durationSeconds: 3600, }); // Step 2: Register on contract const txHash = await addSessionKeyToContractAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 100, // Allow 100 transactions allowedEntrypoints: [], // Empty = all functions allowed }, }, bearerToken, }); // Store session for later use localStorage.setItem('chipiSession', JSON.stringify(session)); console.log('Session registered:', txHash); }; return (

Register Session Key

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded mb-4" /> {error && (
Error: {error.message}
)}
); } ``` ## Restricting Session Permissions For enhanced security, whitelist specific function selectors: ```typescript theme={null} const txHash = await addSessionKeyToContractAsync({ params: { encryptKey: pin, wallet: walletData, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 10, // Only 10 calls allowed allowedEntrypoints: [ "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve ], }, }, bearerToken, }); ``` Limiting `allowedEntrypoints` restricts what functions the session can call. This is recommended for production to minimize attack surface. ## Error Handling Common errors: | Error | Cause | Solution | | ---------------------------------------- | -------------------------- | ---------------------------------------------------------- | | `Session keys require CHIPI wallet type` | Wallet is READY type | Create a new wallet with `walletType: "CHIPI"` | | `Invalid encryptKey` | Wrong PIN entered | Verify the PIN matches the one used during wallet creation | | `Session already exists` | Session already registered | Use `useGetSessionData` to check status | Registration requires a blockchain transaction. The transaction is gas-sponsored but may take 10-30 seconds to confirm. # useApprove Source: https://docs.chipipay.com/sdk/react/hooks/use-approve Grants permission to a smart contract to spend tokens from the user wallet. Required before staking or other delegated actions. ## Usage ```typescript theme={null} const { approve, approveAsync, data, isLoading, isError, error, isSuccess, reset } = useApprove(); ``` ### Parameters The hook accepts an object with: * `params` (`ApproveHookInput`): * `encryptKey` (string): User's decryption PIN * `wallet` (`WalletData`): Wallet public/private key pair * `contractAddress` (string): Token contract to approve * `spender` (string): Contract address to authorize * `amount` (number): Maximum spend allowance (will be converted to string internally) * `decimals` (number, optional): Token decimals (default: 18) * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `approve`: Function to trigger token approval (fire-and-forget) * `approveAsync`: Promise-based function that resolves with the transaction hash * `data`: Transaction hash of the approval (string | undefined) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the approval completed successfully * `reset`: Function to reset the mutation state ## Example Implementation for Approving VESU ```typescript theme={null} const VESU_CONTRACT = "0x037ae3f583c8d644b7556c93a04b83b52fa96159b2b0cbd83c14d3122aef80a2"; // Sample wallet data - replace with your wallet in production. // Keep walletType + signerKind from createWallet / useGetWallet: the SDK // routes by walletType, and omitting it defaults to "READY" (a SHHH wallet // would revert). const sampleWallet: { publicKey: string; encryptedPrivateKey: string; walletType: "SHHH" | "CHIPI" | "READY"; signerKind?: "STARK" | "WEBAUTHN_P256" | "EIP191_SECP256K1" | "ED25519"; } = { publicKey: "0x123...yourPublicKeyHere", encryptedPrivateKey: "encrypted:key:data", // This would be encrypted data in production walletType: "SHHH", signerKind: "STARK", }; export function ApproveForm() { const { approveAsync, data, isLoading, isError, error } = useApprove(); const [wallet] = useState(sampleWallet); const [form, setForm] = useState({ pin: '', amount: '' }); const handleApprove = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken) { console.error('No bearer token available'); return; } try { await approveAsync({ params: { encryptKey: form.pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: wallet.walletType, // routing-critical (SHHH/CHIPI/READY) signerKind: wallet.signerKind, }, contractAddress: VESU_CONTRACT, spender: VESU_CONTRACT, // Authorizing VESU contract amount: Number(form.amount), decimals: 18 }, bearerToken, }); } catch (err) { console.error('Approval failed:', err); } }; return (

Approve Token Access

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, amount: e.target.value})} className="w-full p-2 border rounded-md" required />
{data && (

Approval TX: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always use encrypted private keys * Never store or transmit raw private keys * Implement proper PIN validation * Use secure storage for wallet data * Consider implementing approval limits ## Error Handling * Handle insufficient token balance * Validate contract addresses * Check for existing approvals * Monitor gas fees * Implement retry logic for failed transactions Approvals are specific to each token-contract pair. You'll need to re-approve when interacting with new contracts. ## Related Hooks * **useTransfer** - For moving tokens * **useCallAnyContract** - For custom contract interactions that need prior approval # useCallAnyContract Source: https://docs.chipipay.com/sdk/react/hooks/use-call-any-contract Executes any StarkNet contract method. Handles all contract interactions not covered by specific hooks. ## Usage ```typescript theme={null} const { callAnyContract, callAnyContractAsync, data, isLoading, isError, error, isSuccess, reset } = useCallAnyContract(); ``` ### Parameters The hook accepts an object with: * `params` (`CallAnyContractParams`): * `encryptKey` (string): User's decryption PIN (or passkey-derived key) * `wallet` (`WalletData`): the full wallet object — `publicKey`, `encryptedPrivateKey`, and (routing-critical) **`walletType`** + **`signerKind`** from `createWallet` / `useGetWallet`. Omitting `walletType` defaults to `"READY"` and reverts on a SHHH wallet. * `contractAddress` (string): Target contract address * `calls` (`Call[]`): Array of contract calls with `contractAddress`, `entrypoint`, and `calldata` * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `callAnyContract`: Function to trigger contract call (fire-and-forget) * `callAnyContractAsync`: Promise-based function that resolves with the transaction hash * `data`: Transaction hash of the call (string | undefined) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the call completed successfully * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} export function GenericContractForm() { const { callAnyContractAsync, data, isLoading, isError, error } = useCallAnyContract(); const [form, setForm] = useState({ pin: '', contractAddress: '', entrypoint: '', calldata: '' }); const handleCall = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getBearerToken(); try { await callAnyContractAsync({ params: { encryptKey: form.pin, wallet: walletData, contractAddress: form.contractAddress, calls: [ { contractAddress: form.contractAddress, entrypoint: form.entrypoint, calldata: JSON.parse(form.calldata || '[]') } ], }, bearerToken, }); } catch (err) { console.error('Contract call failed:', err); } }; return (

Contract Interaction

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, contractAddress: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, entrypoint: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, calldata: e.target.value})} className="w-full p-2 border rounded-md" placeholder='Example: [123, "0xabc...", true]' />
{data && (

TX Hash: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Verify contract addresses * Validate calldata format * Use encrypted private keys * Implement proper PIN validation * Review contract ABI before calling ## Error Handling * Handle invalid contract addresses * Validate calldata format * Monitor gas fees * Implement retry logic for failed calls Use with caution. Direct contract calls require deep understanding of the target contract's ABI and security implications. ## Related Hooks * **useApprove** - For token approvals * **useTransfer** - For token transfers # useChipiSession Source: https://docs.chipipay.com/sdk/react/hooks/use-chipi-session Unified session key management for gasless UX - create, register, execute, and revoke sessions with a single hook. ## Overview `useChipiSession` provides a unified API for managing session keys, enabling gasless transactions without requiring the owner's signature for each operation. This hook combines all session-related operations: * **Session creation** (local keypair generation) * **Session registration** (on-chain) * **Transaction execution** (using session key) * **Session revocation** (on-chain) * **Status checking** (remaining calls, expiration) Session keys only work with **CHIPI wallets**. READY wallets do not support session keys. ## Prerequisites Before using `useChipiSession`, you need: 1. A **CHIPI wallet** (check `wallet.walletType === "CHIPI"` or `wallet.supportsSessionKeys`) 2. The user's **encryption key (PIN)** 3. An **authentication token** (from Clerk, Firebase, etc.) ## Session Lifecycle | State | Description | | --------- | ---------------------------------------------------- | | `none` | No session exists | | `created` | Session created locally, not yet registered on-chain | | `active` | Session registered and usable | | `expired` | Session has expired (time or call limit) | | `revoked` | Session has been revoked on-chain | ## Quick Start ```tsx theme={null} import { useChipiWallet, useChipiSession } from "@chipi-stack/chipi-react"; function SessionComponent() { const { userId, getToken } = useYourAuthProvider(); const [pin, setPin] = useState(""); const { wallet } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); const { session, sessionState, hasActiveSession, remainingCalls, createSession, registerSession, executeWithSession, isCreating, isRegistering, isExecuting, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, }); // Setup session (create + register) const handleSetup = async () => { await createSession(); await registerSession(); }; // Execute a transfer using the session const handleTransfer = async () => { await executeWithSession([{ contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb", entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }]); }; return (

Session: {sessionState}

{hasActiveSession &&

Calls remaining: {remainingCalls}

} {!hasActiveSession ? ( ) : ( )}
); } ``` ## Configuration Options ```typescript theme={null} interface UseChipiSessionConfig { // Required wallet: SessionWallet | null | undefined; encryptKey: string; getBearerToken: () => Promise; // Optional storedSession?: SessionKeyData | null; onSessionCreated?: (session: SessionKeyData) => void | Promise; defaultDurationSeconds?: number; // Default: 21600 (6 hours) defaultMaxCalls?: number; // Default: 1000 autoCheckStatus?: boolean; // Default: true } ``` | Option | Type | Default | Description | | ------------------------ | ----------------------- | -------- | -------------------------------- | | `wallet` | `SessionWallet` | Required | Wallet data (must be CHIPI type) | | `encryptKey` | `string` | Required | User's PIN for signing | | `getBearerToken` | `() => Promise` | Required | Auth token function | | `storedSession` | `SessionKeyData` | `null` | Previously stored session | | `onSessionCreated` | `(session) => void` | - | Callback to persist new session | | `defaultDurationSeconds` | `number` | `21600` | Session duration (6 hours) | | `defaultMaxCalls` | `number` | `1000` | Max calls per session | | `autoCheckStatus` | `boolean` | `true` | Auto-fetch on-chain status | ## Return Values ### Session Data | Property | Type | Description | | ------------------ | ------------------------ | -------------------------------- | | `session` | `SessionKeyData \| null` | Current session data | | `sessionStatus` | `SessionDataResponse` | On-chain session status | | `sessionState` | `SessionState` | Lifecycle state | | `hasActiveSession` | `boolean` | Whether session is usable | | `isSessionExpired` | `boolean` | Whether session has expired | | `remainingCalls` | `number \| undefined` | Calls left in session | | `supportsSession` | `boolean` | Whether wallet supports sessions | ### Actions | Method | Returns | Description | | --------------------------- | ------------------------- | ---------------------------------- | | `createSession(config?)` | `Promise` | Create new session locally | | `registerSession(config?)` | `Promise` | Register on-chain (returns txHash) | | `revokeSession()` | `Promise` | Revoke on-chain (returns txHash) | | `executeWithSession(calls)` | `Promise` | Execute calls (returns txHash) | | `clearSession()` | `void` | Clear local session state | | `refetchStatus()` | `Promise` | Refresh on-chain status | ### Loading States | Property | Description | | ----------------- | ---------------------------- | | `isCreating` | Creating session locally | | `isRegistering` | Registering session on-chain | | `isRevoking` | Revoking session on-chain | | `isExecuting` | Executing transaction | | `isLoadingStatus` | Fetching on-chain status | ## Persisting Sessions Sessions should be persisted between page loads. Here's an example using local state: ```tsx theme={null} import { useState, useEffect } from "react"; import { useChipiWallet, useChipiSession } from "@chipi-stack/chipi-react"; function PersistentSession() { const { wallet } = useChipiWallet({ ... }); // Load session from storage const [storedSession, setStoredSession] = useState(() => { const saved = localStorage.getItem("chipi-session"); return saved ? JSON.parse(saved) : null; }); const { session, hasActiveSession, createSession, registerSession, } = useChipiSession({ wallet, encryptKey: pin, getBearerToken: getToken, storedSession, onSessionCreated: (newSession) => { // Persist to localStorage localStorage.setItem("chipi-session", JSON.stringify(newSession)); setStoredSession(newSession); }, }); // Session is automatically restored on mount! } ``` ## Executing Transactions The `executeWithSession` method accepts an array of Starknet calls: ```typescript theme={null} // Single transfer await executeWithSession([{ contractAddress: USDC_CONTRACT, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }]); // Batch multiple calls await executeWithSession([ { contractAddress: USDC_CONTRACT, entrypoint: "approve", calldata: [spender, amount, "0x0"], }, { contractAddress: DEX_CONTRACT, entrypoint: "swap", calldata: [...], }, ]); ``` ## Custom Session Configuration ### Custom Duration ```typescript theme={null} // Create session valid for 1 hour await createSession({ durationSeconds: 3600 }); ``` ### Custom Max Calls ```typescript theme={null} // Register with 500 max calls await registerSession({ maxCalls: 500 }); ``` ## Error Handling ```tsx theme={null} const { error, session } = useChipiSession({...}); // Check for errors if (error) { console.error("Session error:", error.message); } // Handle specific errors const handleSetup = async () => { try { await createSession(); await registerSession(); } catch (err) { if (err.message.includes("does not support sessions")) { alert("Please use a CHIPI wallet for sessions"); } else { alert(`Error: ${err.message}`); } } }; ``` # useChipiWallet Source: https://docs.chipipay.com/sdk/react/hooks/use-chipi-wallet All-in-one hook for wallet management - combines wallet fetching, creation, and balance checking into a single unified API. ## Overview `useChipiWallet` is a convenience hook that simplifies wallet management by combining multiple operations into one: * **Wallet fetching** (replaces `useGetWallet`) * **Wallet creation** (replaces `useCreateWallet`) * **Balance checking** (replaces `useGetTokenBalance`) Use this hook when you want a streamlined API. Use the individual hooks (`useGetWallet`, `useCreateWallet`, `useGetTokenBalance`) when you need more granular control. ## Quick Start ```typescript theme={null} import { useChipiWallet } from "@chipi-stack/chipi-react"; function WalletComponent() { const { userId, getToken } = useYourAuthProvider(); // Clerk, Firebase, etc. const { wallet, hasWallet, balance, formattedBalance, createWallet, isCreating, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); if (isLoadingWallet) return
Loading...
; if (!hasWallet) { return ( ); } return (

Address: {wallet?.shortAddress}

Balance: ${formattedBalance} USDC

); } ``` ## Configuration Options ```typescript theme={null} interface UseChipiWalletConfig { // Required externalUserId: string | null | undefined; getBearerToken: () => Promise; // Optional defaultToken?: ChainToken; // Default: "USDC" enabled?: boolean; // Default: true } ``` | Option | Type | Default | Description | | ---------------- | ----------------------- | -------- | -------------------------------- | | `externalUserId` | `string \| null` | Required | User ID from your auth provider | | `getBearerToken` | `() => Promise` | Required | Function to get the auth token | | `defaultToken` | `ChainToken` | `"USDC"` | Token to fetch balance for | | `enabled` | `boolean` | `true` | Whether to fetch wallet on mount | ## Return Values ### Wallet Data | Property | Type | Description | | ----------------- | -------------------------------------- | ------------------------------------ | | `wallet` | `ChipiWalletData \| null \| undefined` | Wallet data with computed properties | | `hasWallet` | `boolean` | Whether the user has a wallet | | `isLoadingWallet` | `boolean` | Wallet fetch loading state | | `walletError` | `Error \| null` | Any error from fetching | ### Balance Data | Property | Type | Description | | ------------------ | -------------------------------------- | ------------------------------------ | | `balance` | `GetTokenBalanceResponse \| undefined` | Raw balance data | | `formattedBalance` | `string` | Formatted balance (e.g., "1,234.56") | | `isLoadingBalance` | `boolean` | Balance fetch loading state | ### Create Wallet | Property | Type | Description | | --------------- | ------------------------------------------- | ------------------------ | | `createWallet` | `(params) => Promise` | Create a new wallet | | `isCreating` | `boolean` | Creation loading state | | `createdWallet` | `CreateWalletResponse \| undefined` | Last created wallet data | ### Actions | Method | Description | | ------------------ | ------------------------------- | | `refetchWallet()` | Refetch wallet data | | `refetchBalance()` | Refetch balance data | | `refetchAll()` | Refetch both wallet and balance | ## Computed Wallet Properties The `wallet` object includes these computed properties: ```typescript theme={null} interface ChipiWalletData extends GetWalletResponse { supportsSessionKeys: boolean; // true for CHIPI wallets shortAddress: string; // Truncated address for display } ``` ## Example with Clerk Authentication ```tsx theme={null} import { useState } from "react"; import { useAuth, SignInButton, SignedIn, SignedOut } from "@clerk/clerk-react"; import { useChipiWallet } from "@chipi-stack/chipi-react"; export function WalletDashboard() { const { userId, getToken } = useAuth(); const [pin, setPin] = useState(""); const { wallet, hasWallet, formattedBalance, createWallet, isCreating, isLoadingWallet, refetchAll, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); const handleCreateWallet = async () => { if (!pin || pin.length < 4) { alert("Please enter a 4-digit PIN"); return; } try { const result = await createWallet({ encryptKey: pin, walletType: "CHIPI", }); alert(`Wallet created! Address: ${result.publicKey}`); } catch (err) { console.error(err); alert("Failed to create wallet"); } }; return (
{isLoadingWallet ? (

Loading wallet...

) : hasWallet ? (

Address: {wallet?.shortAddress}

${formattedBalance} USDC

Sessions: {wallet?.supportsSessionKeys ? "✓" : "✗"}

) : (
setPin(e.target.value)} maxLength={4} />
)}
); } ``` ## Wallet Types | Type | Session Keys | Description | | ------- | ------------ | ---------------------------------------------------- | | `CHIPI` | ✅ Yes | OpenZeppelin account with SNIP-9 session key support | | `READY` | ❌ No | Argent X compatible wallet | ```typescript theme={null} // Create a CHIPI wallet (supports session keys) await createWallet({ encryptKey: pin, walletType: "CHIPI" }); // Create a READY wallet (no session keys) await createWallet({ encryptKey: pin, walletType: "READY" }); ``` ## Migration from Individual Hooks ### Before ```typescript theme={null} const { data: wallet, isLoading } = useGetWallet({ params: { externalUserId: userId }, getBearerToken: getToken, enabled: !!userId, }); const { createWalletAsync } = useCreateWallet(); const { data: balance } = useGetTokenBalance({ params: { walletPublicKey: wallet?.publicKey, ... }, getBearerToken: getToken, enabled: !!wallet?.publicKey, }); ``` ### After ```typescript theme={null} const { wallet, hasWallet, balance, formattedBalance, createWallet, isLoadingWallet, } = useChipiWallet({ externalUserId: userId, getBearerToken: getToken, }); ``` `useChipiWallet` automatically handles the dependency chain - it only fetches balance after the wallet is loaded. # useCreateSessionKey Source: https://docs.chipipay.com/sdk/react/hooks/use-create-session-key Generate a session keypair locally for temporary, delegated transaction execution. Session keys enable gasless, frictionless UX without requiring the owner private key for each transaction. Session keys only work with **CHIPI wallets** - not READY wallets. Make sure your wallet was created with `walletType: "CHIPI"`. ## Usage ```typescript theme={null} const { createSessionKey, createSessionKeyAsync, data, isLoading, error, isSuccess, reset } = useCreateSessionKey(); ``` ### Parameters | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------ | | `encryptKey` | string | Yes | User's PIN/password to encrypt the session private key | | `durationSeconds` | number | No | Session duration in seconds (default: 21600 = 6 hours) | ### Return Value Returns an object containing: | Property | Type | Description | | ----------------------- | -------------- | ------------------------------------------------ | | `createSessionKey` | function | Trigger session key generation (fire-and-forget) | | `createSessionKeyAsync` | function | Trigger session key generation (returns Promise) | | `data` | SessionKeyData | Generated session key data | | `isLoading` | boolean | Whether generation is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether generation succeeded | | `reset` | function | Reset mutation state | ### SessionKeyData Structure ```typescript theme={null} { publicKey: string; // Session public key (hex) encryptedPrivateKey: string; // AES encrypted with encryptKey validUntil: number; // Unix timestamp (seconds) } ``` ## Example Implementation ```typescript theme={null} import { useCreateSessionKey } from "@chipi-stack/chipi-react"; export function CreateSession() { const { createSessionKeyAsync, data: session, isLoading, error } = useCreateSessionKey(); const [pin, setPin] = useState(''); const handleCreateSession = async () => { try { const sessionData = await createSessionKeyAsync({ encryptKey: pin, durationSeconds: 3600, // 1 hour session }); // Store session in client-side storage (NOT your database!) localStorage.setItem('chipiSession', JSON.stringify(sessionData)); console.log('Session created:', sessionData.publicKey); } catch (err) { console.error('Failed to create session:', err); } }; return (

Create Session Key

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded" />
{error && (
Error: {error.message}
)} {session && (

Session created! Expires: {new Date(session.validUntil * 1000).toLocaleString()}

)}
); } ``` ## Security Considerations **Never store session keys in your backend database.** This defeats the purpose of self-custodial security. Store only in client-side secure storage. | Platform | Recommended Storage | | ---------------- | ---------------------------------------------- | | **Web** | `localStorage` or `sessionStorage` | | **iOS** | Secure Enclave / Keychain | | **Android** | Android Keystore | | **React Native** | `expo-secure-store` or `react-native-keychain` | ## Best Practices 1. **Set short durations** - Use 1-6 hours, not days 2. **Clear on logout** - Always remove session from storage when user logs out 3. **Register on-chain** - After creating, use `useAddSessionKeyToContract` to register it This hook only generates the session keypair locally. You must call `useAddSessionKeyToContract` to register it on-chain before it can be used for transactions. # useCreateWallet Source: https://docs.chipipay.com/sdk/react/hooks/use-create-wallet Creates a new Argent-compatible wallet on StarkNet. This hook deploys the wallet contract behind the scenes and uses Avnus gasless to sponsor gas fees, resulting in a frictionless onboarding experience. ## Usage ```typescript theme={null} const { createWalletAsync, data, isLoading, error } = useCreateWallet(); ``` ### Parameters The mutation accepts an object with: * `params` (`CreateWalletParams`): * `encryptKey` (string): PIN or passkey-derived key used to encrypt the private key * `externalUserId` (string): Your application's unique identifier for the user * `chain` (`Chain`): The blockchain network. Use `Chain.STARKNET` * `walletType` (`"SHHH" | "CHIPI" | "READY"`, optional): **defaults to `"SHHH"`** (the pluggable-signer ShhhAccount — guardian recovery, multisig). `"CHIPI"` is the legacy account that supports session keys; `"READY"` is Argent X compatible. Whatever you create here, persist `walletType` (and `signerKind`) and pass them back on every transfer/approve/call. * `usePasskey` (boolean, optional): Set `true` when `encryptKey` was derived from a passkey (via `@chipi-stack/chipi-passkey`) * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `createWallet`: Function to trigger wallet creation (fire-and-forget) * `createWalletAsync`: Promise-based function that resolves with the wallet data * `data`: The created wallet (`CreateWalletResponse`) — flat object with `publicKey`, `encryptedPrivateKey`, `walletType`, `chain`, etc. * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if wallet creation succeeded * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useCreateWallet, Chain } from "@chipi-stack/nextjs"; export function CreateWallet() { const { userId, getToken } = useAuth(); const { createWalletAsync, data, isLoading, error } = useCreateWallet(); const [pin, setPin] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken || !userId) { console.error('No bearer token or user ID available'); return; } try { const wallet = await createWalletAsync({ params: { encryptKey: pin, externalUserId: userId, chain: Chain.STARKNET, // Omit walletType to get the default SHHH account, or set it // explicitly. Use "CHIPI" only if you need legacy session keys. walletType: "SHHH", }, bearerToken, }); alert('Wallet created successfully!'); } catch (err) { console.error('Wallet creation failed:', err); } }; return (

Create New Wallet

setPin(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500" required minLength={4} />
{error && (
Error: {error.message}
)} {data && (

Wallet Details

View Contract

Address: {data.publicKey}

)}
); } ``` ## Security Considerations * PINs should always be collected client-side * Never log or store raw PIN values * Use secure encryption before any transmission * Store encrypted private key securely * Associate with user session (not persistent storage) ## Error Handling * Catch and handle RPC connection errors * Monitor gasless API limits * Implement retry logic for failed deployments Wallet creation is free! Gas fees are covered by our gasless integration. # useExecuteWithSession Source: https://docs.chipipay.com/sdk/react/hooks/use-execute-with-session Execute gasless transactions using a session key instead of the owner private key. Enables frictionless UX for gaming, DeFi automation, and more. This hook uses the session key signature (4-element format) instead of the owner signature. The session must be registered on-chain first via `useAddSessionKeyToContract`. ## Usage ```typescript theme={null} const { executeWithSession, executeWithSessionAsync, data, isLoading, error, isSuccess, reset } = useExecuteWithSession(); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------- | -------------- | -------- | --------------------------------------------------------- | | `params.encryptKey` | string | Yes | PIN to decrypt the **session** private key | | `params.wallet` | WalletData | Yes | Wallet object (session executes on behalf of this wallet) | | `params.session` | SessionKeyData | Yes | Session key data from `useCreateSessionKey` | | `params.calls` | Call\[] | Yes | Array of contract calls to execute | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### Call Structure ```typescript theme={null} { contractAddress: string; // Target contract address entrypoint: string; // Function name to call calldata: string[]; // Function arguments } ``` ### Return Value | Property | Type | Description | | ------------------------- | ------------- | ------------------------------------------------ | | `executeWithSession` | function | Trigger execution (fire-and-forget) | | `executeWithSessionAsync` | function | Trigger execution (returns Promise with tx hash) | | `data` | string | Transaction hash | | `isLoading` | boolean | Whether execution is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether execution succeeded | | `reset` | function | Reset mutation state | ## Example: Token Transfer ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/chipi-react"; // Native USDC on Starknet mainnet (not USDC.e bridged) const USDC_ADDRESS = "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb"; export function SessionTransfer() { const { executeWithSessionAsync, isLoading, error, data: txHash } = useExecuteWithSession(); const [amount, setAmount] = useState(''); const [recipient, setRecipient] = useState(''); const handleTransfer = async () => { const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession')!); const pin = await promptForPin(); // Your PIN input method try { const hash = await executeWithSessionAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, session, calls: [{ contractAddress: USDC_ADDRESS, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }], }, bearerToken, }); console.log('Transfer executed:', hash); } catch (err) { console.error('Transfer failed:', err); } }; return (

Transfer with Session

setRecipient(e.target.value)} placeholder="Recipient address (0x...)" className="w-full p-2 border rounded" /> setAmount(e.target.value)} placeholder="Amount (in smallest units)" className="w-full p-2 border rounded" />
{error && (
Error: {error.message}
)} {txHash && ( )}
); } ``` ## Example: Gaming Actions ```typescript theme={null} import { useExecuteWithSession } from "@chipi-stack/chipi-react"; const GAME_CONTRACT = "0x..."; export function GameBoard() { const { executeWithSessionAsync, isLoading } = useExecuteWithSession(); const makeMove = async (moveData: string) => { const session = JSON.parse(localStorage.getItem('chipiSession')!); const bearerToken = await getBearerToken(); // Execute instantly - no wallet popup! await executeWithSessionAsync({ params: { encryptKey: userPin, wallet: { ...wallet, walletType: "CHIPI" }, session, calls: [{ contractAddress: GAME_CONTRACT, entrypoint: "make_move", calldata: [moveData], }], }, bearerToken, }); }; return (
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((cell) => ( ))}
); } ``` ## Multi-Call Transactions Execute multiple calls in a single transaction: ```typescript theme={null} const txHash = await executeWithSessionAsync({ params: { encryptKey: pin, wallet: walletData, session, calls: [ // Approve spending { contractAddress: USDC_ADDRESS, entrypoint: "approve", calldata: [SWAP_CONTRACT, amount, "0x0"], }, // Execute swap { contractAddress: SWAP_CONTRACT, entrypoint: "swap", calldata: [USDC_ADDRESS, ETH_ADDRESS, amount, minReceived], }, ], }, bearerToken, }); ``` ## Error Handling | Error | Cause | Solution | | ----------------------------------------------- | ----------------------------- | -------------------------------------------------- | | `Session execution only supports CHIPI wallets` | Wallet is READY type | Use a CHIPI wallet | | `Session has expired` | `validUntil` timestamp passed | Create and register a new session | | `Session not found` | Not registered on-chain | Call `useAddSessionKeyToContract` first | | `Max calls exceeded` | `remainingCalls` is 0 | Create a new session with higher `maxCalls` | | `Entrypoint not allowed` | Function not in whitelist | Register session with correct `allowedEntrypoints` | Session transactions decrement `remainingCalls` on the contract. Monitor usage with `useGetSessionData` and create new sessions before exhaustion. For the best UX, pre-create sessions during onboarding and store the PIN securely. This allows instant transactions without prompting for PIN each time. # useGetSessionData Source: https://docs.chipipay.com/sdk/react/hooks/use-get-session-data Query session key status from the CHIPI wallet smart contract. Returns whether the session is active, remaining calls, and allowed entrypoints. ## Usage ```typescript theme={null} const { data, isLoading, isError, error, isSuccess, refetch } = useGetSessionData(params, options); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------- | ------- | -------- | ---------------------------------------- | | `params.walletAddress` | string | Yes | Wallet address to query | | `params.sessionPublicKey` | string | Yes | Session public key to query | | `options.enabled` | boolean | No | Enable/disable the query (default: true) | Pass `null` as params to disable the query. ### Return Value | Property | Type | Description | | ----------- | ------------------- | ----------------------------- | | `data` | SessionDataResponse | Session status from contract | | `isLoading` | boolean | Whether query is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether query succeeded | | `refetch` | function | Manually refetch session data | ### SessionDataResponse Structure ```typescript theme={null} { isActive: boolean; // Whether session is currently valid validUntil: number; // Expiration timestamp (Unix seconds) remainingCalls: number; // Transactions remaining before exhausted allowedEntrypoints: string[]; // Whitelisted function selectors } ``` ## Example Implementation ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; export function SessionStatus() { // Get session from local storage const session = JSON.parse(localStorage.getItem('chipiSession') || 'null'); const { data: sessionData, isLoading, refetch } = useGetSessionData( session ? { walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, } : null ); if (!session) { return
No active session
; } if (isLoading) { return
Loading session status...
; } return (

Session Status

Status: {sessionData?.isActive ? "Active" : "Inactive"}
Expires: {sessionData?.validUntil ? new Date(sessionData.validUntil * 1000).toLocaleString() : "N/A"}
Remaining Calls: {sessionData?.remainingCalls ?? 0}
Restrictions: {sessionData?.allowedEntrypoints?.length ? `${sessionData.allowedEntrypoints.length} functions` : "All functions"}
{!sessionData?.isActive && (
Session is inactive. Create a new session to continue.
)}
); } ``` ## Conditional Rendering Based on Session ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; export function GameInterface() { const session = JSON.parse(localStorage.getItem('chipiSession') || 'null'); const { data: sessionData } = useGetSessionData( session ? { walletAddress: wallet.publicKey, sessionPublicKey: session.publicKey, } : null ); // Check if session is valid before allowing actions const canPlay = sessionData?.isActive && sessionData?.remainingCalls > 0; return (
{canPlay ? ( ) : (

Session expired or no calls remaining

)}
); } ``` ## Auto-Refresh Session Status ```typescript theme={null} import { useGetSessionData } from "@chipi-stack/chipi-react"; import { useEffect } from "react"; export function useSessionMonitor(walletAddress: string, sessionPublicKey: string) { const { data, refetch } = useGetSessionData({ walletAddress, sessionPublicKey, }); // Refresh every 30 seconds useEffect(() => { const interval = setInterval(() => { refetch(); }, 30000); return () => clearInterval(interval); }, [refetch]); return { isActive: data?.isActive ?? false, remainingCalls: data?.remainingCalls ?? 0, expiresAt: data?.validUntil ? new Date(data.validUntil * 1000) : null, }; } ``` This is a **read-only** query that doesn't require gas or signatures. It directly calls the smart contract's view function. If the session doesn't exist on-chain, `isActive` will be `false` with `remainingCalls: 0`. This is normal for sessions that were never registered or have been revoked. # useGetTokenBalance Source: https://docs.chipipay.com/sdk/react/hooks/use-get-token-balance Fetches the on-chain token balance for a wallet address. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTokenBalance, } = useGetTokenBalance({ params: { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey: "0x...", }, getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ------------------------ | ----------------------- | -------- | --------------------------------------------------- | | `params.chainToken` | `ChainToken` | Yes | Token identifier. Use `ChainToken.USDC` | | `params.chain` | `Chain` | Yes | Blockchain network. Use `Chain.STARKNET` | | `params.walletPublicKey` | `string` | No | Wallet address to check balance for | | `params.externalUserId` | `string` | No | External user ID (alternative to `walletPublicKey`) | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ------------------- | --------------------------------------------- | --------------------------------------------- | | `data` | `GetTokenBalanceResponse \| undefined` | Balance data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => void` | Re-run the query | | `fetchTokenBalance` | `(input) => Promise` | Imperatively fetch balance with custom params | ## Example Implementation ```typescript theme={null} import { useGetTokenBalance, Chain, ChainToken } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function TokenBalance({ walletPublicKey }: { walletPublicKey: string }) { const { getToken } = useAuth(); const { data, isLoading, error } = useGetTokenBalance({ params: { chainToken: ChainToken.USDC, chain: Chain.STARKNET, walletPublicKey, }, getBearerToken: getToken, }); if (isLoading) return

Loading balance...

; if (error) return

Error: {error.message}

; return (

Balance: {data?.balance} USDC

); } ``` Either `walletPublicKey` or `externalUserId` must be provided to identify the wallet. # useGetTransaction Source: https://docs.chipipay.com/sdk/react/hooks/use-get-transaction Fetches a single transaction by its hash or internal ID. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransaction, } = useGetTransaction({ hashOrId: "0x...", getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ---------------- | ----------------------- | -------- | ------------------------------------------------- | | `hashOrId` | `string` | Yes | Transaction hash (`0x...`) or internal ID | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ------------------ | ------------------------------------ | ------------------------------------- | | `data` | `Transaction \| undefined` | Transaction data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => Promise` | Re-run the query | | `fetchTransaction` | `(input) => Promise` | Imperatively fetch with custom params | ## Example Implementation ```typescript theme={null} import { useGetTransaction } from "@chipi-stack/chipi-react"; export function TransactionDetail({ txHash, getBearerToken, }: { txHash: string; getBearerToken: () => Promise; }) { const { data: tx, isLoading, error } = useGetTransaction({ hashOrId: txHash, getBearerToken, }); if (isLoading) return

Loading transaction...

; if (error) return

Error: {error.message}

; return (

Hash: {tx?.transactionHash}

Status: {tx?.status}

From: {tx?.senderAddress}

To: {tx?.destinationAddress}

Amount: {tx?.amount} {tx?.token}

); } ``` # useGetTransactionList Source: https://docs.chipipay.com/sdk/react/hooks/use-get-transaction-list Fetches a paginated list of transactions with optional date and address filters. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransactionList, } = useGetTransactionList({ query: { page: 1, limit: 10, walletAddress: "0x...", }, getBearerToken: getToken, }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ---------------------- | ----------------------- | -------- | ------------------------------------------------- | | `query.page` | `number` | No | Page number (default: `1`) | | `query.limit` | `number` | No | Results per page (default: `10`) | | `query.walletAddress` | `string` | No | Filter by wallet address | | `query.calledFunction` | `string` | No | Filter by contract function name | | `query.day` | `number` | No | Filter by day (1–31) | | `query.month` | `number` | No | Filter by month (1–12) | | `query.year` | `number` | No | Filter by year | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `queryOptions` | `UseQueryOptions` | No | React Query options (e.g. `staleTime`, `enabled`) | ### Return Value | Property | Type | Description | | ---------------------- | ---------------------------------------------------- | ------------------------------------- | | `data` | `PaginatedResponse \| undefined` | Paginated transaction results | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => void` | Re-run the query | | `fetchTransactionList` | `(input) => Promise>` | Imperatively fetch with custom params | ## Example Implementation ```typescript theme={null} import { useGetTransactionList } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; import { useState } from "react"; export function TransactionList({ walletAddress }: { walletAddress: string }) { const { getToken } = useAuth(); const [page, setPage] = useState(1); const { data, isLoading, error } = useGetTransactionList({ query: { page, limit: 10, walletAddress, }, getBearerToken: getToken, }); if (isLoading) return

Loading transactions...

; if (error) return

Error: {error.message}

; return (
    {data?.data.map((tx) => (
  • {tx.transactionHash} — {tx.status}
  • ))}
Page {page}
); } ``` # useGetTransactionStatus Source: https://docs.chipipay.com/sdk/react/hooks/use-get-transaction-status Fetches and polls the on-chain status of a transaction. ## Usage ```typescript theme={null} const { data, isLoading, isError, isSuccess, error, refetch, fetchTransactionStatus, } = useGetTransactionStatus({ hash: "0x...", getBearerToken: getToken, refetchInterval: 3000, // polls every 3s, stops on terminal status }); ``` ### Input Parameters | Parameter | Type | Required | Description | | ----------------- | ----------------------- | -------- | ------------------------------------------------------------- | | `hash` | `string` | Yes | Transaction hash (`0x...`) | | `getBearerToken` | `() => Promise` | Yes | Function returning the auth token | | `refetchInterval` | `number \| false` | No | Polling interval in ms (default: `3000`). `false` to disable. | | `queryOptions` | `UseQueryOptions` | No | React Query options | ### Return Value | Property | Type | Description | | ------------------------ | ----------------------------------------------- | ---------------------------- | | `data` | `TransactionStatusResponse \| undefined` | Transaction status data | | `isLoading` | `boolean` | True while fetching | | `isError` | `boolean` | True if an error occurred | | `isSuccess` | `boolean` | True if the query succeeded | | `error` | `Error \| null` | Error when `isError` is true | | `refetch` | `() => Promise` | Re-run the query | | `fetchTransactionStatus` | `(input) => Promise` | Imperatively fetch | Polling automatically stops when the status reaches a terminal state: `ACCEPTED_ON_L1`, `REJECTED`, or `REVERTED`. ## Example Implementation ```typescript theme={null} import { useGetTransactionStatus } from "@chipi-stack/chipi-react"; import { OnChainTxStatus } from "@chipi-stack/types"; export function TransactionTracker({ txHash, getBearerToken, }: { txHash: string; getBearerToken: () => Promise; }) { const { data, isLoading } = useGetTransactionStatus({ hash: txHash, getBearerToken, refetchInterval: 3000, }); if (isLoading) return

Checking status...

; const confirmed = data?.status === OnChainTxStatus.ACCEPTED_ON_L2 || data?.status === OnChainTxStatus.ACCEPTED_ON_L1; return (

Status: {data?.status}

{data?.blockNumber &&

Block: {data.blockNumber}

} {data?.revertReason &&

Revert reason: {data.revertReason}

} {confirmed &&

Transaction confirmed!

}
); } ``` # useGetWallet Source: https://docs.chipipay.com/sdk/react/hooks/use-get-wallet Get an authenticated user Wallet ## Usage ```typescript theme={null} const { fetchWallet, data, isLoading, error } = useGetWallet(); ``` ### Parameters * `params` (`GetWalletParams`): * `externalUserId` (string): User ID from your auth provider * `getBearerToken` (`() => Promise`): Function returning the auth token * `queryOptions` (`UseQueryOptions`, optional): React Query options (e.g. `enabled`) ### Return Value Returns an object containing: * `fetchWallet`: Function to manually fetch wallet data with custom params * `data`: The wallet object (`publicKey`, `encryptedPrivateKey`, `walletType`, etc.) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `isSuccess`: Boolean indicating if the query succeeded * `error`: Any error that occurred during the process * `refetch`: Function to re-run the query ## Example Implementation ```typescript theme={null} import { useGetWallet } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function WalletPage(){ const { userId, getToken } = useAuth(); const { data: wallet, isLoading, error, fetchWallet } = useGetWallet({ params: { externalUserId: userId || "", }, getBearerToken: async () => { const token = await getToken(); if (!token) throw new Error("No token found"); return token; }, queryOptions: { enabled: Boolean(userId), }, }); // Or use fetchWallet manually: const loadWallet = async () => { if (!userId) return; try { const token = await getToken(); if (!token) throw new Error("No token found"); const wallet = await fetchWallet({ params: { externalUserId: userId, }, getBearerToken: async () => token, }); console.log("Wallet loaded:", wallet); } catch (error) { console.error("Error loading wallet:", error); } }; return (
{isLoading &&

Loading wallet...

} {error &&

Error: {error.message}

} {wallet && (

Public Key: {wallet.publicKey}

Normalized Public Key: {wallet.normalizedPublicKey}

)}
); } ``` Chipi never stores your decrypted sensitive data. All private keys and authentication tokens remain secure and are never accessible to Chipi servers. # useMigrateWalletToPasskey Source: https://docs.chipipay.com/sdk/react/hooks/use-migrate-wallet-to-passkey Migrates an existing PIN-encrypted wallet to passkey (biometric) authentication. ## Usage ```typescript theme={null} const { migrateWalletToPasskey, migrateWalletToPasskeyAsync, data, isLoading, isError, isSuccess, error, reset, } = useMigrateWalletToPasskey(); ``` ### Parameters The mutation accepts an object with: | Parameter | Type | Required | Description | | ---------------- | ------------ | -------- | --------------------------------------------------------------- | | `wallet` | `WalletData` | Yes | The current wallet object (`publicKey` + `encryptedPrivateKey`) | | `oldEncryptKey` | `string` | Yes | The user's current PIN used to decrypt the wallet | | `externalUserId` | `string` | Yes | Your app's user ID — used as the passkey username | | `bearerToken` | `string` | Yes | Auth token from your provider | ### Return Value | Property | Type | Description | | ----------------------------- | -------------------------------------------------- | --------------------------- | | `migrateWalletToPasskey` | `(input) => void` | Fire-and-forget migration | | `migrateWalletToPasskeyAsync` | `(input) => Promise` | Promise-based migration | | `data` | `MigrateWalletToPasskeyResult \| undefined` | Migration result | | `isLoading` | `boolean` | True while migrating | | `isError` | `boolean` | True if migration failed | | `isSuccess` | `boolean` | True if migration succeeded | | `error` | `Error \| null` | Error details | | `reset` | `() => void` | Reset mutation state | ### `MigrateWalletToPasskeyResult` | Property | Type | Description | | -------------- | ------------ | ------------------------------------------------------ | | `success` | `boolean` | Whether migration succeeded | | `wallet` | `WalletData` | Updated wallet with new passkey-encrypted private key | | `credentialId` | `string` | The passkey credential ID (store this for future auth) | ## How It Works 1. A new passkey is created in the browser/device (triggers biometric prompt) 2. The wallet's private key is decrypted using `oldEncryptKey` (the PIN) 3. The private key is re-encrypted using the passkey-derived key 4. The updated wallet is returned — **persist the new wallet object and `credentialId`** After migration, the old PIN (`oldEncryptKey`) will no longer work. Store the updated wallet object and `credentialId` securely. ## Example Implementation ```typescript theme={null} import { useMigrateWalletToPasskey } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; import { useState } from "react"; export function MigrateToPasskey({ currentWallet, userId }: { currentWallet: WalletData; userId: string; }) { const { getToken } = useAuth(); const [currentPin, setCurrentPin] = useState(""); const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey(); const handleMigrate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; try { const result = await migrateWalletToPasskeyAsync({ wallet: currentWallet, oldEncryptKey: currentPin, externalUserId: userId, bearerToken, }); // Persist the updated wallet and credentialId localStorage.setItem("wallet", JSON.stringify(result.wallet)); localStorage.setItem("passkeyCredentialId", result.credentialId); alert("Successfully migrated to passkey!"); } catch (err) { console.error("Migration failed:", err); } }; return (
setCurrentPin(e.target.value)} /> {error &&

Error: {error.message}

}
); } ``` ## Related * [Use Passkeys Guide](/sdk/react/use-passkeys) — Full passkey setup walkthrough * **useCreateWallet** — Create a wallet with passkey from the start (set `usePasskey: true`) # useRecordSendTransaction Source: https://docs.chipipay.com/sdk/react/hooks/use-record-transaction Records a send transaction in the Chipi system for tracking and webhook notifications. ## Usage ```typescript theme={null} const { recordSendTransactionAsync, data, isLoading, error } = useRecordSendTransaction(); ``` ### Parameters The hook accepts an object with: * `params` (`RecordSendTransactionParams`): * `transactionHash` (string): The hash of the transaction to record * `chain` (`Chain`): The blockchain network. Use `Chain.STARKNET` * `expectedSender` (string): The expected sender wallet address * `expectedRecipient` (string): The expected recipient wallet address * `expectedToken` (`ChainToken`): The expected token. Use `ChainToken.USDC` * `expectedAmount` (string): The expected transaction amount * `bearerToken` (string): Bearer token for authentication ### Return Value Returns an object containing: * `recordSendTransaction`: Function to record transaction (fire-and-forget) * `recordSendTransactionAsync`: Promise-based function that resolves with the transaction record * `data`: The recorded transaction data (`Transaction | undefined`) * `isLoading`: Boolean indicating if the operation is in progress * `isError`: Boolean indicating if an error occurred * `error`: Error instance when `isError` is true, otherwise `null` * `isSuccess`: Boolean indicating if the recording completed successfully * `reset`: Function to reset the mutation state ## Example Implementation ```typescript theme={null} import { useRecordSendTransaction } from "@chipi-stack/chipi-react"; import { Chain, ChainToken } from "@chipi-stack/types"; export function RecordTransactionForm() { const { recordSendTransactionAsync, data, isLoading, isError, error } = useRecordSendTransaction(); const { getToken } = useAuth(); const handleRecordTransaction = async () => { try { const token = await getToken(); if (!token) throw new Error("No token found"); // After a transfer, record the transaction const transactionHash = "0x..."; // From your transfer transaction const senderWallet = { publicKey: "0x123..." }; const merchant = "0x456..."; const amountUsd = "100.00"; await recordSendTransactionAsync({ params: { transactionHash, chain: Chain.STARKNET, expectedSender: senderWallet.publicKey, expectedRecipient: merchant, expectedToken: ChainToken.USDC, expectedAmount: amountUsd, }, bearerToken: token, }); } catch (err) { console.error('Recording transaction failed:', err); } }; return (

Record Transaction

{data && (

Transaction Recorded: {data.id}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always verify recipient addresses * Use encrypted private keys * Implement proper PIN validation * Monitor transaction status ## Error Handling * Handle insufficient token balance * Validate wallet addresses * Monitor gas fees * Implement retry logic for failed transactions Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Hooks * **useTransfer** - Execute the transfer before recording it * **useGetTransactionList** - List all recorded transactions # useRevokeSessionKey Source: https://docs.chipipay.com/sdk/react/hooks/use-revoke-session-key Revoke a session key from the CHIPI wallet smart contract. After revocation, the session key can no longer execute transactions. Always revoke active sessions when a user logs out to prevent unauthorized access. ## Usage ```typescript theme={null} const { revokeSessionKey, revokeSessionKeyAsync, data, isLoading, error, isSuccess, reset } = useRevokeSessionKey(); ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------- | ---------- | -------- | ------------------------------------------------------------------- | | `params.encryptKey` | string | Yes | Owner's PIN to decrypt wallet private key for signing | | `params.wallet` | WalletData | Yes | Wallet object with `publicKey`, `encryptedPrivateKey`, `walletType` | | `params.sessionPublicKey` | string | Yes | Public key of the session to revoke | | `bearerToken` | string | Yes | Authentication token from your auth provider | ### Return Value | Property | Type | Description | | ----------------------- | ------------- | ------------------------------------------------- | | `revokeSessionKey` | function | Trigger revocation (fire-and-forget) | | `revokeSessionKeyAsync` | function | Trigger revocation (returns Promise with tx hash) | | `data` | string | Transaction hash of revocation | | `isLoading` | boolean | Whether revocation is in progress | | `isError` | boolean | Whether an error occurred | | `error` | Error \| null | Error details if any | | `isSuccess` | boolean | Whether revocation succeeded | | `reset` | function | Reset mutation state | ## Example Implementation ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/chipi-react"; export function RevokeSession() { const { revokeSessionKeyAsync, isLoading, error, isSuccess } = useRevokeSessionKey(); const [pin, setPin] = useState(''); const handleRevoke = async () => { const bearerToken = await getBearerToken(); const session = JSON.parse(localStorage.getItem('chipiSession') || '{}'); if (!session.publicKey) { alert('No active session found'); return; } try { const txHash = await revokeSessionKeyAsync({ params: { encryptKey: pin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI", }, sessionPublicKey: session.publicKey, }, bearerToken, }); // Clear from local storage localStorage.removeItem('chipiSession'); console.log('Session revoked:', txHash); } catch (err) { console.error('Failed to revoke session:', err); } }; return (

Revoke Session

setPin(e.target.value)} placeholder="Enter your PIN" className="w-full p-2 border rounded mb-4" /> {error && (
Error: {error.message}
)} {isSuccess && (
Session revoked successfully!
)}
); } ``` ## Secure Logout Handler Always revoke sessions during logout: ```typescript theme={null} import { useRevokeSessionKey } from "@chipi-stack/chipi-react"; export function useSecureLogout() { const { revokeSessionKeyAsync } = useRevokeSessionKey(); const logout = async (wallet: WalletData, pin: string) => { const sessionStr = localStorage.getItem('chipiSession'); if (sessionStr) { const session = JSON.parse(sessionStr); const bearerToken = await getBearerToken(); try { // Revoke session on-chain before logout await revokeSessionKeyAsync({ params: { encryptKey: pin, wallet: { ...wallet, walletType: "CHIPI" }, sessionPublicKey: session.publicKey, }, bearerToken, }); } catch (error) { console.warn("Failed to revoke session:", error); // Continue with logout even if revoke fails } // Clear from local storage localStorage.removeItem('chipiSession'); } // Proceed with auth logout await authProvider.signOut(); }; return { logout }; } ``` If revocation fails (e.g., network error), the session will still expire at its `validUntil` timestamp. However, it's best practice to always attempt revocation. ## When to Revoke | Scenario | Should Revoke? | | ------------------------- | --------------------------------------------- | | User logs out | ✅ Yes - always | | Session expired naturally | ❌ No - already inactive | | User requests new session | ⚠️ Optional - old session will expire | | Security concern | ✅ Yes - immediately | | App uninstall | N/A - Can't revoke, session expires naturally | Revocation requires a blockchain transaction which may take 10-30 seconds. Don't block the logout flow - fire the revocation and continue. # useSyncOnChainTransfers Source: https://docs.chipipay.com/sdk/react/hooks/use-sync-on-chain-transfers Sync on-chain token transfers into the database. Discovers external receives not made through the SDK. ## Usage ```typescript theme={null} const { syncTransfers, syncTransfersAsync, data, isLoading, isError, error, isSuccess, reset } = useSyncOnChainTransfers(); ``` ### Parameters `syncTransfers` / `syncTransfersAsync` accept an object with: | Parameter | Type | Required | Description | | --------------- | -------- | -------- | --------------------------------------- | | `walletAddress` | `string` | Yes | Wallet public key to sync transfers for | | `bearerToken` | `string` | Yes | Bearer token for authentication | ### Return Value | Property | Type | Description | | -------------------- | -------------------------------------------------- | ------------------------------ | | `syncTransfers` | `(input) => void` | Trigger sync (fire-and-forget) | | `syncTransfersAsync` | `(input) => Promise` | Promise-based sync | | `data` | `SyncOnChainTransfersResponse \| undefined` | Sync result | | `isLoading` | `boolean` | Whether sync is in progress | | `isError` | `boolean` | Whether an error occurred | | `error` | `Error \| null` | Error from last sync attempt | | `isSuccess` | `boolean` | Whether last sync succeeded | | `reset` | `() => void` | Reset mutation state | ### SyncOnChainTransfersResponse | Field | Type | Description | | -------- | -------- | ---------------------------------------------- | | `synced` | `number` | New transactions discovered and saved to DB | | `total` | `number` | Total transfers found on-chain for this wallet | ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useSyncOnChainTransfers, useGetTransactionList, } from "@chipi-stack/nextjs"; export function TransactionHistory({ wallet }: { wallet: { publicKey: string } }) { const { getToken } = useAuth(); const { syncTransfersAsync, isLoading: isSyncing } = useSyncOnChainTransfers(); const { data: txList, refetch } = useGetTransactionList({ query: { walletAddress: wallet.publicKey }, getBearerToken: getToken, }); const handleSync = async () => { const token = await getToken(); if (!token) return; // Discover external transfers (e.g., USDC received from Argent X) const result = await syncTransfersAsync({ walletAddress: wallet.publicKey, bearerToken: token, }); console.log(`Found ${result.synced} new transfers`); // Transaction list cache is automatically invalidated — refetch happens }; return (
{txList?.data.map((tx) => (
{tx.subType === "receive" ? "Received" : "Sent"} {tx.amount} {tx.token} {tx.transactionHash.slice(0, 12)}...
))}
); } ``` ## How It Works 1. Calls `POST /transactions/sync-on-chain` on the backend 2. Backend queries Voyager API for all token transfers for the wallet 3. Compares with existing DB records by transaction hash 4. Saves new transfers to the `Transaction` table (read-through cache) 5. Returns `{ synced, total }` 6. Invalidates `useGetTransactionList` cache so new transfers appear immediately After sync, `useGetTransactionList` returns both SDK-initiated transfers AND external receives. ## When to Use * **On wallet load**: Sync once when user opens their wallet to discover any external receives * **Pull-to-refresh**: Let users manually trigger sync * **After receiving funds**: If user expects incoming funds from an external wallet Synced transfers are cached in the database. Subsequent `useGetTransactionList` calls return them without re-querying Voyager. ## Limitations * Only discovers token transfers (ERC-20). Contract interactions without transfers are not synced. * Voyager API rate limits apply. Avoid calling sync on every render. * First sync for a wallet with many transfers may take a few seconds (paginated, max 500 transfers per sync). ## Related * [useGetTransactionList](/sdk/nextjs/hooks/use-get-transaction-list) — Read the transaction list (includes synced transfers) * [useGetTransaction](/sdk/nextjs/hooks/use-get-transaction) — Get a single transaction by hash * [useGetTransactionStatus](/sdk/nextjs/hooks/use-get-transaction-status) — Poll transaction status # useTransfer Source: https://docs.chipipay.com/sdk/react/hooks/use-transfer Transfers tokens from the user wallet to another address. Uses Avnus gasless to cover gas fees. ## Usage ```typescript theme={null} const { transfer, transferAsync, data, isLoading, isError, error, isSuccess, reset, } = useTransfer(); ``` ### Parameters * `transfer` / `transferAsync` both accept an object with: * `params` (`TransferHookInput`): * `encryptKey` (string): PIN used to decrypt the private key. * `wallet` (`WalletData`): the wallet object. **Pass the full object from `createWallet` / `useGetWallet`** — `publicKey`, `encryptedPrivateKey`, **`walletType`**, and **`signerKind`**. `walletType` is routing-critical: SHHH wallets execute through a different path, and omitting it defaults to `"READY"`, which **reverts** on a SHHH account. `signerKind` is SHHH-only (defaults to `"STARK"`). * `token` (`ChainToken`): Token identifier (e.g. `ChainToken.USDC`). * `usePasskey` (boolean, optional): When `true`, the hook authenticates with passkey internally (requires `externalUserId`). Do not use with external `setupPasskey`/`authenticate` calls. * `otherToken` (optional): Custom token configuration with: * `contractAddress` (string): ERC-20 token contract address. * `decimals` (number): Token decimals. * `recipient` (string): Destination wallet address. * `amount` (number): Transfer amount (will be converted to a string internally). * `bearerToken` (string): Bearer token for authentication (e.g. from your auth provider). ### Return Value Returns an object containing: * `transfer`: Function to trigger the transfer (fire-and-forget style). * `transferAsync`: Promise-based function that resolves with the transaction hash. * `data`: Transaction hash of the transfer (string | undefined). * `isLoading`: Boolean indicating if the operation is in progress. * `isError`: Boolean indicating if an error occurred. * `error`: Error instance when `isError` is true, otherwise `null`. * `isSuccess`: Boolean indicating if the transfer completed successfully. * `reset`: Function to reset the mutation state. ## Example Implementation ```typescript theme={null} import { useState } from "react"; import { useAuth } from "@clerk/nextjs"; import { useTransfer, ChainToken } from "@chipi-stack/chipi-react"; export function TransferForm() { const { getToken } = useAuth(); const { transferAsync, data, isLoading, isError, error } = useTransfer(); const [form, setForm] = useState({ pin: '', recipient: '', amount: '' }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const bearerToken = await getToken(); if (!bearerToken) { console.error('No bearer token available'); return; } try { await transferAsync({ params: { amount: Number(form.amount), encryptKey: form.pin, // Use the wallet you got from createWallet / useGetWallet, with its // walletType + signerKind. These route the transfer correctly — // omitting walletType defaults to "READY" and a SHHH wallet reverts. wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: wallet.walletType, // "SHHH" | "CHIPI" | "READY" signerKind: wallet.signerKind, // SHHH only; defaults to "STARK" }, token: ChainToken.USDC, recipient: form.recipient }, bearerToken, }); } catch (err) { console.error('Transfer failed:', err); } }; return (

Transfer Tokens

setForm({...form, pin: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, recipient: e.target.value})} className="w-full p-2 border rounded-md" required />
setForm({...form, amount: e.target.value})} className="w-full p-2 border rounded-md" required />
{data && (

TX Hash: {data}

)} {isError && error && (
Error: {error.message}
)}
); } ``` ## Security Considerations * Always verify recipient addresses * Use encrypted private keys * Implement proper PIN validation * Monitor transaction status ## Error Handling * Handle insufficient token balance * Validate wallet addresses * Monitor gas fees * Implement retry logic for failed transactions Always verify recipient addresses. Transfers on StarkNet are irreversible. ## Related Hooks * **useApprove** - Required before transferring new token types * **useCallAnyContract** - For custom contract interactions # useUpdateWalletEncryption Source: https://docs.chipipay.com/sdk/react/hooks/use-update-wallet-encryption Updates a wallet's encrypted private key after the user changes their passkey or PIN. ## Usage ```typescript theme={null} const { updateWalletEncryption, updateWalletEncryptionAsync, data, isLoading, isError, error, isSuccess, reset, } = useUpdateWalletEncryption(); ``` ### Parameters The mutation accepts an object with: | Parameter | Type | Required | Description | | ------------------------ | -------- | -------- | --------------------------------------------------------------- | | `externalUserId` | `string` | Yes | Your app's user ID | | `newEncryptedPrivateKey` | `string` | Yes | The wallet private key re-encrypted with the new passkey or PIN | | `publicKey` | `string` | No | Wallet public key; required when the user has multiple wallets | | `bearerToken` | `string` | Yes | Auth token from your provider | ### Return Value | Property | Type | Description | | ----------------------------- | ---------------------------------------------------- | ------------------------------------ | | `updateWalletEncryption` | `(input) => void` | Fire-and-forget update | | `updateWalletEncryptionAsync` | `(input) => Promise` | Promise-based update | | `data` | `UpdateWalletEncryptionResponse \| undefined` | Response from the API | | `isLoading` | `boolean` | True while the update is in progress | | `isError` | `boolean` | True if the update failed | | `isSuccess` | `boolean` | True if the update succeeded | | `error` | `Error \| null` | Error details | | `reset` | `() => void` | Reset mutation state | ### `UpdateWalletEncryptionResponse` | Property | Type | Description | | --------- | --------- | ---------------------------- | | `success` | `boolean` | Whether the update succeeded | ## How It Works 1. The client re-encrypts the wallet private key with a new passkey or PIN 2. `updateWalletEncryption` sends the new ciphertext to the Chipi backend 3. The backend replaces the stored encrypted private key 4. The wallet query cache is automatically invalidated so subsequent reads return fresh data After calling this hook, any previous passkey or PIN used to encrypt the private key will no longer work. Make sure the new encryption is persisted before discarding the old key material. ## Example Implementation ```typescript theme={null} import { useUpdateWalletEncryption } from "@chipi-stack/chipi-react"; import { usePasskeySetup } from "@chipi-stack/chipi-passkey/hooks"; import { useAuth } from "@clerk/nextjs"; export function RotatePasskey({ wallet, userId }: { wallet: WalletData; userId: string; }) { const { getToken } = useAuth(); const { setupPasskey } = usePasskeySetup(); const { updateWalletEncryptionAsync, isLoading, isSuccess, error, } = useUpdateWalletEncryption(); const handleRotate = async () => { const bearerToken = await getToken(); if (!bearerToken) return; try { // Create a new passkey (triggers biometric prompt). setupPasskey needs // userId + userName and returns an object — destructure encryptKey. const { encryptKey: newEncryptKey } = await setupPasskey(userId, userName); // Re-encrypt the private key client-side with the new key. There is no // `wallet.privateKey` — decrypt `wallet.encryptedPrivateKey` with the OLD // key first to recover the plaintext, then re-encrypt with the new one. // Keep plaintext key material in-memory only — never log or persist it. const plaintextKey = decrypt(wallet.encryptedPrivateKey, oldEncryptKey); const newEncryptedPrivateKey = encrypt( plaintextKey, newEncryptKey ); await updateWalletEncryptionAsync({ externalUserId: userId, newEncryptedPrivateKey, publicKey: wallet.publicKey, bearerToken, }); alert("Passkey rotated successfully!"); } catch (err) { console.error("Failed to rotate passkey:", err); } }; return (
{isSuccess &&

Encryption updated.

} {error &&

Error: {error.message}

}
); } ``` ## Related * [useMigrateWalletToPasskey](/sdk/react/hooks/use-migrate-wallet-to-passkey) — Migrate from PIN to passkey * [Use Passkeys Guide](/sdk/react/use-passkeys) — Full passkey setup walkthrough # useX402Payment Source: https://docs.chipipay.com/sdk/react/hooks/use-x402-payment React hook for the x402 payment protocol. Wraps fetch with automatic HTTP 402 payment handling using USDC on Starknet. ## Usage ```typescript theme={null} const { x402Fetch, payFetch, signPayment, isPaying, lastTxHash, lastAmount, lastPayment, totalSpent, error, paymentCount, } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "1.00", }); ``` ### Configuration | Parameter | Type | Required | Description | | ------------------- | ------------------------------------------ | -------- | ----------------------------------------------------------------- | | `wallet` | `WalletData \| null` | Yes | Wallet data with `publicKey` and `encryptedPrivateKey` | | `encryptKey` | `string` | Yes | PIN or passkey-derived key for signing | | `getBearerToken` | `() => Promise` | No | Function returning auth token (for session key path) | | `bearerToken` | `string` | No | Static bearer token (alternative to `getBearerToken`) | | `maxPaymentAmount` | `string` | No | Max USDC per payment in human-readable format (e.g. `"1.00"`) | | `maxAmount` | `string` | No | Alias for `maxPaymentAmount` | | `allowedRecipients` | `string[]` | No | Whitelist of recipient addresses. Empty = allow all | | `session` | `SessionKeyData \| null` | No | Active session key for automatic payments without owner signature | | `onPaymentComplete` | `(txHash: string, amount: string) => void` | No | Callback fired after successful payment | ### Return Value | Property | Type | Description | | -------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `x402Fetch` | `(url: string, init?: RequestInit) => Promise` | Drop-in `fetch` replacement — auto-handles 402 | | `payFetch` | Same as `x402Fetch` | Alias | | `signPayment` | `(requirement: PaymentRequirement) => Promise` | Manual payment signing for custom flows | | `isPaying` | `boolean` | Whether a payment is in progress | | `lastTxHash` | `string \| null` | Last payment transaction hash (session payments only — standard wallet returns `null`) | | `lastAmount` | `string \| null` | Last payment amount in human-readable USDC | | `lastPayment` | `LastPaymentInfo \| null` | Last payment details: `{ txHash, amount, timestamp }` | | `totalSpent` | `string` | Total USDC spent this session (e.g. `"0.030000"`) | | `error` | `Error \| null` | Error from last payment attempt | | `paymentCount` | `number` | Total payments made this session | ## Example Implementation ```typescript theme={null} import { useX402Payment, ChainToken } from "@chipi-stack/chipi-react"; import { useAuth } from "@clerk/nextjs"; export function PremiumApiClient({ wallet, pin }: { wallet: WalletData; pin: string }) { const { getToken } = useAuth(); const { x402Fetch, isPaying, lastAmount, paymentCount, totalSpent, error, } = useX402Payment({ wallet, encryptKey: pin, getBearerToken: getToken, maxPaymentAmount: "0.10", // Max 0.10 USDC per request onPaymentComplete: (txHash, amount) => { console.log(`Paid ${amount} USDC`); }, }); const handleQuery = async () => { // x402Fetch works like fetch — if the server returns 402, // the hook automatically signs and retries with X-PAYMENT header const response = await x402Fetch("https://api.example.com/premium/data"); const data = await response.json(); console.log("Response:", data); }; return (
{lastAmount &&

Last payment: {lastAmount} USDC

}

Total spent: {totalSpent} USDC ({paymentCount} payments)

{error &&

Error: {error.message}

}
); } ``` ## Payment Flows ### Standard Wallet (no session) 1. `x402Fetch` makes initial request 2. Server returns `402` with `PAYMENT-REQUIRED` header 3. Hook parses requirement, validates against `maxPaymentAmount` and `allowedRecipients` 4. Signs SNIP-12 typed data locally via `signTypedData()` (no on-chain transaction) 5. Retries with `X-PAYMENT` header containing the signed payload 6. Facilitator settles the payment on-chain ### Session Key (automatic payments) 1. Same flow, but signs using session key via `executeTransactionWithSession()` 2. Payment executes on-chain immediately (pre-signed) 3. `lastTxHash` contains the actual transaction hash 4. No owner signature needed — session key handles it ## Security * `maxPaymentAmount` enforced before signing — prevents overpayment * `allowedRecipients` whitelist — prevents payment to unauthorized addresses * Only `"exact"` scheme and `"starknet-mainnet"` network accepted * Only USDC asset accepted (native USDC contract address validated) * Amount validation uses BigInt math (no float precision issues) Standard wallet payments return `lastTxHash: null` because the signature is SNIP-12 off-chain — the facilitator settles on-chain later. Session payments return the actual transaction hash. # React SDK Introduction Source: https://docs.chipipay.com/sdk/react/introduction Learn about the React SDK capabilities and what you can build with it # React SDK Our React SDK allows you to create self-custodial wallets and sign transactions without the need for gas in web applications. You can then use the Chipi Dashboard to manage your wallets and transactions, as well as see analytics on your transactions. The Chipi React SDK is designed specifically for web applications, focusing on gasless transactions and wallet management for React apps. ## ✨ Key Features ### Complete Wallet Suite * **Wallet Management** - Create and manage wallets * **Transaction Signing** - Sign transactions without the need for gas * **Social Login Integration** - Bring your own auth provider (Clerk, Firebase, etc.) * **Automatic Wallet Creation** - No manual key management ### Advanced Wallet Features * **Sessions** - Allow users to sign once and use their wallet for multiple transactions * **Web Authentication** - Browser-based authentication * **MPC Support** - Coming soon! ### Developer Experience * **TypeScript Support** - Full type safety * **React Support** - Full React compatibility * **Quickstart** - We provide a quickstart guide to get you started ## 🏗️ Architecture Our React SDK is a simple wrapper around the Chipi API, optimized for web development. It is designed to be used with the Chipi Dashboard, which is a web-based dashboard for setting up your project. Simply follow the steps in the [Quickstart Guide](/sdk/react/gasless-quickstart) to get started. If you wish to use an unlisted programming language, you can use the Chipi API directly. Send us a message on [Telegram](https://t.me/+e2qjHEOwImkyZDVh) and we will help you get started. ## 🌐 Platform Support * **Modern Browsers** - Chrome, Firefox, Safari, Edge * **React 18+** - Hooks support required * **TypeScript** - Full TypeScript support * **Webpack/Vite** - Compatible with modern bundlers ## 🚦 Getting Started Ready to build? Start with our [Gasless Quickstart Guide](/sdk/react/gasless-quickstart) to set up gasless transactions, or explore our other guides: * **[Gasless Tutorial](/sdk/react/gasless-clerk-setup)** - Learn gasless transactions for web ## 🔗 Resources * [Dashboard](https://dashboard.chipipay.com/) - Manage your account * [API Reference](/sdk/api/quickstart) - Complete API documentation * [GitHub](https://github.com/chipi-pay) - Source code and examples * [Support](https://t.me/+e2qjHEOwImkyZDVh) - Community help # useShield / useUnshield / usePrivateBalance Source: https://docs.chipipay.com/sdk/react/privacy-hooks Privacy hooks over the mainnet-proven STRK20 engine in @chipi-stack/core. **`useShield` (deposits) is temporarily paused.** After StarkWare's July 2026 STRK20 pool **v2.0** upgrade, shielding into the shared privacy pool requires a screener attestation issued by StarkWare's hosted proving service — Chipi is finalizing that access. **`useUnshield` and `usePrivateBalance` work today; shields will revert until access lands.** Don't ship a shield flow to production yet — we'll announce on X ([@hichipipay](https://x.com/hichipipay)) when shielding is back on. Signatures below are stated from the package types (`@chipi-stack/chipi-react`) — if a snippet here doesn't compile against the installed package, that's a docs bug; report it. ## The trust model, honestly Keys derive client-side and are never stored server-side — but STRK20's **delegated proving requires the pool spending key inside each proof request**, so Chipi's proving service (a private prover Chipi operates; the same model as StarkWare's hosted prover) sees it per operation and must be trusted not to retain or use it. Privacy holds against the **public** — chain observers, other users — not against the proving operator. Do not tell your users "nobody, including Chipi, can see your balance." Local proving is the roadmap item that removes this trust. ## useShield() Moves money INTO the private balance. Returns `{ shield, shieldAsync, data, isLoading, isError, error, isSuccess, reset }`. `shieldAsync(input: ShieldHookInput)` where: ```ts theme={null} interface ShieldHookInput { config: PrivacyConfig; keys: PrivacyKeys; // from derivePrivacyAccount — client-side only token: string; // token contract address amount: bigint; // base units fundDeposit?: (companionAddress: string, amount: bigint) => Promise; onStep?: (step: PrivacyStep) => void; // "prepare"|"fund"|"deposit"|"prove"|"finish" } ``` Orchestration (all retried/hardened): gas-tank funding → your `fundDeposit` transfer → funding-arrival wait → companion deploy + approvals → prove → gateway submit → **receipt + pool-event assertion** (no phantom successes) → background STRK sweep. Resolves with `{ transactionHash, via, receipt, feeStrk }`. ## useUnshield() Same spine, plus **`deliverTo: string`** (required): withdrawals land in the companion and are delivered onward to the user's wallet (awaited). ## Multiple tokens Both hooks are **per-token by design** (`token: string, amount: bigint`). * **Batch shield** (N tokens, one proof, one 4-STRK fee) lives in the core engine: [`execShieldMany`](/sdk/core/privacy#multi-token-operations). Call it directly for multi-token shields; a single-item batch is exactly the hook's path. * **Sequential unshields** need spacing: proving runs against `head − 12`, so a second withdrawal within \~13 blocks (\~5–6 min) of the first fails with the prover's `state_too_fresh` hint. Catch it, tell the user what already came back, and retry the rest a few minutes later — the full pattern is on the [Core SDK privacy page](/sdk/core/privacy#unshielding-several-tokens). ## usePrivateBalance(input) Read-only — the difference from the actions: no gesture after setup, no gas, no writes. * `{ mode: "ledger", events: PrivacyLedgerEvent[] }` — instant netting of your recorded events, clamped at zero. Use for passive display. * `{ mode: "onchain", config, keys, tokens? }` — authoritative pool note scan with the viewing key. Use for an explicit "verify on-chain" action. Returns `{ byToken: Record, isLoading, isError, error, refetch }`. ## derivePrivacyAccount(p) `{ anchorSecret: Uint8Array; seedDomainTag: string; classHash: string } → Promise` — deterministic, client-side. Cache the address, never the keys. # Use Passkeys as a PIN Source: https://docs.chipipay.com/sdk/react/use-passkeys Secure wallet authentication with biometric passkeys instead of PINs ## Overview WebAuthn passkeys replace PINs with biometric authentication (Face ID, Touch ID, Windows Hello). No PINs to remember or store. ## Prerequisites * Modern browser with WebAuthn support (Chrome, Safari, Firefox, Edge) * Biometric hardware (fingerprint, face recognition) * Chipi React SDK installed ## Installation ```bash theme={null} npm install @chipi-stack/chipi-passkey ``` ## Basic Implementation ```typescript Import theme={null} import { usePasskeySetup } from "@chipi-stack/chipi-passkey/hooks"; ``` ```typescript Usage theme={null} ``` ```typescript Import theme={null} import { usePasskeyAuth } from "@chipi-stack/chipi-passkey/hooks"; ``` ```typescript Usage theme={null} ``` If users already have a PIN-based wallet, they can migrate to passkey: ```typescript Import theme={null} import { useMigrateWalletToPasskey } from "@chipi-stack/chipi-react"; ``` ```typescript Usage theme={null} ``` ## Example ```typescript theme={null} import { useCreateWallet, Chain } from '@chipi-stack/chipi-react'; import { usePasskeySetup } from '@chipi-stack/chipi-passkey/hooks'; import { useState } from 'react'; export function CreateWalletWithPasskey() { const { createWalletAsync, isLoading, error } = useCreateWallet(); const { setupPasskey } = usePasskeySetup(); const [wallet, setWallet] = useState(null); const handleCreate = async () => { try { // Triggers biometric prompt const passkeyResult = await setupPasskey('user-123', 'User Name'); const encryptKey = passkeyResult.encryptKey; const result = await createWalletAsync({ params: { encryptKey, usePasskey: true, externalUserId: 'user-123', }, bearerToken: 'your-jwt-token', }); localStorage.setItem('wallet', JSON.stringify(result)); setWallet(result); } catch (err) { console.error(err); } }; return (
{error &&

Error: {error.message}

} {wallet &&

Wallet created: {wallet.publicKey}

}
); } ```
```typescript theme={null} import { useTransfer, ChainToken } from '@chipi-stack/chipi-react'; import { usePasskeyAuth } from '@chipi-stack/chipi-passkey/hooks'; import { useState } from 'react'; export function TransferWithPasskey() { const { transferAsync, isLoading } = useTransfer(); const { authenticate } = usePasskeyAuth(); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); const handleTransfer = async () => { try { // Triggers biometric prompt — returns null if user cancels const encryptKey = await authenticate(); if (!encryptKey) return; const wallet = JSON.parse(localStorage.getItem('wallet')!); const txHash = await transferAsync({ params: { encryptKey, usePasskey: true, wallet, token: 'USDC', recipient, amount: Number(amount), }, bearerToken: 'your-jwt-token', }); alert(`Transfer complete: ${txHash}`); } catch (err) { console.error(err); } }; return (
setRecipient(e.target.value)} /> setAmount(e.target.value)} type="number" />
); } ```
```typescript theme={null} import { useMigrateWalletToPasskey } from '@chipi-stack/chipi-react'; import { useState } from 'react'; export function MigrateToPasskey() { const { migrateWalletToPasskeyAsync, isLoading } = useMigrateWalletToPasskey(); const [currentPin, setCurrentPin] = useState(''); const handleMigrate = async () => { try { const wallet = JSON.parse(localStorage.getItem('wallet')!); // The hook handles passkey creation internally — // just pass the old PIN and it re-encrypts with passkey await migrateWalletToPasskeyAsync({ wallet, oldEncryptKey: currentPin, externalUserId: 'user-123', bearerToken: 'your-jwt-token', }); alert('Successfully migrated to passkey!'); } catch (err) { console.error(err); } }; return (
setCurrentPin(e.target.value)} />
); } ```
## Hooks Reference | Hook | Purpose | | --------------------------- | ---------------------------------- | | `usePasskeySetup` | Create new passkey | | `usePasskeyAuth` | Authenticate with existing passkey | | `usePasskeyStatus` | Check passkey support & status | | `useMigrateWalletToPasskey` | Migrate from PIN to passkey | ## Browser Support * ✅ Chrome 67+ (Desktop & Android) * ✅ Safari 14+ (iOS 14+, macOS) * ✅ Firefox 60+ (Desktop) * ✅ Edge 18+ (Desktop) Passkeys are stored in the browser and synced via platform (iCloud Keychain, Google Password Manager). ## Security Benefits * **No PINs stored** - Keys derived on-demand from biometric * **Platform-level security** - Hardware-backed authentication * **Phishing resistant** - Domain-bound credentials * **User convenience** - One tap authentication ## Next Steps * Check out [useCreateWallet](/sdk/react/hooks/use-create-wallet) for wallet creation options * Learn about [useTransfer](/sdk/react/hooks/use-transfer) for token transfers * Explore [session keys](/sdk/react/hooks/use-create-session-key) for additional security # v14.5.0 — SHHH V8.4 is the new default Source: https://docs.chipipay.com/sdk/releases/v14-5-0 What changed when chipi-stack v14.5.0 flipped the createWallet default from CHIPI v29 to SHHH V8.4 STARK, who's affected, and the safe upgrade path for existing integrations. v14.5.0 ships SHHH V8.4 ShhhAccount as the new default wallet class — pluggable signer kinds, guardian recovery, threshold support — alongside every legacy CHIPI v29 + READY path. Existing wallets are unaffected; only NEW wallets created via `createWallet` without an explicit `walletType` see the change. SHHH V8.4 is available without being formally advertised until external audit closes. ## TL;DR — does this affect me? | Your setup | Action you need to take | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Pinned to `@chipi-stack/backend@14.4.x` (or earlier) | **None.** Default unchanged on the version you have. Upgrade when you're ready. | | Upgrading to `@chipi-stack/backend@14.5.0` AND you pass `walletType: "CHIPI"` explicitly | **None.** Your CHIPI v29 wallets still produce CHIPI v29 wallets. | | Upgrading to `@chipi-stack/backend@14.5.0` AND you pass `walletType: "READY"` | **None.** READY (Argent X v0.4.0) is unchanged. | | Upgrading to `@chipi-stack/backend@14.5.0` AND you OMIT `walletType` | **Read this page.** New wallets now deploy as SHHH V8.4 with `signerKind: "STARK"` by default. | | Operating end-user wallets via the dashboard | **None.** Your existing wallets keep working; new dashboard-created wallets default to SHHH V8.4 STARK. | ## What changed In every version up to and including `@chipi-stack/backend@14.4.x`, omitting `walletType` from `createWallet` produced a CHIPI v29 wallet (legacy OpenZeppelin account with SNIP-9 session-keys support). As of `@chipi-stack/backend@14.5.0`, the same call produces a **SHHH V8.4** wallet with `signerKind: "STARK"`. The wallet's address is computed differently for SHHH V8.4 (`computeShhhAddress`), so a wallet created with the new default has a different address than one created at the same `externalUserId` would have under the old default. **Address determinism IS preserved within the same default** — calling `createWallet` twice with the same inputs against v14.5.0 produces the same SHHH address. The behavior matrix: | Caller passes | Pre-v14.5.0 result | Post-v14.5.0 result | | --------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Nothing (omits `walletType`) | CHIPI v29 | **SHHH V8.4 STARK** ← changed | | `walletType: "CHIPI"` | CHIPI v29 | CHIPI v29 (unchanged) | | `walletType: "READY"` | READY | READY (unchanged) | | `walletType: "SHHH"`, `signerKind: "STARK"` | SHHH (opt-in, since the SHHH builders landed in the SDK) | SHHH (unchanged) | | `walletType: "SHHH"` w/o `signerKind` (TS) | implicit STARK | implicit STARK | | `wallet_type=SHHH` w/o `signer_kind` (Python) | ValueError | **ValueError** (Python keeps the explicit-SHHH strictness; only the default-flip path falls back to STARK) | ## What you get for free SHHH V8.4 closes the three biggest gaps the legacy CHIPI v29 had: 1. **Guardian recovery** — 7-day timelocked lost-key recovery without giving up custody. See [recovery](/services/gasless/recovery). 2. **Multi-device + multi-owner** — add a passkey or MetaMask owner to an existing wallet via a 48-hour timelocked `propose_add_owner` flow. 3. **Threshold (N-of-M)** — multi-owner approvals with mixed signer kinds. See [threshold](/services/gasless/threshold). You also get the pluggable-signer model: a single account class dispatches to 10 verifier classes (`STARK`, `Ed25519`, `EIP-191`, `EIP-712`, `P-256`, `WebAuthn P-256`, `JWT ES256`, `JWT ES256 Apple Sub`, `BLS12-381`, `SECP256K1`). Browser onboarding can default to passkey-rooted Starknet wallets; EVM and Solana users can keep their existing wallets. See the [passkey API reference](/sdk/passkeys/api) for the browser-side surface. ## Sessions still work If your app relies on SNIP-9 / SNIP-163 session keys (the canonical CHIPI v29 use case), **they work on SHHH V8.4 too.** V8.4 embeds the same SNIP-163 component; `executeTransactionWithSession`, `createSessionKey`, `addSessionKeyToContract`, and `revokeSessionKey` accept both `walletType: "CHIPI"` and `walletType: "SHHH"` wallets transparently as of v14.5.0. ## Three rollback levers If something goes wrong: 1. **Pin your dependency** — keep `@chipi-stack/backend@14.4.x` until you're ready to upgrade. The default flip only affects integrators who actively bump. 2. **Pin `walletType: "CHIPI"`** — pass it explicitly on every `createWallet` call. The legacy v29 path is supported indefinitely. 3. **Server-side kill switch** — `chipi-back` honors a `CHIPI_SHHH_DEFAULT` env var. If your direct callers bypass the SDK and POST to `/chipi-wallets` without `walletType`, ops can force the legacy default back without a redeploy. See [chipi-back PR #267](https://github.com/chipi-pay/chipi-back/pull/267). ## Upgrade walkthrough ### TypeScript / Node ```bash theme={null} pnpm add @chipi-stack/backend@^14.5.0 # or yarn / npm ``` If you want SHHH V8.4 explicitly (the new default): ```ts theme={null} const wallet = await sdk.createWallet({ params: { encryptKey, externalUserId, chain: Chain.STARKNET, // walletType omitted — defaults to SHHH V8.4 STARK }, bearerToken, }); // wallet.walletType === "SHHH" // wallet.signerKind === "STARK" ``` If you want to keep producing legacy CHIPI v29 for now: ```ts theme={null} const wallet = await sdk.createWallet({ params: { encryptKey, externalUserId, chain: Chain.STARKNET, walletType: "CHIPI", // explicit — keeps the legacy default }, bearerToken, }); ``` ### Python The Python package version is independent from the TypeScript fixed-version group. The default flip lands in `chipi-stack@2.1.0` (current floor on PyPI) — same release wave, separate version. ```bash theme={null} uv add 'chipi-stack>=2.1.0' # or pip ``` ```python theme={null} from chipi_sdk import CreateWalletParams, WalletType # Defaults to SHHH V8.4 STARK (signer_kind fills in via the default-flip path). result = await sdk.acreate_wallet( CreateWalletParams( encrypt_key=encrypt_key, external_user_id=external_user_id, ), bearer_token=bearer_token, ) # Or, keep producing CHIPI v29: result = await sdk.acreate_wallet( CreateWalletParams( encrypt_key=encrypt_key, external_user_id=external_user_id, wallet_type=WalletType.CHIPI, ), bearer_token=bearer_token, ) ``` Important Python-specific quirk: explicit `wallet_type=SHHH` WITHOUT `signer_kind` still raises `ValueError` (preserving the original opinionated check — Python has no browser context to default to `WEBAUTHN_P256`). Only the default-flip path (omitting `wallet_type`) falls back to STARK. This is intentional: integrators who explicitly opt into SHHH should pick a signer kind. ### React / Next.js / Expo ```bash theme={null} pnpm add @chipi-stack/chipi-react@^14.5.0 @chipi-stack/nextjs@^14.5.0 # or @chipi-stack/chipi-expo for React Native ``` The `useCreateWallet` hook surface is unchanged. Same default-flip semantics — omit `walletType` to get SHHH V8.4 STARK, pass it explicitly to keep the legacy path. ## Migrating existing wallets The default flip ONLY affects NEW wallets. Existing CHIPI v29 wallets keep working as-is and can be upgraded in place — without changing the wallet's address — via the migration flow. If you want your existing user base to get recovery, multi-device, and threshold, see the [migration guide](/services/gasless/migration). The short version: ```tsx theme={null} import { useMigrateWalletToShhh } from "@chipi-stack/chipi-react"; function MigrateButton({ wallet }) { const { migrateAsync, isLoading } = useMigrateWalletToShhh(); async function onClick() { const encryptKey = await promptForPin(); const outcome = await migrateAsync({ walletId: wallet.id, externalUserId: user.id, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, }, encryptKey, bearerToken, }); // outcome.kind: "MIGRATED" | "ALREADY_MIGRATED" } } ``` This preserves the wallet's address — on-chain history, USDC balance, NFT ownership, external integrators (gift recipients, x402 sellers) don't need to update anything. The class hash flips from CHIPI v29 (`0x053f4f87…f459f2a`) to SHHH V8.4 (`0x075dfb39…fa58a`) atomically with a `bootstrap_from_sessions` call that rebuilds the owner set. ## Other v14.5.0 surfaces The default flip is the headline change. The release also includes: * **`@chipi-stack/chipi-passkey@2.2.0`** — `createShhhPasskey` + `signShhhMessage`: true WebAuthn P-256 signer, NO PRF. The private key never leaves the platform authenticator. Pairs with `signerKind: "WEBAUTHN_P256"` for SHHH wallets. * **x402 SHHH support** — `X402ShhhClient` ships SHHH V8.4 buyers for HTTP 402 pay-per-request APIs. The facilitator detects SHHH payments and routes through the V8.4 dispatch path automatically. * **Sessions on SHHH (TS + Python)** — `executeTransactionWithSession` accepts both CHIPI v29 and SHHH V8.4 wallets. Same 4-felt session signature shape on both. * **Recovery + threshold React hooks** — `useGuardianRecovery`, `useThresholdSign`, and the `` route component for shipping recovery / multi-device / threshold UIs. * **PIN-is-weak warnings** — the canonical security note is now reachable from every onboarding entry point. For the per-package PRs and source of every claim above, see: * [`@chipi-stack/backend` repo PRs labeled v14.5.0](https://github.com/chipi-pay/sdks/pulls?q=label%3Av14.5.0) * [Production mainnet smoke receipts](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts) ## Posture SHHH V8.4 is **available** in v14.5.0 — every code path ships for all 10 signer kinds. The server-side kinds that can run end-to-end on mainnet (STARK, ED25519, EIP-191) have signed live transactions on Starknet mainnet — receipts under [`scripts/receipts/cycle-2/`](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts/cycle-2). Browser-rooted kinds (`WEBAUTHN_P256`) and Apple-Developer-gated kinds (`JWT_ES256_APPLE_SUB`) are smoke-tested with synthesized fixtures matching the on-chain verifier shape; their first real mainnet smoke needs a browser session or Apple Developer credentials respectively. SHHH is **not yet formally advertised** until the external audit closes. Use it in production today; the audit-completion announcement will simply move SHHH from "available" to "recommended" without changing the SDK surface. ## Where to ask questions If something on this page is wrong, ambiguous, or out of sync with what you're seeing in your integration, please open an issue at [`chipi-pay/sdks`](https://github.com/chipi-pay/sdks/issues) with the SDK version you're on and a minimal repro. Every speculative claim above is meant to be traceable to a specific source artifact; if you find one that isn't, that's a doc bug we want to fix. # v14.7.0 — External-rooted SHHH: passkey, MetaMask & Phantom Source: https://docs.chipipay.com/sdk/releases/v14-7-0 What v14.7.0 adds: createWallet deploys SHHH wallets whose first owner is an external key (passkey / MetaMask), and Phantom (Ed25519) can co-sign — no Chipi-held key in either case. v14.7.0 completes external-rooted SHHH signing. A SHHH wallet's first owner can now be a **passkey** (`WEBAUTHN_P256`) or a **MetaMask** key (`EIP191_SECP256K1`) at `createWallet` time — Chipi holds no key — and a **Phantom** wallet (`ED25519`) can co-sign an OutsideExecution from the signature its `signMessage` returns. Additive only: existing STARK and Chipi-held paths are unchanged. ## TL;DR — does this affect me? | Your setup | Action you need to take | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Pinned to `@chipi-stack/backend@14.6.x` (or earlier) | **None.** These are additive APIs; nothing existing changes. | | You create SHHH wallets with `signerKind: "STARK"` (or omit it) | **None.** The Chipi-held STARK path is untouched. | | You want a passkey / MetaMask to be the wallet's first owner | Pass `signerKind: "WEBAUTHN_P256"` / `"EIP191_SECP256K1"` + `ownerPubkeyFelts` to `createWallet` (see below). | | You onboard Phantom signers into a threshold wallet | Use the new `buildEd25519EnvelopeFromSignature` to co-sign. | ## External-rooted `createWallet` (passkey, MetaMask) Before v14.7.0, `createWallet` shipped only `STARK` and `ED25519` and always expected a key Chipi could hold or generate. Now `WEBAUTHN_P256` and `EIP191_SECP256K1` deploy a SHHH wallet whose first owner is an **external** key: you supply the owner's pubkey felts, and no `encryptedPrivateKey` is ever created. ```ts theme={null} // Passkey-as-owner (zero Chipi-held key). The 4 felts come from the // passkey's P-256 pubkey — e.g. via createShhhPasskey + the pubkey helpers. const wallet = await sdk.createWallet({ params: { externalUserId: "treasury-owner-1", chain: Chain.STARKNET, walletType: "SHHH", signerKind: "WEBAUTHN_P256", ownerPubkeyFelts, // [x_low, x_high, y_low, y_high] }, }); // wallet.signerKind === "WEBAUTHN_P256" // wallet.encryptedPrivateKey === null ← Chipi holds nothing ``` The same shape works for `signerKind: "EIP191_SECP256K1"` (MetaMask). The create payload sends `ownerPublicKey` (not `encryptedPrivateKey`) as the credential source; the wallet address is derived from `ownerPubkeyFelts`. `JWT_ES256_APPLE_SUB` is still gated — it needs an Apple-Developer context. ## Phantom co-sign — `buildEd25519EnvelopeFromSignature` Phantom (and any external Ed25519 / Solana wallet) never exposes its private key, so `buildEd25519Envelope` — which requires the raw key — couldn't be used. The new from-signature builder wraps the 64-byte signature Phantom returns into a V2\_SNIP12 envelope, mirroring `buildEip191EnvelopeFromSignature` for MetaMask. ```ts theme={null} import { buildEd25519EnvelopeFromSignature, ed25519SignedBytes, } from "@chipi-stack/backend"; await window.solana.connect(); const publicKey = window.solana.publicKey.toBytes(); // 32-byte pubkey // Sign the EXACT bytes the verifier reconstructs — NOT the raw 32-byte hash. const msg = ed25519SignedBytes(oeHash); const { signature } = await window.solana.signMessage(msg); // 64-byte R‖s const envelope = await buildEd25519EnvelopeFromSignature({ signature, publicKey, messageHash: oeHash, }); ``` Two notes that matter: * **It's async.** Unlike the synchronous EIP-191 builder, the Ed25519 path initializes the Garaga WASM hint builder on first call. `await` it. * **Sign the encoded bytes.** The wallet must sign `ed25519SignedBytes(messageHash)` (the 64-byte hex-ASCII encoding), not the raw hash — the on-chain verifier reconstructs those exact bytes. See [Threshold & co-signing](/services/gasless/threshold) for using this in an N-of-M wallet. # Chat & Streaming Source: https://docs.chipipay.com/services/ai-api/chat-and-streaming General-purpose AI chat with 8 models. Synchronous or real-time streaming via SSE. Base URL: `https://api.chipipay.com/v1` ## When to use each | | `/ai/chat` | `/ai/chat/stream` | | ---------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | **How it works** | Send a prompt, get the full response in one JSON object | Send a prompt, get tokens one-by-one via Server-Sent Events | | **Best for** | Backend agents, batch processing, serverless functions, any case where you process the full response at once | Chatbots, real-time UIs, any interface where users watch the response appear | | **Latency feel** | User waits until the entire response is generated (can be 1-5 seconds) | First token arrives in \~200ms, text streams as it's generated | | **Cost** | Per token | Same per-token cost, debited when stream completes | | **Complexity** | Simple — one HTTP request, one JSON response | Requires SSE parsing (see examples below) | ### What you can build * **Customer support bot** — `/chat/stream` for real-time replies, `/chat` for classification/routing behind the scenes * **Code assistant** — `/chat/stream` so developers see code being written line by line * **Transaction summarizer** — `/chat` to batch-summarize wallet activity on your backend * **AI-powered search** — `/chat` to rerank results or generate answers from your data * **DeFi copilot UI** — `/chat/stream` for the conversational layer, pair with `/think` for portfolio decisions * **Telegram/Discord bot** — `/chat` for simple Q\&A, `/chat/stream` if the platform supports progressive message edits * **Content generation** — `/chat` to generate descriptions, emails, or marketing copy *** ## POST /ai/chat General-purpose AI. Any prompt, any model. Powers chatbots, code assistants, finance apps. **Cost:** Per token (varies by model — see [Models & Pricing](/services/ai-api/models)) ```bash Request theme={null} curl -X POST https://api.chipipay.com/v1/ai/chat \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "haiku", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is USDC?"} ], "max_tokens": 256 }' ``` ```json Response (201) theme={null} { "text": "USDC is a stablecoin pegged 1:1 to the US dollar...", "model": "haiku", "provider": "anthropic", "usage": { "inputTokens": 16, "outputTokens": 47, "totalTokens": 63 }, "cost": { "charged": 0.000352, "currency": "USD" }, "finishReason": "stop", "latencyMs": 1422 } ``` ### Request body | Field | Type | Default | Description | | ------------ | ------ | --------- | --------------------------------------------------------------------------------------------- | | `model` | string | `"haiku"` | Any model from [Models & Pricing](/services/ai-api/models). | | `messages` | array | Required | Array of objects with `role` (`"system"`, `"user"`, or `"assistant"`) and `content` (string). | | `max_tokens` | number | 4096 | Max output tokens. Capped at the model's limit. | ### Response fields | Field | Type | Description | | -------------- | ------ | ------------------------------------------------------------------- | | `text` | string | The model's response text. | | `model` | string | Model ID used (e.g. `"haiku"`, `"gpt-4o-mini"`). | | `provider` | string | `"anthropic"`, `"openai"`, or `"google"`. | | `usage` | object | Token counts: `inputTokens`, `outputTokens`, `totalTokens`. | | `cost` | object | `charged` (USD) and `currency`. | | `finishReason` | string | `"stop"` (completed), `"length"` (hit max\_tokens), or `"unknown"`. | | `latencyMs` | number | Server-side processing time in milliseconds. | ***

POST /ai/chat/stream

Streaming version of `/ai/chat`. Returns **Server-Sent Events (SSE)** with token-by-token text deltas. Ideal for chatbots and real-time UIs. **Cost:** Same as `/ai/chat` — per token, debited after stream completes. Same request body as `/ai/chat`. Same models, same auth. ```bash cURL theme={null} curl -N -X POST https://api.chipipay.com/v1/ai/chat/stream \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "haiku", "messages": [{"role": "user", "content": "Tell me a short joke"}]}' ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.chipipay.com/v1/ai/chat/stream", { method: "POST", headers: { "Authorization": "Bearer sk_prod_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ model: "haiku", messages: [{ role: "user", content: "Tell me a short joke" }], }), }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const data = JSON.parse(line.slice(6)); if (data.error) { throw new Error(`Stream error: ${data.error}`); } else if (data.done) { console.log("\nCost:", data.cost.charged, "USD"); console.log("Tokens:", data.usage.totalTokens); } else { console.log(data.text); // append to your UI } } } ``` ```python Python theme={null} import requests response = requests.post( "https://api.chipipay.com/v1/ai/chat/stream", headers={"Authorization": "Bearer sk_prod_YOUR_KEY"}, json={ "model": "haiku", "messages": [{"role": "user", "content": "Tell me a short joke"}], }, stream=True, ) for line in response.iter_lines(): if not line or not line.startswith(b"data: "): continue import json data = json.loads(line[6:]) if data.get("error"): raise RuntimeError(f"Stream error: {data['error']}") elif data.get("done"): print(f"\nCost: ${data['cost']['charged']}") else: print(data["text"], end="", flush=True) ``` ### SSE event format | Event | Shape | When | | ---------- | --------------------------------------------------------------------------------------- | ---------------------------- | | Text delta | `data: {"text":"token"}` | Each token as it's generated | | Completion | `data: {"done":true,"model":"haiku","provider":"anthropic","usage":{...},"cost":{...}}` | Stream finished | | Error | `data: {"error":"message"}` | Provider error during stream | The completion event includes `model`, `provider`, `usage`, and `cost` — same fields as the synchronous response. Credits are debited when this event fires. *** ## Error handling | HTTP Status | Meaning | | ----------- | ----------------------------------------------------- | | 201 | Success (chat) | | 200 | Success (stream — SSE follows) | | 400 | Invalid request (missing messages, unsupported model) | | 401 | Invalid, inactive, or expired API key | | 402 | Insufficient credits | | 429 | Rate limit exceeded | | 502 | Provider error (model returned an error) | # DeFi Intelligence Source: https://docs.chipipay.com/services/ai-api/defi-intelligence AI portfolio advisor with real-time signals, plus on-chain swap execution on Starknet. Base URL: `https://api.chipipay.com/v1` ## When to use `/ai/think` and `/ai/execute` work together to power autonomous DeFi agents. Think decides **what** to do; Execute builds the **how**. | | `/ai/think` | `/ai/execute` | | ---------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | **What it does** | Analyzes a portfolio with live market signals and returns a decision (hold, buy, sell) | Builds unsigned swap calldata from AVNU (Starknet DEX aggregator) | | **Best for** | Autonomous agents, portfolio advisors, risk-managed bots | Executing the decision — turning a "buy ETH" signal into a real transaction | | **Input** | Portfolio balances + risk score | Token pair + USD amount + wallet address | | **Output** | Structured JSON: action, reason, confidence, risk params | Unsigned transaction calls ready to sign and submit | | **Cost** | Per token (same as /chat) | \$0.002 flat per call | ### What you can build * **Autonomous DeFi agent** — `/think` runs on a schedule (hourly, daily), if it says "buy" → `/execute` builds the swap → your agent signs and submits via session key. Fully automated, gasless. * **Portfolio advisor app** — Show users a dashboard with AI recommendations. `/think` with their real balances + chosen risk level. They review the suggestion and approve manually. * **Risk-managed vault** — A smart contract-backed strategy where `/think` enforces risk params (max drawdown, min stablecoins) before any rebalance. * **Telegram trading bot** — User sends portfolio snapshot, bot calls `/think`, replies with the recommendation and a "execute" button that triggers `/execute`. * **Yield optimizer** — `/think` includes DeFi yield data from DefiLlama. An agent can compare swap gains vs. yield APY and pick the better move. `/think` automatically fetches live signals (RSI, MACD, Fear & Greed, yields) — you don't need to call `/signals` first. Just send the portfolio. *** ## POST /ai/think DeFi portfolio advisor. Analyzes your portfolio with real-time market signals (RSI, MACD, Fear & Greed, yields) and returns a structured JSON decision: **hold**, **buy**, or **sell** — with confidence and reasoning. **Cost:** Per token (same as /chat — the model call is the cost) ```bash Request theme={null} curl -X POST https://api.chipipay.com/v1/ai/think \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "haiku", "portfolio": { "USDC": {"balance": 100, "usd": 100}, "ETH": {"balance": 0.04, "usd": 88}, "STRK": {"balance": 500, "usd": 17} }, "riskScore": 7, "chain": "starknet" }' ``` ```json Response (201) theme={null} { "decision": { "action": "hold", "reason": "ETH RSI at 45 (neutral), Fear & Greed at 16 (Extreme Fear). No signal exceeds minSignalStrength threshold of 0.38.", "confidence": 0.4 }, "riskParams": { "riskScore": 7, "maxSingleTradePercent": 38, "minStablecoinPercent": 15, "maxDrawdownPercent": 29.5, "minSignalStrength": 0.38, "minExpectedGainPercent": 0.57 }, "model": "haiku", "provider": "anthropic", "usage": {"inputTokens": 369, "outputTokens": 102, "totalTokens": 471}, "cost": {"charged": 0.001231, "currency": "USD"}, "latencyMs": 1507 } ``` ### Request body | Field | Type | Default | Description | | ----------- | ------ | ------------ | --------------------------------------------------------------------------------- | | `portfolio` | object | Required | Token balances and USD values. Keys are symbols, values have `balance` and `usd`. | | `riskScore` | number | 5 | 1 (ultra conservative) to 10 (max risk). All thresholds scale with this score. | | `chain` | string | `"starknet"` | Chain for yield data in signals. | | `model` | string | `"haiku"` | Any model from [Models & Pricing](/services/ai-api/models). | ### Risk parameter overrides All optional. Defaults scale linearly with `riskScore`. Override any individual param without affecting others. | Field | Type | Description | | ------------------------ | ------ | ----------------------------------- | | `maxSingleTradePercent` | number | Max % of portfolio per trade | | `minStablecoinPercent` | number | Min % to keep in stablecoins | | `maxDrawdownPercent` | number | Max acceptable drawdown % | | `minSignalStrength` | number | Min signal strength to act (0-1) | | `minExpectedGainPercent` | number | Min expected gain to justify a swap | ### How risk scoring works Risk parameters scale with `riskScore` from conservative (1) to aggressive (10): | Parameter | riskScore = 1 | riskScore = 5 | riskScore = 10 | | ------------------- | ------------- | ------------- | -------------- | | Max single trade | 14% | 30% | 50% | | Min stablecoins | 45% | 25% | 5% | | Max drawdown | 8.5% | 22.5% | 40% | | Min signal strength | 0.74 | 0.50 | 0.20 | | Min expected gain | 1.11% | 0.75% | 0.30% | The `/think` endpoint automatically injects live market data (signals, yields, Fear & Greed) into the AI prompt. You don't need to fetch signals separately — just send the portfolio and risk score. ***

POST /ai/execute

Build unsigned swap calldata from AVNU (Starknet DEX aggregator). Returns the transaction calls — you sign and submit via session key or wallet. **Cost:** \$0.002 per call (flat, not per token) ```bash Request theme={null} curl -X POST https://api.chipipay.com/v1/ai/execute \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "starknet", "action": "swap", "from": "USDC", "to": "ETH", "amountUsd": 10, "walletAddress": "0x0189e2ebb955fc19bf37bd8fc0400de24c123363e7955282bcfb99679a773e61", "slippage": 0.005 }' ``` ```json Response (201) theme={null} { "calls": [ { "contractAddress": "0x033068f6...", "entrypoint": "approve", "calldata": ["0x...", "0x..."] }, { "contractAddress": "0x042ab...", "entrypoint": "multi_route_swap", "calldata": ["0x...", "0x..."] } ], "quote": { "sellToken": "USDC", "buyToken": "ETH", "sellAmount": "10000000", "buyAmount": "4500000000000000", "sellAmountUsd": 10.0, "buyAmountUsd": 9.98, "priceImpactPct": 0.12, "route": "AVNU best route" }, "cost": {"charged": 0.002, "currency": "USD"} } ``` ### Request body | Field | Type | Default | Description | | --------------- | ------ | -------- | ----------------------------------------------------------------- | | `chain` | string | Required | `"starknet"` (more chains coming soon). | | `action` | string | Required | `"swap"` (more actions coming soon). | | `from` | string | Required | Sell token symbol: USDC, ETH, STRK, WBTC, USDT, DAI. | | `to` | string | Required | Buy token symbol: USDC, ETH, STRK, WBTC, USDT, DAI. | | `amountUsd` | number | Required | USD amount to swap (converted to token amount using live prices). | | `walletAddress` | string | Required | Your Starknet wallet address. | | `slippage` | number | 0.005 | Slippage tolerance. Default: 0.5%. Max: 50%. | `/execute` returns **unsigned calls**. It does NOT submit the transaction. You sign with your wallet or session key and submit via the Chipi paymaster (gasless). Swaps with more than 3% price impact are rejected. ### Putting it together: Think + Execute A typical DeFi agent flow: 1. **Think** — send portfolio + risk score, get a decision 2. If decision is `buy` or `sell` → **Execute** — get unsigned swap calldata 3. Sign the calls with your wallet or session key 4. Submit via Chipi's gasless paymaster ```typescript theme={null} // 1. Get AI decision const decision = await fetch("https://api.chipipay.com/v1/ai/think", { method: "POST", headers: { "Authorization": "Bearer sk_prod_YOUR_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ portfolio: { USDC: { balance: 100, usd: 100 }, ETH: { balance: 0.04, usd: 88 } }, riskScore: 7, }), }).then(r => r.json()); // 2. If actionable, build swap calldata if (decision.decision.action !== "hold") { const swap = await fetch("https://api.chipipay.com/v1/ai/execute", { method: "POST", headers: { "Authorization": "Bearer sk_prod_YOUR_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ chain: "starknet", action: "swap", from: "USDC", to: "ETH", amountUsd: 10, walletAddress: "0x...", }), }).then(r => r.json()); // 3. Sign swap.calls with your wallet and submit console.log("Calls to sign:", swap.calls); } ``` # Models & Pricing Source: https://docs.chipipay.com/services/ai-api/models 8 AI models across 3 providers. Pay per token with USDC credits. ## Available models All prices are per 1 million tokens (USD). Cost = input tokens + output tokens at the rates below. Pricing is always available live at [`GET /ai/models`](https://api.chipipay.com/v1/ai/models). If there is ever a discrepancy between this page and the API response, the API is the source of truth. | Model ID | Provider | Display Name | Input / 1M tokens | Output / 1M tokens | Best for | | -------------- | --------- | ----------------- | ----------------- | ------------------ | -------------------------------------------- | | `gpt-4o-mini` | OpenAI | GPT-4o Mini | \$0.21 | \$0.84 | Cheapest. Simple tasks, high volume. | | `gemini-flash` | Google | Gemini 2.5 Flash | \$0.42 | \$3.50 | Fast, good quality, low cost. | | `gpt-4.1-mini` | OpenAI | GPT-4.1 Mini | \$0.56 | \$2.24 | Better reasoning at low cost. | | `haiku` | Anthropic | Claude Haiku 4.5 | \$1.40 | \$7.00 | **Default.** Best reasoning-to-cost ratio. | | `gemini-pro` | Google | Gemini 2.5 Pro | \$1.75 | \$14.00 | Strong reasoning. **2 RPM** free-tier limit. | | `gpt-4.1` | OpenAI | GPT-4.1 | \$2.80 | \$11.20 | High quality, fast. | | `gpt-4o` | OpenAI | GPT-4o | \$3.50 | \$14.00 | Multimodal, proven. | | `sonnet` | Anthropic | Claude Sonnet 4.5 | \$4.20 | \$21.00 | Premium. Best quality, highest cost. | Start with `gpt-4o-mini` for prototyping (\$0.21/1M input). Switch to `haiku` or `sonnet` for production quality. ## Typical costs per call | Use case | Model | Tokens | Cost | | ----------------------- | ------------- | ------- | ------------------------------------------------- | | Short chat reply | `gpt-4o-mini` | \~500 | \~\$0.0003 | | DeFi portfolio decision | `haiku` | \~500 | \~\$0.004 | | Detailed analysis | `sonnet` | \~1,000 | \~\$0.013 | | Swap calldata builder | `/execute` | N/A | \$0.002 flat | | Token prices | `/prices` | N/A | **Free** | | Market signals | `/signals` | N/A | **Free** up to 100/mo per org, then \$0.0005/call | ## Rate limits | Model | Provider RPM | Max output tokens | | -------------- | -------------- | ----------------- | | `gpt-4o-mini` | 30,000 | 16,384 | | `gpt-4.1-mini` | 10,000 | 16,384 | | `gpt-4.1` | 10,000 | 32,768 | | `gpt-4o` | 10,000 | 16,384 | | `haiku` | 4,000 | 8,192 | | `sonnet` | 1,000 | 8,192 | | `gemini-flash` | 15 (free tier) | 8,192 | | `gemini-pro` | 2 (free tier) | 8,192 | Your org's API key also has a per-key rate limit (default: 1,000 RPM). The effective limit is the lower of the two. Google Gemini models are on the free tier (low RPM). For production use at scale, prefer OpenAI or Anthropic models. `gemini-pro` in particular is rate-limited to **2 RPM** — fine for occasional reasoning calls, not production traffic. ## Credits Buy credit packs by sending USDC on Starknet: | Pack | Price | | ----- | -------- | | \$2 | 2 USDC | | \$5 | 5 USDC | | \$25 | 25 USDC | | \$50 | 50 USDC | | \$100 | 100 USDC | Buy at [dashboard.chipipay.com/configure/billing](https://dashboard.chipipay.com). Credits are USD-denominated and deducted per API call. Check your balance in the dashboard billing tab. ## OpenAPI spec The full API spec is available at [`/v1/openapi.json`](https://api.chipipay.com/v1/openapi.json) — filtered to AI endpoints only. # AI API Source: https://docs.chipipay.com/services/ai-api/overview 8 models from Anthropic, OpenAI, and Google behind one endpoint. Pay per token in USDC credits, with a sandbox for DEV keys that never calls the provider. The AI API is **live in production**. One HTTP endpoint, 8 models across 3 providers, USDC-credits billing with a 40% margin baked in at the endpoint layer. Smoke-tested 2026-05-20 against all 3 providers; full code-traced verification trail at the bottom of this page. ## At a glance Across Anthropic, OpenAI, Google chat, think, execute, models, usage, chat/stream USDC credits, no monthly plan, no minimum `pk_dev_` returns canned responses, never calls the provider ## Models The model registry lives in chipi-back at `src/ai/model-router.service.ts` and is exposed live at `GET /v1/ai/models` (with the 40% margin already applied to the per-1M-token prices). Pull from the endpoint rather than hardcoding the table below — pricing can change without a docs deploy. | ID | Display name | Use case | | -------- | ----------------- | ----------------------- | | `haiku` | Claude Haiku 4.5 | Fast, cheap default | | `sonnet` | Claude Sonnet 4.5 | Heavy reasoning, agents | | ID | Display name | Use case | | -------------- | ------------ | --------------------- | | `gpt-4o-mini` | GPT-4o Mini | Cheapest in catalog | | `gpt-4o` | GPT-4o | Multimodal, fast | | `gpt-4.1-mini` | GPT-4.1 Mini | Long context, budget | | `gpt-4.1` | GPT-4.1 | Long context, premium | | ID | Display name | Use case | | -------------- | ---------------- | ---------------- | | `gemini-flash` | Gemini 2.5 Flash | Fast, low cost | | `gemini-pro` | Gemini 2.5 Pro | Strong reasoning | If `model` is omitted from the request body, Chipi defaults to `haiku` (`DEFAULT_MODEL` in the same source file). ## Endpoints All endpoints live under `/v1/ai/*` on `https://api.chipipay.com` and are guarded by `ApiKeyGuard`, which accepts either: * `Authorization: Bearer sk_prod_...` (server-to-server), or * `x-api-key: pk_prod_...` (client-side, no secret leaked) | Method + path | Purpose | | ------------------------- | ---------------------------------------------------------------------------------- | | `POST /v1/ai/chat` | General-purpose chat completion | | `POST /v1/ai/chat/stream` | Same as `/chat`, server-sent-event token stream | | `POST /v1/ai/think` | DeFi-specialised decision: returns a structured JSON `{ action, confidence, ... }` | | `POST /v1/ai/execute` | Build unsigned AVNU swap calldata (Starknet). Pure builder, doesn't submit. | | `GET /v1/ai/models` | Lists all 8 models with live margin-applied per-1M-token pricing | | `GET /v1/ai/usage` | Your org's call counts, costs, and breakdowns per model + endpoint | See the [Quickstart](/services/ai-api/quickstart) for a working `curl` against `/chat`, [Chat & Streaming](/services/ai-api/chat-and-streaming) for the SSE wire format, and [DeFi Intelligence](/services/ai-api/defi-intelligence) for the `/think` JSON schema. ## How billing works Every `/v1/ai/chat` and `/v1/ai/chat/stream` response includes a `cost.charged` field expressed in USD. That's the amount debited from your org's Chipi credits balance for the call. The math (from `ai.controller.ts`): ``` chargedAmount = (inputTokens × inputCostPer1M / 1_000_000 + outputTokens × outputCostPer1M / 1_000_000) × 1.4 ``` * `inputCostPer1M` / `outputCostPer1M` come from the model registry (per-provider wholesale). * `1.4` is the `MARGIN` constant — Chipi adds 40% on top. * `/execute` charges a flat `$0.002` per call regardless of size (AVNU API call, no token usage). You see the wholesale-vs-charged split nowhere because we only expose the charged number. The 40% covers infra, paymaster gas for `/execute`, and platform margin. ## DEV sandbox If your API key starts with `pk_dev_` or `sk_dev_`, the AI endpoints short-circuit to canned responses and **never call the underlying provider**. | Behaviour | DEV (`pk_dev_*` / `sk_dev_*`) | PROD (`pk_prod_*` / `sk_prod_*`) | | --------------------------- | ----------------------------- | -------------------------------- | | Provider call | None | Real Anthropic / OpenAI / Google | | Response shape | Identical to production | Real model output | | `cost.charged` | `0` | Real per-token cost × 1.4 margin | | `OrgBalance` | Untouched | Debited | | `LedgerEntry` row | Synthetic `le-dev-*` | Real DB row | | `sandbox: true` on response | Yes | Field absent | | Rate limit + balance check | Skipped | Enforced | Use DEV keys to wire up the integration end-to-end against the real response shape, then swap the prefix to `pk_prod_` / `sk_prod_` to go live. Zero code changes. ## Live smoke (2026-05-20) Real production calls from `dashboard.chipipay.com` admin showing all three providers respond with sane costs. Each row is a single `POST /v1/ai/chat` with `max_tokens: 20` to one phrase: | Model | Provider | Latency | Output | Charged (USD) | | -------------- | --------- | ------- | ----------- | ------------- | | `haiku` | Anthropic | 691 ms | "pong" | \$0.000056 | | `gemini-flash` | Google | 673 ms | "pong" | \$0.000024 | | `gemini-pro` | Google | 1072 ms | (truncated) | \$0.000112 | | `gpt-4o-mini` | OpenAI | 1870 ms | "Pong" | \$0.000006 | A Legaria-org dashboard pull on the same day showed 11 production calls (8 haiku, 1 each of the other three) totaling **\$0.0069** charged for **3.4K total tokens**. Numbers match the per-call costs above to within Math.round. ## Quirks worth knowing 1. **OpenAI minimum output tokens is 16.** The controller clamps `max_tokens` upward to `config.minOutputTokens` per model so a request with `max_tokens: 10` against `gpt-4o-mini` doesn't 400. Anthropic and Google accept any positive integer. Source: `model-router.service.ts` `minOutputTokens` field, applied via `clampMaxTokens(...)` in `ai.controller.ts`. 2. **`/think` truncates if the model gets verbose.** It hardcodes `max_tokens: 1024` because the endpoint expects a single structured JSON decision, not free-form text. If you need more, use `/chat` with your own system prompt. 3. **`max_tokens` is validated `@IsInt() @Min(1)` at the DTO layer.** Fractional or non-positive values get a clean 400 before the request leaves Nest. 4. **No webhook for AI events.** Every response is the immediate request return; no async settlement, no `ai.completion` webhook. Different from Bills. ## Where to go next * **[Quickstart](/services/ai-api/quickstart)** — first `/v1/ai/chat` call in 30 seconds, no SDK. * **[Chat & Streaming](/services/ai-api/chat-and-streaming)** — SSE format, `chat/stream` examples. * **[DeFi Intelligence](/services/ai-api/defi-intelligence)** — `/think` JSON schema, risk-param defaults. * **[Prices & Data](/services/ai-api/prices-and-data)** — token-price + market-signal endpoints feeding `/think`. * **[Models](/services/ai-api/models)** — model-by-model pricing + RPM limits. # Prices & Data Source: https://docs.chipipay.com/services/ai-api/prices-and-data Real-time token prices, technical signals, and DeFi yields. 90 tokens, 11 chains, 60 fiat currencies. Base URL: `https://api.chipipay.com/v1` ## When to use | | `/ai/prices` | `/ai/signals` | | ---------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- | | **What it does** | Real-time token prices with market data (24h change, volume, ATH, market cap) | Technical indicators (RSI, MACD, Bollinger) + sentiment + DeFi yields | | **Best for** | Wallets, portfolio trackers, price displays, fiat conversion | Trading bots, AI agents, technical analysis dashboards | | **Cost** | **Free** | **Free** up to 100/mo per org, then \$0.0005/call | | **Refresh rate** | Every 30 seconds | Indicators every 60s, Fear & Greed every 30 min, yields every 5 min | | **Coverage** | 90 tokens, 11 chains, 60 fiat currencies | Per-token signals + global sentiment + chain-filtered yields | ### What you can build * **Crypto wallet** — Show balances in local currency. `/prices?tokens=ETH,USDC&fiat=MXN` gives you everything: price, 24h change, market cap, and MXN conversion rate. Free, no credit cost. * **Portfolio tracker** — `/prices` for all token values + `/prices/currencies` for fiat conversion. Update every 30 seconds. * **Price alert bot** — Poll `/prices` and notify users when a token crosses a threshold. * **Trading dashboard** — `/signals?tokens=ETH,BTC` for RSI, MACD, Bollinger Bands, trend classification, and signal strength. * **DeFi yield aggregator** — `/signals?chain=starknet` returns top DeFi pools with APY and TVL from DefiLlama. * **Market sentiment widget** — `/signals` includes the Fear & Greed Index (0-100) with a human-readable label. * **Multi-chain price API** — One call covers Ethereum, Starknet, Solana, Base, Arbitrum, Polygon, and 5 more chains. Filter with `?chain=` to get only the tokens relevant to your app. *** ## GET /ai/prices Real-time token prices for **90 tokens** across **11 chains**. Refreshed every 30 seconds from CoinGecko. **Free — no credit debit.** ```bash Request theme={null} curl "https://api.chipipay.com/v1/ai/prices?chain=starknet&tokens=ETH,STRK&fiat=MXN" \ -H "x-api-key: pk_prod_YOUR_KEY" ``` ```json Response (200) theme={null} { "tokens": { "ETH": { "price": 2223.98, "change1h": 0.23, "change24h": 2.71, "change7d": 7.63, "high24h": 2234.95, "low24h": 2160.40, "marketCap": 268231561389, "marketCapRank": 2, "volume24h": 16297316533, "ath": 4946.05, "athDate": "2025-08-24", "circulatingSupply": 120691091, "image": "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png", "chains": ["ethereum", "starknet", "base", "arbitrum", "optimism", "polygon", "bnb", "avalanche"] } }, "fiat": { "rates": { "MXN": {"rate": 17.31, "name": "Mexican Peso", "unit": "MX$"} }, "source": "coingecko/exchange_rates", "updatedAt": "2026-04-10T08:25:00Z" }, "meta": { "totalTokens": 2, "source": "coingecko", "refreshInterval": 30, "updatedAt": "2026-04-10T08:30:15Z" } } ``` ### Query parameters | Param | Type | Description | | ---------- | ------ | ------------------------------------------------------------------------------------------------------ | | `tokens` | string | Filter by symbol: `ETH,BTC,STRK`. Omit for all 90 tokens. | | `chain` | string | Filter by chain (see supported chains below). | | `category` | string | Filter: `core`, `stablecoin`, `defi`, `btc-variant`, `yield`, `meme`, `rwa`, `l1`, `l2`, `governance`. | | `fiat` | string | Include fiat rates: `MXN,COP,BRL` or `all` for 60 currencies. | To compute a price in local currency: `ETH in MXN = 2223.98 x 17.31 = MX$38,497`. ### Related endpoints | Endpoint | Description | Cost | | --------------------------- | -------------------------------------------------- | ---- | | `GET /ai/prices/chains` | List supported chains with token counts | Free | | `GET /ai/prices/currencies` | List all 60 fiat currencies with names and symbols | Free | *** ## Supported chains 11 chains tracked. Each token lists which chains it's available on. | Chain | Examples | | ----------- | -------------------------------------------- | | `ethereum` | ETH, USDC, WBTC, UNI, AAVE, LINK | | `starknet` | ETH, STRK, USDC, WBTC, USDT, DAI, LBTC, tBTC | | `base` | ETH, USDC, cbBTC, AERO | | `arbitrum` | ETH, ARB, USDC, GMX, PENDLE | | `optimism` | ETH, OP, USDC, VELO | | `polygon` | MATIC, USDC, AAVE, QI | | `solana` | SOL, JUP, RAY, BONK, WIF, JTO | | `bnb` | BNB, CAKE, XVS | | `avalanche` | AVAX, JOE, QI | | `sui` | SUI, DEEP, CETUS | | `ton` | TON, STON | *** ## Token categories | Category | Tokens | Description | | ------------- | -------------------------------------- | ----------------------------- | | `core` | ETH, BTC, SOL, BNB, AVAX, SUI, TON... | Major L1 native tokens | | `stablecoin` | USDC, USDT, DAI | USD-pegged stablecoins | | `defi` | UNI, AAVE, LINK, MKR, CRV, PENDLE... | DeFi protocol tokens | | `btc-variant` | WBTC, LBTC, tBTC, SolvBTC, cbBTC, eBTC | BTC wrapped/bridged variants | | `yield` | stETH, wstETH, rETH, cbETH, SFRAX | Liquid staking / yield tokens | | `meme` | DOGE, SHIB, PEPE, BONK, WIF, FLOKI | Community/meme tokens | | `rwa` | ONDO | Real-world asset tokens | | `l1` | NEAR, FTM, SEI, APT, INJ, TIA | Alternative L1 chains | | `l2` | STRK, ARB, OP, MATIC, IMX, MANTA | L2 scaling tokens | | `governance` | AAVE, MKR, UNI, CRV, COMP | Protocol governance tokens | ***

GET /ai/signals

Technical indicators, market sentiment, and DeFi yields. Computed from 30-second price snapshots. **Cost:** Free up to 100 calls per org per calendar month (UTC). Beyond that, \$0.0005 per call debited from your Chipi credits. DEV-environment API keys (`pk_dev_*` / `sk_dev_*`) are always free and don't count against the quota. ```bash Request theme={null} curl "https://api.chipipay.com/v1/ai/signals?chain=starknet&tokens=ETH" \ -H "x-api-key: pk_prod_YOUR_KEY" ``` ```json Response (200) theme={null} { "tokens": { "ETH": { "rsi14": 28.3, "macd": {"value": -12.5, "signal": -8.2, "histogram": -4.3}, "bollingerBands": {"upper": 2620, "middle": 2510, "lower": 2400}, "ema": {"ema20": 2495, "ema50": 2510, "ema200": null}, "trend": "oversold", "signalStrength": 0.82 } }, "market": { "fearGreedIndex": 16, "fearGreedLabel": "Extreme Fear" }, "yields": [ {"pool": "STRK", "protocol": "endur", "chain": "Starknet", "apy": 7.13, "tvlUsd": 3131873}, {"pool": "ETH", "protocol": "nostra-money-market", "chain": "Starknet", "apy": 1.83, "tvlUsd": 2510408} ], "definitions": { "rsi14": "Relative Strength Index (14 periods). <30 = oversold, >70 = overbought.", "signalStrength": "0-1 composite. >0.6 = actionable signal. <0.3 = noise.", "fearGreedIndex": "0-100. 0-25 = Extreme Fear (buy zone). 75-100 = Extreme Greed (sell zone)." }, "meta": { "sources": ["coingecko", "alternative.me", "defillama"], "snapshotsUsed": 100, "updatedAt": "2026-04-10T08:30:15Z" } } ``` ### Query parameters | Param | Type | Description | | -------- | ------ | -------------------------------- | | `tokens` | string | Filter by symbol: `ETH,BTC,STRK` | | `chain` | string | Filter yields by chain | ### Signal definitions | Indicator | Source | Refresh | Description | | ------------------------- | --------------------------- | ------- | -------------------------------------------------------------------------- | | RSI-14 | Computed from 30s snapshots | 60s | Relative Strength Index. Below 30 = oversold, above 70 = overbought. | | MACD (12,26,9) | Computed from 30s snapshots | 60s | Moving Average Convergence Divergence. Histogram shows momentum direction. | | Bollinger Bands (20,2) | Computed from 30s snapshots | 60s | Price bands. Price near lower band = potential bounce. | | EMA-20 / EMA-50 / EMA-200 | Computed from 30s snapshots | 60s | Exponential moving averages. EMA-200 requires \~7 days of data. | | Signal Strength | Computed from RSI + MACD | 60s | 0-1 composite. Above 0.6 = actionable signal. | | Fear & Greed Index | alternative.me | 30 min | 0-100 market sentiment. 0-25 = Extreme Fear, 75-100 = Extreme Greed. | | DeFi Yields | DefiLlama | 5 min | APY and TVL for top pools per chain. | Signals need \~7 minutes of price data after server start. If you get a 503, wait and retry. # Quickstart Source: https://docs.chipipay.com/services/ai-api/quickstart Get an AI response in 30 seconds. No SDK needed — just HTTP. ## 1. Get your API key Go to [dashboard.chipipay.com](https://dashboard.chipipay.com), create an org, and copy your API key from the Credentials page. ## 2. Make your first call ```bash cURL theme={null} curl -X POST https://api.chipipay.com/v1/ai/chat \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "What is Starknet?"}] }' ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.chipipay.com/v1/ai/chat", { method: "POST", headers: { "Authorization": "Bearer sk_prod_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: "What is Starknet?" }], }), }); const data = await response.json(); console.log(data.text); // → "Starknet is a Layer 2 scaling solution for Ethereum..." console.log(data.cost.charged); // → 0.00001 (USD) ``` ```python Python theme={null} import requests response = requests.post( "https://api.chipipay.com/v1/ai/chat", headers={"Authorization": "Bearer sk_prod_YOUR_KEY"}, json={ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "What is Starknet?"}], }, ) data = response.json() print(data["text"]) print(f"Cost: ${data['cost']['charged']}") ``` ## 3. Stream a response For chatbots and real-time UIs, use the streaming endpoint — same request body, token-by-token delivery via SSE: ```bash cURL theme={null} curl -N -X POST https://api.chipipay.com/v1/ai/chat/stream \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "haiku", "messages": [{"role": "user", "content": "Tell me a joke"}]}' ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.chipipay.com/v1/ai/chat/stream", { method: "POST", headers: { "Authorization": "Bearer sk_prod_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ model: "haiku", messages: [{ role: "user", content: "Tell me a joke" }], }), }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const data = JSON.parse(line.slice(6)); if (data.done) { console.log("Cost:", data.cost.charged, "USD"); } else { console.log(data.text); // append to your UI } } } ``` ## 4. Buy credits AI calls debit from your org's credit balance. Buy a credit pack ($2 / $5 / $25 / $50 / \$100) at [dashboard.chipipay.com/configure/billing](https://dashboard.chipipay.com/configure/billing) by sending USDC. \$5 gets you **\~50,000 calls** with `gpt-4o-mini` or **\~700 calls** with `haiku`. ## Auth Two options — both work on all AI endpoints, no JWKS setup needed: | Method | Header | Best for | | ---------- | ----------------------------------- | ----------------------------- | | Secret key | `Authorization: Bearer sk_prod_...` | Server-side (backend, agent) | | Public key | `x-api-key: pk_prod_...` | Client-side (browser, mobile) | Never put `sk_` keys in client-side code. Use `pk_` for browser/mobile apps. ## What you can build | Use case | Endpoint | Cost | | ------------------------- | ----------------------------------------------------------------------- | ------------------------------------- | | Chatbot / code assistant | [`POST /ai/chat`](/services/ai-api/chat-and-streaming) | Per token | | Real-time streaming UI | [`POST /ai/chat/stream`](/services/ai-api/chat-and-streaming#streaming) | Per token | | DeFi portfolio advisor | [`POST /ai/think`](/services/ai-api/defi-intelligence) | Per token | | Swap execution | [`POST /ai/execute`](/services/ai-api/defi-intelligence#execute-a-swap) | \$0.002/call | | Token prices for wallets | [`GET /ai/prices`](/services/ai-api/prices-and-data) | Free | | Market signals for agents | [`GET /ai/signals`](/services/ai-api/prices-and-data#technical-signals) | Free up to 100/mo, then \$0.0005/call | ## Next * [Chat & Streaming](/services/ai-api/chat-and-streaming) — full request/response reference * [DeFi Intelligence](/services/ai-api/defi-intelligence) — portfolio advisor + swap execution * [Prices & Data](/services/ai-api/prices-and-data) — 90 tokens, 11 chains, 60 currencies * [Models & Pricing](/services/ai-api/models) — all models, providers, and rate limits # Billing & Credits Source: https://docs.chipipay.com/services/billing/overview How Chipi credits work — what consumes them, how to top up, and where to monitor balance, holds, and the ledger. Chipi runs on a **prepaid credit balance** that lives at the org level. You top up by sending USDC to a payment address; service calls (bill payments, gift cards, AI API) deduct credits as they happen. Everything is visible from the **Billing** tab in the dashboard. Billing is managed entirely from the dashboard at `/configure/billing` — there is no SDK surface to call `/treasury/*` from your app. Credit consumption is **automatic**: when your code calls `purchaseSku`, redeems a gift card, or hits the AI API, the matching debit lands in your ledger server-side. This page explains the model so you know what you're being charged for and how to keep the balance topped up. ## The credit model * **1 credit = 1 USD.** Internally tracked as USDC on Starknet at \$1 parity. * Each org has one **OrgBalance** with two numbers: `totalUsd` (gross deposits) and `availableUsd` (deposits − pending holds − consumed). Each Chipi service that draws on prepaid credits writes one or more `LedgerEntryType` rows so you can audit exactly what was consumed when. Gift cards span three entries (the hold / redeem / release lifecycle); other services map to a single dedicated entry. | Service (`ServiceType`) | `LedgerEntryType` | Triggered by | | ----------------------- | -------------------------------------------------------------- | --------------------------------------------------------- | | `SKU_PURCHASE` | `SKU_MARGIN` | `purchaseSku()` settles successfully | | `GIFT_CARD` | `GIFT_HOLD` → `GIFT_REDEEM` (or `GIFT_HOLD_RELEASE` on cancel) | Gift creation reserves credits; redeem / cancel finalises | | `AI_API` | `AI_API_CALL` | Each `/v1/ai/chat`, `/think`, `/execute` call | | `GAS_SUBSIDY` | `GAS_SUBSIDY` | Paymaster covers a tx your wallet didn't pay for | ### Services with a different billing model Two shipped services don't draw down `availableUsd` from your prepaid credits — they settle differently: * **x402 protocol payments** ([`@chipi-stack/x402`](https://www.npmjs.com/package/@chipi-stack/x402), facilitator at [x402.chipipay.com](https://x402.chipipay.com)) — settles **per call** via the x402 protocol on Starknet. The end user pays the merchant directly through the facilitator; nothing flows through `OrgBalance`. See the **x402 Payments** dropdown for the full integration guide. The `ServiceType.X402` enum value is reserved for any future flows that *do* route through credits (e.g., facilitator fees), but no runtime code uses it today. * **Pay-with-crypto button** (`@chipi-stack/backend` + a UI component shipped in the dashboard) — a checkout primitive that sends USDC straight to the merchant wallet. Same model: direct on-chain settlement, no credit debit. The `ServiceType.PAY_BUTTON` enum value is reserved. `ServiceType.KYC_VERIFICATION` and `ServiceType.ADDRESS_SCREENING` are reserved for compliance services not yet shipped. When they go live, the service entry above will be updated. Top-ups land as `DEPOSIT`. Adjustments (manual reconciliation), `REFUND`, `REV_SHARE_CREDIT/PAYOUT`, `SETTLEMENT` (catch-all), and `WITHDRAWAL` round out the `LedgerEntryType` vocabulary. ## Credit packs There are five purchasable packs. Each ships with a number of free gift-card credits as a bundled bonus. | Pack | Amount | Free gift cards | Tagline | | ---------- | -------- | --------------- | ---------------- | | Tester | \$2 | 1 | Learn first | | Starter | \$5 | 2 | Try it out | | **Growth** | **\$25** | **10** | **Most popular** | | Scale | \$50 | 25 | | | Pro | \$100 | 50 | | The packs are returned by `GET /treasury/packs` and rendered in the dashboard's Billing tab. ## How top-up works ``` ┌──────────────┐ 1. register address ┌────────────────────┐ │ Your wallet │ ─────────────────────▶│ POST /treasury/ │ │ (Starknet) │ │ register-sender │ └──────────────┘ └────────────────────┘ │ │ 2. send USDC ▼ ┌────────────────────────────────────┐ │ Chipi credits address │ │ (CHIPI_CREDITS_ADDRESS env var) │ └────────────────────────────────────┘ │ │ 3. background detector matches deposit │ to a registered sender → org ▼ ┌────────────────────────────────────┐ │ OrgBalance += deposit │ │ LedgerEntry { type: DEPOSIT } │ └────────────────────────────────────┘ ``` 1. **Register your sender wallet** so the deposit detector knows which org gets credit. The dashboard's Billing tab does this when you select a credit pack — it `POST`s to `/treasury/register-sender` with your Starknet address. 2. **Send USDC** from that address to the Chipi credits address (revealed in the dashboard after registration). 3. **Wait \~2 minutes.** The detector runs every 2 min on Trigger.dev. The dashboard polls for up to 3 min after a purchase before timing out the optimistic UI; the actual credit lands as soon as the next detector cycle catches it. You can re-register the same address (it's an upsert), and one org can have multiple registered senders. ## Holds — the gift card pattern Some services use a **hold-then-consume** pattern instead of a flat debit. It protects against partial failures and over-consumption when an action takes time to settle: 1. **Hold** — when the action starts, Chipi reserves an estimated max cost (`POST /treasury/hold`). The held amount drops `availableUsd` immediately so concurrent calls see the correct headroom. Lands in the ledger as `GIFT_HOLD`. 2. **Consume** — when the action finishes, the actual cost gets `consume-hold`'d. Only the actual amount is debited; any unused reservation is freed. Lands as `GIFT_REDEEM`. 3. **Release** — if the action errors out before completing, the entire hold is `release-hold`'d (full refund, no debit). Lands as `GIFT_HOLD_RELEASE`. **Currently only Gifts use this pattern.** Bill payments use a flat `SKU_MARGIN` debit at settlement; AI API uses an up-front `checkBalance` + a final `AI_API_CALL` debit (no hold lifecycle, no separate ledger lines for in-flight calls). The hold / consume / release primitives are general-purpose and would suit any future long-running flow (e.g., streaming AI calls), but no other service uses them today. You don't call `/treasury/hold` directly — the service that needs it does so server-side. ## Monitoring In the dashboard at **`/configure/billing`** you get: * **Balance card** — `totalUsd` deposited, `availableUsd` after holds/debits, status (`OK` / `LOW` / `CRITICAL` / `SUSPENDED`). * **Ledger table** — paginated `GET /treasury/ledger`, filterable by `type` (any `LedgerEntryType`) and date range. * **Wallet address** — your treasury wallet, one per org. Created on demand by `POST /treasury/create-wallet`. * **Credit packs** — the catalog above plus a purchase modal that handles registration + payment-address reveal. You can configure two thresholds via `PATCH /treasury/settings`: * `lowBalanceThresholdUsd` — when `availableUsd` drops below this, the balance card shows `LOW`. Wire to a webhook for alerting (coming soon). * `criticalThresholdUsd` — same but at `CRITICAL`. When you hit zero, the org is `SUSPENDED` and service calls reject with a credit-insufficient error until you top up. ## What this means for your code **Devs writing app code** don't call `/treasury/*`. Instead: * Sufficient credits → service calls succeed and the debit lands silently in the ledger. * Insufficient credits → calls reject with a credit-insufficient error (you'll see it as a `ChipiApiError` from the SDK; check `.code` for the specific reason). So the right pattern in your backend is to **catch the error, surface it to the user, and prompt them (or you) to top up** — not to pre-check `/treasury/balance` before every call. See [Error Handling](/services/gasless/errors) for the catch patterns. ## Where each thing lives | Capability | Dashboard URL | Endpoint | | ------------------ | -------------------------------- | ----------------------------------------------------------- | | Balance + status | `/configure/billing` | `GET /treasury/balance` | | Ledger / activity | `/configure/billing` | `GET /treasury/ledger` | | Buy credit pack | `/configure/billing` (Buy modal) | `GET /treasury/packs` + `POST /treasury/register-sender` | | Treasury wallet | `/configure/billing` | `GET /treasury/wallet-info`, `POST /treasury/create-wallet` | | Threshold settings | `/configure/billing` | `PATCH /treasury/settings` | > ✅ Endpoint contracts, credit pack catalog, and `LedgerEntryType` / `ServiceType` enums verified against the live `/treasury/*` API on **2026-05-10**. # Bill Payments — Node.js Source: https://docs.chipipay.com/services/bills/node Browse the SKU catalog, charge your Chipi credits balance for a bill payment, and poll for settlement. Pure REST + customer JWT — no end-user wallet, no on-chain signing. This page shows the full purchase flow — browse catalog → debit credits → poll for settlement — using the public REST API and `@chipi-stack/backend` for the read-only calls. The purchase itself is a plain HTTP POST because the SDK's `purchaseSku` is wired to an internal wallet-pays-on-chain path that doesn't apply to API-key integrations. ## Install You only need the SDK for the read calls (`getSkuList`, `getSku`, `getSkuPurchase`). The purchase POST is plain `fetch`, so you can skip the SDK entirely if you want zero dependencies. ```bash npm theme={null} npm install @chipi-stack/backend ``` ```bash pnpm theme={null} pnpm add @chipi-stack/backend ``` ```bash yarn theme={null} yarn add @chipi-stack/backend ``` ## Initialise the SDK ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); ``` ## Resolve a customer JWT Every bill purchase requires `Authorization: Bearer ` in addition to the `x-api-key` header. The JWT is issued by *your* auth provider — Chipi validates it against the JWKS rule you configured for this API key (typically `https://clerk.yourdomain.com/.well-known/jwks.json`). In a server-side flow, you'd typically receive the JWT from your authenticated session for the user making the purchase: ```ts theme={null} // however you obtain a signed session token for the requesting user — // e.g. from Clerk's server SDK, NextAuth, your custom JWT service, etc. const bearerToken: string = await getCurrentUserJwt(); ``` ## Browse the catalog `getSkuList` returns a paginated response. `GetSkuListQuery` supports four filters today (all typed in `@chipi-stack/types@14.4.0`): `provider` (`"TET" | "CHIPI"`), `category` (`SkuCategory` enum — the upstream classification), `chipiCategory` (the admin-curated taxonomy code, e.g. `"RECARGAS"`, `"GIFT_CARDS"`, `"GENERAL"`, `"TELEFONIA"`), and `carrierName` (case-insensitive substring match against the carrier). ```ts theme={null} const result = await sdk.getSkuList( { chipiCategory: "RECARGAS", carrierName: "Telcel", limit: 50 }, bearerToken, ); console.log(result.total, "matching SKUs"); console.log(result.data[0]); // { id, name, fixedAmount, carrierName, ... } ``` The response is a `PaginatedResponse`: `{ data, total, page, limit, totalPages }`. ## Buy a SKU The SDK's `purchaseSku` requires an on-chain wallet path that's reserved for Chipi's internal PWA. **For API-key integrations, call the REST endpoint directly.** Same auth headers as the read calls, body matches the `CreateSkuPurchaseInput` DTO: ```ts theme={null} const body = { // Used purely as the idempotency key for the credits debit. Any // unique string is fine — UUID, your internal order id, "smoke-", // etc. Re-sending the same value within today returns the existing // Transaction row instead of double-charging. transactionHash: `order-${crypto.randomUUID()}`, skuId: chosenSku.id, skuReference: userPhoneNumber, // phone, account, etc. currencyAmount: chosenSku.fixedAmount, // must match SKU's fixedAmount chain: "STARKNET", token: "USDC", walletAddress: "0x0", // required by the DTO but unused for credits flow orgMarkup: 5, // optional, defaults to 0; recorded for analytics }; const res = await fetch("https://api.chipipay.com/v1/sku-purchases", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.CHIPI_PUBLIC_KEY!, Authorization: `Bearer ${bearerToken}`, }, body: JSON.stringify(body), }); if (!res.ok) { throw new Error(`Purchase failed: ${res.status} ${await res.text()}`); } const transaction = await res.json(); const purchaseId: string = transaction.id; console.log("Purchase created:", purchaseId, "status:", transaction.status); ``` The response is the freshly-created `Transaction` row. `status` is `PENDING` initially. The `ledgerEntryId` field references the debit Chipi just took against your credits balance. ## Poll for settlement `getSkuPurchase` is a read; the SDK works fine here. ```ts theme={null} async function waitForSettlement(id: string) { for (let i = 0; i < 12; i++) { const purchase = await sdk.getSkuPurchase(id, bearerToken); if (purchase.status === "SUCCESS" || purchase.status === "FAILED") { return purchase; } await new Promise((r) => setTimeout(r, 2_500)); } throw new Error("Settlement timeout — set up a webhook for production"); } const final = await waitForSettlement(purchaseId); console.log(final.status, "file:", final.skuFileNumber); ``` Most transactions settle in under 5 seconds. Gift cards with pin-code delivery can take longer. For production traffic, prefer a webhook over polling — configure one at `/configure/notifications` in the dashboard. Chipi delivers `sku-purchase.completed`, `sku-purchase.failed`, and `sku-purchase.refunded` events with HMAC-signed bodies. ## DEV sandbox When the `x-api-key` header carries a DEV key (`pk_dev_...`), the entire purchase flow is sandboxed: * **No real credits debited.** Your `OrgBalance.availableUsd` is never touched. * **No carrier call.** TET is never invoked. * **Deterministic SUCCESS.** Every purchase succeeds within milliseconds. Sandbox responses are tagged so your integration code can distinguish them: ```ts theme={null} // DEV response shape (key fields) { id: "tx-...", status: "SUCCESS", // always SUCCESS in DEV ledgerEntryId: "le-dev-", // synthetic, not a real DB row skuFileNumber: "dev-sandbox-", // not a real carrier receipt } ``` You can integrate freely against `pk_dev_...` without worrying about credits or accidentally recharging a real phone. Swap the API key to `pk_prod_...` (and point the SKU lookup at the production catalog) and the same code path runs against the real carrier with no other changes. ## Putting it together ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; import * as crypto from "crypto"; async function payTelcelRecharge( sdk: ChipiServerSDK, apiPublicKey: string, bearerToken: string, userPhone: string, ) { // 1. Find the right SKU — say, Telcel $200 prepaid const list = await sdk.getSkuList( { chipiCategory: "RECARGAS", carrierName: "Telcel", limit: 50 }, bearerToken, ); const sku = list.data.find((s) => s.fixedAmount === 200); if (!sku) throw new Error("SKU not found"); // 2. Debit credits + queue fulfillment const res = await fetch("https://api.chipipay.com/v1/sku-purchases", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiPublicKey, Authorization: `Bearer ${bearerToken}`, }, body: JSON.stringify({ transactionHash: `order-${crypto.randomUUID()}`, skuId: sku.id, skuReference: userPhone, currencyAmount: 200, chain: "STARKNET", token: "USDC", walletAddress: "0x0", }), }); if (!res.ok) throw new Error(`Purchase failed: ${res.status}`); const { id: purchaseId } = (await res.json()) as { id: string }; // 3. Wait for settlement for (let i = 0; i < 12; i++) { const p = await sdk.getSkuPurchase(purchaseId, bearerToken); if (p.status === "SUCCESS") return p; if (p.status === "FAILED") throw new Error("Purchase failed"); await new Promise((r) => setTimeout(r, 2_500)); } throw new Error("Settlement timeout — set up a webhook for production"); } ``` > ✅ Verified against the live API on **2026-05-19** — exact pattern used in the production smoke purchase for `sku-VIR020` (Virgin \$20 MXN). 201 + PENDING from the POST, SUCCESS within 5 seconds on the first poll, real TET file number returned, recharge landed on a live phone. ## What's next * Browse the catalog visually in the dashboard at `/admin/bills` to discover what's available. * Configure your per-transaction markup at `/configure/skus`. * Set up a webhook at `/configure/notifications` so you don't have to poll. * See the [React guide](/services/bills/react) and the [Python guide](/services/bills/python) for idiomatic versions of the same REST + JWT pattern. # Bill Payments Source: https://docs.chipipay.com/services/bills/overview Let your users pay 700+ Mexican services — phone recharges, gift cards, utility bills, toll roads — from your pre-funded Chipi credits. One REST endpoint, 170+ brands, USDC settlement, no end-user wallet required. The Bill Payments service is **live in Mexico** (MXN). You pre-fund Chipi credits in USDC; on each purchase Chipi debits your credits, fulfills with the provider in MXN, and lets you charge your own users however you want (card, transfer, in-app balance — that side is outside Chipi). You earn a configurable markup on every transaction. ## At a glance Live SKUs across all 4 categories Direct integrations with major Mexican carriers and providers Phone, Gift Cards, Bills, Phone Bundles Set your own per-transaction markup; we settle in USDC ## What's in the catalog **35 carriers · 379 services** Top up prepaid phone balance for any major Mexican carrier. Your users pay in USDC, the carrier sees a normal recharge. Telcel AT&T (Iusacell - Nextel) Movistar BAIT Mimovil ULTRACEL Axios Mobile VALOR TELECOM …and 27 more carriers (FlashMobile, OUI, RediCoppel, FRC Mobile, ABIB, Beneleit, Megamovil, and others). ```bash theme={null} curl -G "https://api.chipipay.com/v1/skus" \ --data-urlencode "chipiCategory=RECARGAS" \ --data-urlencode "carrierName=Telcel" \ --data-urlencode "limit=20" \ -H "x-api-key: $CHIPI_PUBLIC_KEY" \ -H "Authorization: Bearer $CHIPI_CUSTOMER_JWT" ``` Response is a paginated `{ data, total, page, limit, totalPages }` where each entry in `data` is an SKU (`{ id, name, chipiName, fixedAmount, currency, carrierName, ... }`). Pass an `id` to `POST /v1/sku-purchases` to charge — see the [Node guide](/services/bills/node). **41 brands · 142 services** Sell digital gift cards funded with crypto. Codes are delivered instantly after settlement. Amazon Gift Card Cinepolis Nintendo Netflix Google Play FREE FIRE CINEMEX GANDHI …and 33 more brands (Innvictus, PUBG Mobile, Amazon Prime Video, and others). ```bash theme={null} curl -G "https://api.chipipay.com/v1/skus" \ --data-urlencode "chipiCategory=GIFT_CARDS" \ --data-urlencode "carrierName=Amazon Gift Card" \ -H "x-api-key: $CHIPI_PUBLIC_KEY" \ -H "Authorization: Bearer $CHIPI_CUSTOMER_JWT" ``` **88 providers · 141 services** Utility bills, toll roads, government services, cable / streaming subscriptions, beauty brand orders. CFE Telmex Megacable Dish Infonavit Televia IAVE Pase Urbano …plus water utilities (28 cities), state government services, and 60+ other providers. ```bash theme={null} curl -G "https://api.chipipay.com/v1/skus" \ --data-urlencode "chipiCategory=GENERAL" \ --data-urlencode "carrierName=CFE" \ -H "x-api-key: $CHIPI_PUBLIC_KEY" \ -H "Authorization: Bearer $CHIPI_CUSTOMER_JWT" ``` **7 carriers · 48 services** Postpaid plans, internet packages, and combo bundles. Telcel Internet Amigo Paquetes BAIT Paquete Amigo Sin Limite WimoTelecom Internet **Logos** are hot-linked from the production catalog — when admin updates a logo, this page reflects it on the next docs build with no code change. **Carrier and SKU counts** are a static snapshot taken on **2026-05-19**; the live catalog continues to grow. ## Prerequisites Before your first bill purchase you need three things in place: 1. **A Chipi org with an API key.** Get yours from `dashboard.chipipay.com` → API Keys. Use `pk_dev_...` while integrating (sandboxed, see below) and `pk_prod_...` for live traffic. 2. **A pre-funded credits balance.** Deposit USDC to your org's Chipi credits address (visible in the dashboard's Billing tab). Each PROD purchase debits this balance, not an end-user wallet. DEV purchases never touch it. 3. **A JWKS rule registered for your API key.** Every endpoint on this page (catalog reads, purchase POST, status polling) is guarded by Chipi's `BearerTokenGuard` and requires *both* an `x-api-key` header *and* a customer JWT in the `Authorization: Bearer` header. The JWT is issued by your auth provider; Chipi validates it against the JWKS URL you register (typically your auth provider's `.well-known/jwks.json`, e.g. `https://clerk.yourdomain.com/.well-known/jwks.json`). ## How it works 1. **Browse** the catalog with `GET /v1/skus` — filter by `category` + `carrierName` to find what you need. 2. **Buy** with `POST /v1/sku-purchases` — pass `skuId`, `skuReference` (phone number, account, etc.), `currencyAmount`, and any non-empty string as `transactionHash` (it's used as the idempotency key, not as an on-chain hash). Chipi debits your credits balance, returns a `PENDING` transaction immediately. 3. **Track** with `GET /v1/sku-purchases/:id` — poll until status is `SUCCESS` or `FAILED`. Most settle in under 5 seconds. Full code (auth + the three endpoints + polling) is on the [Node guide](/services/bills/node). ## DEV sandbox Every API key has a DEV (`pk_dev_...`) and PROD (`pk_prod_...`) variant. **DEV is a true sandbox**: every purchase succeeds deterministically, no real credits are debited, and the carrier is never called. Synthetic markers identify sandbox calls: | Field | DEV value | PROD value | | --------------- | -------------------------- | ---------------------------------------------- | | `ledgerEntryId` | starts with `le-dev-` | real `LedgerEntry` row id | | `skuFileNumber` | starts with `dev-sandbox-` | the carrier's actual fulfillment receipt | | `status` | always `SUCCESS` | `PENDING` → `SUCCESS` or `FAILED` | | `OrgBalance` | never mutated | debited by `currencyAmount` (converted to USD) | Integrate against DEV freely — your credits balance is untouched and your test phone numbers will never receive an actual recharge. When you're ready to go live, swap the key prefix to `pk_prod_...` and the same code path runs against the real carrier. ## What you charge your users How and how much you charge end users is up to you — Chipi never sees the end-user's payment. We settle the provider in MXN and debit your Chipi credits in USDC (FX rate captured at the time of the call). The optional `orgMarkup` field on the purchase body lets you record the markup you charged this end-user, which Chipi surfaces in your dashboard analytics. > ✅ Verified against the live API on **2026-05-19** with a real production purchase: Virgin \$20 MXN recharge to a live phone, settled SUCCESS with TET file number returned in under 5 seconds. # Bill Payments — Python Source: https://docs.chipipay.com/services/bills/python Browse the SKU catalog, submit a bill payment against your Chipi credits, and poll for settlement using the chipi-stack Python SDK plus httpx. No end-user wallet, no on-chain signing. The `chipi-stack` Python package exposes the read side of Bills (`get_sku_list`, `get_sku`, `get_sku_purchase`). The purchase itself is a plain HTTP POST against `/v1/sku-purchases` — the SDK's `purchase_sku` method wraps an on-chain wallet path that's reserved for Chipi's internal PWA, not what you want for API-key integrations. Every endpoint on this page is guarded by Chipi's `BearerTokenGuard` and requires *both* an `x-api-key` header *and* a customer JWT in the `Authorization: Bearer` header. The JWT is issued by *your* auth provider; Chipi validates it against the JWKS URL you register for this API key. The Chipi API secret key (`sk_dev_...` / `sk_prod_...`) is **not** a valid Bearer token — passing it where a customer JWT is expected will 401. ## Install ```bash pip theme={null} pip install "chipi-stack>=2.1.0" httpx ``` ```bash uv theme={null} uv add "chipi-stack>=2.1.0" httpx ``` ```bash poetry theme={null} poetry add "chipi-stack>=2.1.0" httpx ``` ## Initialise the SDK ```python theme={null} import os from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key=os.environ["CHIPI_PUBLIC_KEY"], api_secret_key=os.environ["CHIPI_SECRET_KEY"], ) ) ``` ## Obtain a customer JWT Every call needs a customer JWT for the `Authorization: Bearer` header. In a typical server-side flow you'd have one for the user making the purchase — from your auth provider's session, an exchange of an API token, etc. ```python theme={null} # however you obtain a signed session token for the requesting user — # e.g. from Clerk's Python SDK, your custom JWT service, or an exchange # flow against your auth provider. bearer_token: str = get_current_user_jwt() ``` ## Browse the catalog — `get_sku_list` `get_sku_list` accepts the same filters as the Node SDK: `category` (`SkuCategory` enum), `chipi_category` (curated taxonomy: `"RECARGAS"`, `"GIFT_CARDS"`, `"GENERAL"`, `"TELEFONIA"`), `carrier_name` (case-insensitive substring), `provider` (`"TET" | "CHIPI"`). ```python theme={null} from chipi_sdk import GetSkuListQuery result = sdk.get_sku_list( params=GetSkuListQuery( chipi_category="RECARGAS", carrier_name="Telcel", limit=20, ), bearer_token=bearer_token, ) print(f"{result.total} matching SKUs") print(result.data[0].name, result.data[0].fixed_amount, result.data[0].currency) ``` The async variant is `aget_sku_list`. Same applies to all paired methods on this page. ## Submit a purchase — direct `POST /v1/sku-purchases` The SDK's `purchase_sku` requires an on-chain wallet path that's reserved for Chipi's internal PWA. For API-key integrations, post to the REST endpoint directly. Body matches the `CreateSkuPurchaseInput` DTO: ```python theme={null} import os import uuid import httpx def buy_sku( chosen_sku, user_phone: str, bearer_token: str, *, org_markup: float = 0, ) -> dict: """Debit org credits + queue carrier fulfillment. Returns the freshly-created Transaction row (status="PENDING" initially).""" body = { # transactionHash is used purely as an idempotency key — any # unique string works (UUID, your internal order id, etc.). # Re-sending the same value within today returns the existing # Transaction row instead of double-charging. "transactionHash": f"order-{uuid.uuid4()}", "skuId": chosen_sku.id, "skuReference": user_phone, "currencyAmount": chosen_sku.fixed_amount, "chain": "STARKNET", "token": "USDC", "walletAddress": "0x0", # required by the DTO but unused for credits flow "orgMarkup": org_markup, # optional analytics field } response = httpx.post( "https://api.chipipay.com/v1/sku-purchases", headers={ "Content-Type": "application/json", "x-api-key": os.environ["CHIPI_PUBLIC_KEY"], "Authorization": f"Bearer {bearer_token}", }, json=body, timeout=15.0, ) response.raise_for_status() return response.json() transaction = buy_sku(chosen_sku, "5512345678", bearer_token) purchase_id = transaction["id"] print(f"Purchase created: {purchase_id} (status={transaction['status']})") ``` For async code paths, swap `httpx.post` for `httpx.AsyncClient().post` — same body and headers. ## Poll for settlement — `get_sku_purchase` ```python theme={null} import time def wait_for_settlement(sdk, purchase_id: str, bearer_token: str): for _ in range(12): purchase = sdk.get_sku_purchase( purchase_id, bearer_token=bearer_token, ) if purchase.status in ("SUCCESS", "FAILED"): return purchase time.sleep(2.5) raise TimeoutError("Settlement timeout — set up a webhook for production") final = wait_for_settlement(sdk, purchase_id, bearer_token) print(final.status, "file:", final.sku_file_number) ``` Most transactions settle in under 5 seconds. For production traffic, prefer a webhook over polling — configure one at `/configure/notifications` in the dashboard. ## DEV sandbox When `CHIPI_PUBLIC_KEY` is a DEV key (`pk_dev_...`), every purchase is sandboxed: * **No real credits debited.** Your `OrgBalance.availableUsd` is never touched. * **No carrier call.** TET is never invoked. * **Deterministic SUCCESS.** Every purchase succeeds within milliseconds. Sandbox responses are tagged so your code can distinguish them: ```python theme={null} # DEV response shape (key fields) { "id": "tx-...", "status": "SUCCESS", # always SUCCESS in DEV "ledgerEntryId": "le-dev-", # synthetic, not a real DB row "skuFileNumber": "dev-sandbox-", # not a real carrier receipt } ``` You can integrate freely against `pk_dev_...` without worrying about credits or accidentally recharging a real phone. Swap the API key to `pk_prod_...` (and point catalog lookups at the production data) and the same code runs against the real carrier with no other changes. ## Putting it together ```python theme={null} import os, time, uuid import httpx from chipi_sdk import ChipiSDK, ChipiSDKConfig, GetSkuListQuery def pay_telcel_recharge(sdk: ChipiSDK, user_phone: str, bearer_token: str): # 1. Find the right SKU — say, Telcel $200 prepaid listing = sdk.get_sku_list( params=GetSkuListQuery( chipi_category="RECARGAS", carrier_name="Telcel", limit=50, ), bearer_token=bearer_token, ) sku = next((s for s in listing.data if s.fixed_amount == 200), None) if sku is None: raise ValueError("SKU not found") # 2. Debit credits + queue fulfillment response = httpx.post( "https://api.chipipay.com/v1/sku-purchases", headers={ "Content-Type": "application/json", "x-api-key": os.environ["CHIPI_PUBLIC_KEY"], "Authorization": f"Bearer {bearer_token}", }, json={ "transactionHash": f"order-{uuid.uuid4()}", "skuId": sku.id, "skuReference": user_phone, "currencyAmount": 200, "chain": "STARKNET", "token": "USDC", "walletAddress": "0x0", }, timeout=15.0, ) response.raise_for_status() purchase_id = response.json()["id"] # 3. Wait for settlement for _ in range(12): p = sdk.get_sku_purchase(purchase_id, bearer_token=bearer_token) if p.status == "SUCCESS": return p if p.status == "FAILED": raise RuntimeError("Purchase failed") time.sleep(2.5) raise TimeoutError("Settlement timeout — set up a webhook for production") ``` > ✅ Verified against the live API on **2026-05-19** — exact pattern used in the production smoke purchase for `sku-VIR020` (Virgin \$20 MXN), settled SUCCESS in \< 5s. ## What's next * Browse the catalog visually in the dashboard at `/admin/bills`. * Configure your per-transaction markup at `/configure/skus`. * Set up a webhook at `/configure/notifications` so you don't have to poll. * Need server-side billing in TypeScript? See the [Node guide](/services/bills/node). React-side rendering? See the [React guide](/services/bills/react). # Bill Payments — React Source: https://docs.chipipay.com/services/bills/react Browse the SKU catalog, submit a bill payment against your Chipi credits, and poll for settlement using @chipi-stack/chipi-react hooks plus React Query. No end-user wallet, no on-chain signing. `@chipi-stack/chipi-react` exposes the read side of the Bills flow as React Query hooks (`useGetSkuList`, `useGetSku`, `useGetSkuPurchase`). The purchase itself is a plain `useMutation` calling `POST /v1/sku-purchases` — the SDK's `usePurchaseSku` hook wraps an on-chain wallet path that's reserved for Chipi's internal PWA, not what you want here. Every endpoint on this page is guarded by Chipi's `BearerTokenGuard` and requires *both* an `x-api-key` header *and* a customer JWT in the `Authorization: Bearer` header. The JWT is issued by *your* auth provider (Clerk, Auth0, BetterAuth, your own); Chipi validates it against the JWKS URL you register for this API key. ## Install ```bash npm theme={null} npm install @chipi-stack/chipi-react @tanstack/react-query ``` ```bash pnpm theme={null} pnpm add @chipi-stack/chipi-react @tanstack/react-query ``` ```bash yarn theme={null} yarn add @chipi-stack/chipi-react @tanstack/react-query ``` ## Wrap your app in ChipiProvider `ChipiProvider` accepts your `ChipiSDKConfig`. It mounts a built-in `QueryClient`, so you don't need to provide one separately for Chipi hooks. ```tsx theme={null} import { ChipiProvider } from "@chipi-stack/chipi-react"; export function App({ children }) { return ( {children} ); } ``` ## Resolve a customer JWT Every hook + the purchase mutation need a customer JWT. Wrap your auth-provider's session-token getter in a `getBearerToken` function. With Clerk: ```tsx theme={null} import { useAuth } from "@clerk/nextjs"; export function useBearerToken() { const { getToken } = useAuth(); return () => getToken(); // returns Promise } ``` The same pattern works with Auth0 (`getAccessTokenSilently`), BetterAuth (`session.token`), or any provider that exposes a signed session token. The token is sent verbatim in the `Authorization: Bearer` header and validated against your registered JWKS URL. ## Browse the catalog — `useGetSkuList` Filters are typed in `@chipi-stack/types@14.4.0`: `provider` (`"TET" | "CHIPI"`), `category` (`SkuCategory` enum), `chipiCategory` (curated taxonomy code), `carrierName` (case-insensitive substring). ```tsx theme={null} import { useGetSkuList } from "@chipi-stack/chipi-react"; function TelcelOptions() { const getBearerToken = useBearerToken(); const { data, isLoading, isError } = useGetSkuList({ query: { chipiCategory: "RECARGAS", carrierName: "Telcel", limit: 20, }, getBearerToken, }); if (isLoading) return

Loading…

; if (isError) return

Failed to load catalog

; return (
    {data.data.map((sku) => (
  • {sku.name} — ${sku.fixedAmount} MXN
  • ))}
); } ``` The hook is automatically disabled until `getBearerToken` returns a non-null token, so it's safe to render before sign-in. `data` shape: `PaginatedResponse` — `{ data: Sku[], total, page, limit, totalPages }`. ## Look up a single SKU — `useGetSku` ```tsx theme={null} import { useGetSku } from "@chipi-stack/chipi-react"; function SkuDetail({ skuId }: { skuId: string }) { const getBearerToken = useBearerToken(); const { data: sku, isLoading } = useGetSku({ id: skuId, getBearerToken }); if (isLoading || !sku) return

Loading…

; return

{sku.name} costs ${sku.fixedAmount} MXN

; } ``` ## Submit a purchase — custom `useMutation` against `/v1/sku-purchases` Use React Query's `useMutation` to wrap a plain `fetch`. The request body matches the `CreateSkuPurchaseInput` DTO on chipi-back. `transactionHash` is purely an idempotency key — any unique string is fine; re-sending the same value within the day returns the existing Transaction row instead of double-charging. ```tsx theme={null} import { useMutation } from "@tanstack/react-query"; interface PurchaseInput { skuId: string; skuReference: string; // phone, account, etc. currencyAmount: number; // must match sku.fixedAmount orgMarkup?: number; // analytics; defaults to 0 } function useSubmitPurchase() { const getBearerToken = useBearerToken(); return useMutation({ mutationFn: async (input: PurchaseInput) => { const bearerToken = await getBearerToken(); if (!bearerToken) throw new Error("Sign in first"); const res = await fetch( `${process.env.NEXT_PUBLIC_CHIPI_API_URL}/v1/sku-purchases`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.NEXT_PUBLIC_CHIPI_PUBLIC_KEY!, Authorization: `Bearer ${bearerToken}`, }, body: JSON.stringify({ transactionHash: `order-${crypto.randomUUID()}`, skuId: input.skuId, skuReference: input.skuReference, currencyAmount: input.currencyAmount, chain: "STARKNET", token: "USDC", walletAddress: "0x0", // required by the DTO but unused for credits flow orgMarkup: input.orgMarkup ?? 0, }), }, ); if (!res.ok) throw new Error(`Purchase failed: ${res.status}`); return res.json(); // Transaction }, }); } ``` The mutation returns a `Transaction`. `id` is what you pass to `useGetSkuPurchase` to poll for settlement. ## Poll for settlement — `useGetSkuPurchase` ```tsx theme={null} import { useGetSkuPurchase } from "@chipi-stack/chipi-react"; function PurchaseStatus({ purchaseId }: { purchaseId: string }) { const getBearerToken = useBearerToken(); const { data: purchase } = useGetSkuPurchase({ id: purchaseId, getBearerToken, queryOptions: { // React Query refetches while status is non-terminal refetchInterval: (q) => { const s = q.state.data?.status; return s === "SUCCESS" || s === "FAILED" ? false : 2_500; }, }, }); if (!purchase) return

Loading…

; if (purchase.status === "SUCCESS") return

✅ Done — file {purchase.skuFileNumber}

; if (purchase.status === "FAILED") return

❌ Failed

; return

{purchase.status}…

; } ``` For production traffic, prefer a webhook over polling — configure one at `/configure/notifications` in the dashboard. ## DEV sandbox When `NEXT_PUBLIC_CHIPI_PUBLIC_KEY` is a DEV key (`pk_dev_...`), every purchase is sandboxed: * **No real credits debited.** Your `OrgBalance.availableUsd` is never touched. * **No carrier call.** TET is never invoked. * **Deterministic SUCCESS.** Every purchase succeeds within milliseconds. Sandbox responses are tagged so your UI can distinguish them — `ledgerEntryId` starts with `le-dev-` and `skuFileNumber` starts with `dev-sandbox-`. Swap to `pk_prod_...` (and point the catalog reads at the production data) and the same code path runs against the real carrier. ## Putting it together ```tsx theme={null} import { useState } from "react"; import { useGetSkuList, useGetSkuPurchase } from "@chipi-stack/chipi-react"; export function TelcelRecharge({ userPhone }: { userPhone: string }) { const getBearerToken = useBearerToken(); const [purchaseId, setPurchaseId] = useState(null); const { data: list } = useGetSkuList({ query: { chipiCategory: "RECARGAS", carrierName: "Telcel", limit: 20 }, getBearerToken, }); const submit = useSubmitPurchase(); const { data: purchase } = useGetSkuPurchase({ id: purchaseId ?? "", getBearerToken, queryOptions: { enabled: !!purchaseId, refetchInterval: (q) => { const s = q.state.data?.status; return s === "SUCCESS" || s === "FAILED" ? false : 2_500; }, }, }); const buy = async (sku: any) => { const result = await submit.mutateAsync({ skuId: sku.id, skuReference: userPhone, currencyAmount: sku.fixedAmount, }); setPurchaseId(result.id); }; return (
{list?.data.map((sku) => ( ))} {purchase &&

Status: {purchase.status}

}
); } ``` ## What's next * Need server-side billing instead? See the [Node guide](/services/bills/node) — same REST pattern with `fetch` + customer JWT. * Need to filter by carrier? Use `carrierName` — case-insensitive substring match. * Need credits to actually charge for purchases? See [Billing & Credits](/services/billing/overview). > ✅ Verified against the live API on **2026-05-19** — same REST contract used in the production smoke purchase against Legaria (transaction `tx-75456794-f4b8-4835-8282-0f89f1964eda`, sku-VIR020 Virgin \$20 MXN, settled SUCCESS in \< 5s). # Refunds & failed purchases Source: https://docs.chipipay.com/services/bills/refunds When a bill payment, recharge, or gift card can't be fulfilled after the user paid, return their money — safely, idempotently, and in the right place depending on how you collected it. A purchase can fail at the provider *after* you've already charged your user. Carriers reject the **same product + reference within a short window** by design — airtime: "same transaction within 15 minutes"; gift cards: "one code per phone number". When that happens the user paid but received nothing, and you need to give it back. Where the refund happens depends on **how you collected the money**. Most integrators pre-fund Chipi credits and charge their own users by card, transfer, or in-app balance — that side is yours. When a purchase fails: * Chipi restores **your credits** automatically and sends `sku-purchase.refunded` (so your ledger reconciles off one signal). * **You refund the user in your own payment system** (reverse the card charge, credit their in-app balance, etc.) — Chipi never touched the user's money, so only you can return it. Listen for `sku-purchase.failed` / `sku-purchase.refunded` and trigger your own refund. No on-chain work is involved. If your user paid **USDC on-chain into your wallet** (e.g. a consumer wallet app), the funds sit in **your** wallet — only you can sign them back. Chipi emits `sku-purchase.refund-due` with a ready-to-use refund instruction; you execute the on-chain refund. See [Executing an on-chain refund](#executing-an-on-chain-refund). Returning the user's money is always the **integrator's responsibility** — you collected it, so you control where it sits. Chipi restores *your* credits and gives you the signal + data to make the user whole. ## Webhook events Configure a webhook at `/configure/notifications` in the dashboard. Every body is HMAC-signed (see [Verifying the signature](#verifying-the-signature)). | Event | Meaning | | ------------------------- | ---------------------------------------------------------------------------------------------- | | `sku-purchase.completed` | Fulfilled successfully. | | `sku-purchase.failed` | Terminal failure. Refund the user in *your* payment system if you charged them off-platform. | | `sku-purchase.refunded` | **Your Chipi credits** were restored (internal accounting) — not the user's money. | | `sku-purchase.refund-due` | You collected the user's payment **on-chain** and owe it back. Carries a `refund` instruction. | ## Verifying the signature Each delivery includes a `chipi-signature` header: `HMAC-SHA256(rawBody, signingKey)` as hex. Verify it against the **raw** request body before trusting the event. ```ts theme={null} import { createHmac, timingSafeEqual } from "crypto"; function verifyChipiSignature(rawBody: string, signature: string, signingKey: string): boolean { const expected = createHmac("sha256", signingKey).update(rawBody).digest("hex"); try { return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex")); } catch { return false; } } ``` ## Executing an on-chain refund Only relevant if you collected an on-chain USDC payment. On `sku-purchase.refund-due` the payload carries everything you need: ```json theme={null} { "event": "sku-purchase.refund-due", "data": { "skuPurchaseTransaction": { "...": "the full transaction row" }, "refund": { "recipient": "0x…", // the wallet that paid — refund here "token": "USDC", "amount": "12.50", // ADVISORY (recorded debit) — verify on-chain "paymentTransactionHash": "0x…", // the idempotency key "reason": "fulfillment_failed" } } } ``` Returning money is irreversible, so the executor must be defensive. These are the same four guardrails Chipi's own consumer wallet uses. A Starknet tx can report `execution_status = SUCCEEDED` while the inner transfer no-op'd (a "phantom success"). Read the receipt and confirm a **USDC `Transfer` event into your wallet** actually fired. No event → no money arrived → do not refund. This doubles as the ownership gate: only refund payments that settled into *your* wallet. Reserve `refund-` **before** sending; if it already exists, skip. Webhooks can deliver more than once — refund **exactly once**. `refund.amount` is advisory; verify the actual settled `Transfer` amount and never exceed it, bounded by a sane maximum. Above the cap, escalate to manual review instead of auto-paying. Send USDC from your collection wallet back to `refund.recipient` and record the refund tx hash so it's auditable. ```ts theme={null} import { Account, Contract, RpcProvider, cairo } from "starknet"; async function handleRefundDue(refund: { recipient: string; paymentTransactionHash: string; amount: string; }) { const provider = new RpcProvider({ nodeUrl: process.env.STARKNET_RPC_URL! }); // 1. Verify a USDC Transfer into YOUR wallet actually settled (phantom guard). const receipt: any = await provider.getTransactionReceipt(refund.paymentTransactionHash); const settled = receipt.execution_status === "SUCCEEDED" && receipt.events?.some( (e: any) => BigInt(e.from_address) === BigInt(USDC_ADDRESS) && [...(e.keys ?? []), ...(e.data ?? [])].some((x: string) => BigInt(x) === BigInt(MY_WALLET)), ); if (!settled) return; // 2. Idempotency: reserve before sending. if (!(await reserveRefundOnce(refund.paymentTransactionHash))) return; // 3. Cap. 4. Send USDC back to the user. const amount = Math.min(Number(refund.amount), MAX_REFUND_USD); const account = new Account({ provider, address: MY_WALLET, signer: process.env.MY_WALLET_PK! }); const usdc = new Contract({ abi: ERC20_ABI, address: USDC_ADDRESS }); const call = usdc.populate("transfer", [ refund.recipient, cairo.uint256(BigInt(Math.floor(amount * 1_000_000))), // USDC is 6 dp ]); const { transaction_hash } = await account.execute(call); await markRefundSent(refund.paymentTransactionHash, transaction_hash); } ``` Return `2xx` quickly and do the on-chain work asynchronously so a slow refund doesn't trigger webhook retries. Idempotency makes retries safe regardless. ## Avoid charging twice in the first place Refunds are the safety net — the better fix is not double-charging: * **Reuse the idempotency key on retry.** `transactionHash` on `POST /v1/sku-purchases` is the idempotency key. Persist it per `(skuId, reference, amount)` and reuse it when the user taps "try again" — a repeated key returns the **existing** purchase instead of creating a second charge. * **Lock the buy action while a purchase is in flight** to prevent a double-submit. * Chipi also rejects an exact `(skuId, reference, amount)` repeat within the carrier's dedup window before any new debit, as a server-side backstop. # Reading State — Node.js Source: https://docs.chipipay.com/services/gasless/balance Read-only operations every Chipi integration uses: token balances, the SKU catalog, and fiat-to-USD conversions. Zero gas, zero cost. The reads on this page never touch a transaction. Use them whenever your app needs to display balances, browse the catalog, or quote a fiat amount in USD before charging the user. ## Token balances `getTokenBalance` returns the on-chain balance of an ERC-20 token for a wallet. Look the wallet up by your own user identifier, or by the wallet's public address. ```ts theme={null} import { Chain, ChainToken } from "@chipi-stack/backend"; // By externalUserId (the identifier you passed to createWallet) const usdc = await sdk.getTokenBalance({ externalUserId: "user-42", chainToken: ChainToken.USDC, chain: Chain.STARKNET, }); // Or by raw public key const eth = await sdk.getTokenBalance({ walletPublicKey: "0x...", chainToken: ChainToken.ETH, chain: Chain.STARKNET, }); ``` The response shape is: ```ts theme={null} { chain: Chain; // "STARKNET" chainToken: ChainToken; // "USDC" | "ETH" | "STRK" | ... chainTokenAddress: string; decimals: number; // 6 for USDC, 18 for ETH/STRK balance: string; // raw on-chain units, e.g. "1234567" = 1.234567 USDC } ``` `balance` is a **string of raw token units** (not human-formatted). It's a string because raw balances exceed JavaScript's safe-integer range — a single ETH (`10^18` wei) is already past `Number.MAX_SAFE_INTEGER` (`2^53 − 1`). Parse with `BigInt`; only convert to `Number` at display time. ```ts theme={null} // For display (acceptable for typical balances; loses precision past ~9 PB of USDC) const display = Number(BigInt(usdc.balance)) / 10 ** usdc.decimals; console.log(`USDC balance: ${display}`); // For ledgers / arithmetic that must stay precise — keep it as BigInt const balance = BigInt(usdc.balance); const oneUsdc = 10n ** BigInt(usdc.decimals); if (balance >= oneUsdc) { console.log("User has at least 1 USDC"); } ``` ## Browse the SKU catalog `getSkuList` returns paginated SKUs for the Bill Payments service. See the [Bills guide](/services/bills/node) for filtering by `category`, `chipiCategory`, `carrierName`, and `provider`. ```ts theme={null} const result = await sdk.getSkuList({ page: 1, limit: 5 }); console.log(result.total); // total matching SKUs console.log(result.data[0].name); // first SKU's display name ``` `getSku` fetches a single SKU by id: ```ts theme={null} const sku = await sdk.getSku("sku-MI200"); console.log(sku.name, sku.fixedAmount, sku.currency); ``` ## Fiat → USD conversion `getUsdAmount` converts a local-currency amount to USD using Chipi's exchange rate. Useful for showing the user "you'll be charged \~$X USDC for a $200 MXN bill" before they confirm. ```ts theme={null} import { Currency } from "@chipi-stack/backend"; const usd = await sdk.getUsdAmount(200, Currency.MXN); console.log(`200 MXN ≈ $${usd.toFixed(2)} USD`); ``` `Currency` values today: `"MXN"` and `"USD"`. ## Putting it together A common dashboard pattern: show the user their USDC balance and quote the cost of a recharge before they tap "buy". ```ts theme={null} import { Chain, ChainToken, Currency } from "@chipi-stack/backend"; async function quotePurchase(sdk, externalUserId, skuId) { const [balance, sku] = await Promise.all([ sdk.getTokenBalance({ externalUserId, chainToken: ChainToken.USDC, chain: Chain.STARKNET, }), sdk.getSku(skuId), ]); const usdQuote = await sdk.getUsdAmount(sku.fixedAmount, sku.currency as Currency); const balanceHuman = Number(BigInt(balance.balance)) / 10 ** balance.decimals; return { balance: balanceHuman, // USDC the user holds cost: usdQuote, // approx USDC needed canAfford: balanceHuman >= usdQuote, }; } ``` > ✅ Verified against the live API on **2026-05-11**. # Error Handling — Node.js Source: https://docs.chipipay.com/services/gasless/errors Concrete examples of every failure path the SDK throws — constructor validation, transfer rejection, missing wallets — plus the typed error classes so you can branch on them with `instanceof`. The SDK throws fast and throws specifically. Constructor validation rejects bad config before any network call; API calls throw a `ChipiApiError` with a usable `status` code; missing resources return `null` (not a throw) where it makes sense. This page walks the patterns you'll actually hit. ## The error hierarchy Concrete classes live in `@chipi-stack/shared` and all extend a base `ChipiError`: ```ts theme={null} import { ChipiError, // base — { code: string, status?: number } ChipiApiError, // HTTP-level failures — carries the HTTP status (its // constructor takes `status: number`, though the // inherited type signature is `status?: number`) ChipiAuthError, // 401 from the API ChipiValidationError, // 400 from the API ChipiWalletError, // wallet ops (encryption, lookup) ChipiTransactionError,// transfer / contract calls ChipiSessionError, // session keys ChipiSkuError, // sku purchases / catalog WalletNotCompatibleError, // < ChipiWalletError WalletClassHashNotFoundError, PaymasterIncompatibleError, } from "@chipi-stack/shared"; ``` Every `ChipiError` has `.code` (string) and `.message`. `ChipiApiError` carries `.status` (HTTP status code) — its constructor requires it, though the inherited type makes it optional, so devs reading the field still need a fallback (`err.status ?? 500`) to satisfy strict TypeScript. Use `instanceof` for type narrowing or pattern-match on `.code` / `.name` / `.status`. ## Constructor validation The `ChipiServerSDK` constructor throws synchronously on bad config — before any network call. This is intentional: catch typos in your env vars at boot, not at first request. ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; // Throws: empty / invalid keys try { new ChipiServerSDK({ apiPublicKey: "invalid-key-12345", apiSecretKey: "invalid-secret-12345", }); } catch (e) { // catch binds `e` as `unknown` under TS strict mode — narrow before reading const message = e instanceof Error ? e.message : String(e); console.error("SDK config rejected:", message); process.exit(1); } // Throws: empty secret try { new ChipiServerSDK({ apiPublicKey: "pk_live_xxx", apiSecretKey: "", }); } catch { console.error("Missing CHIPI_SECRET_KEY in env"); } ``` Wire this into your bootstrap so the process can't start with broken auth. ## API-level rejections API calls throw `ChipiApiError` (or one of the more specific subclasses) when the server returns a non-2xx. The `status` field tells you which HTTP code — branch on it. ```ts theme={null} import { ChipiApiError, ChipiAuthError } from "@chipi-stack/shared"; try { await sdk.transfer({ params: { encryptKey: "", // ← invalid: empty PIN wallet: userWallet, token: "USDC", recipient: "0x...", amount: 1000n, }, }); } catch (err) { if (err instanceof ChipiAuthError) { // 401 — your CHIPI_SECRET_KEY is wrong / revoked / scoped wrong return res.status(401).send("Auth misconfigured"); } if (err instanceof ChipiApiError && err.status === 400) { // Validation failure on the request body return res.status(400).send(err.message); } if (err instanceof ChipiApiError && err.status === 404) { // Resource missing return res.status(404).send("Wallet not found"); } throw err; // unexpected — let it bubble } ``` ## `getWallet` returns `null`, doesn't throw For lookups that "might not exist", the SDK returns `null` rather than throwing. This is true for `getWallet` — wrap your code accordingly: ```ts theme={null} const wallet = await sdk.getWallet({ externalUserId: "non-existent-user" }); if (!wallet) { // No wallet for this user — your call to make: create one, or ask them to onboard return res.status(404).send("User has no wallet"); } // wallet is non-null here ``` `null` is a happy-path signal here, not an error. The same call only `throw`s if the API itself failed (auth, network, 5xx) — which you'd catch with the `ChipiApiError` patterns above. ## Invalid on-chain inputs throw before sending Inputs that don't pass on-chain validation (malformed addresses, unknown tokens) reject **before** the paymaster fires the transaction — you don't pay gas for a rejected request. ```ts theme={null} import { Chain, ChainToken } from "@chipi-stack/backend"; try { await sdk.getTokenBalance({ walletPublicKey: "0xinvalid", // ← not a valid Starknet address chainToken: ChainToken.USDC, chain: Chain.STARKNET, }); } catch (err) { // Throws synchronously with a validation error — no on-chain cost const message = err instanceof Error ? err.message : String(err); console.error("Bad address:", message); } ``` ## Putting it together A small middleware that maps every Chipi error class to the right HTTP response: ```ts theme={null} import { ChipiError, ChipiApiError, ChipiAuthError, ChipiValidationError, ChipiWalletError, } from "@chipi-stack/shared"; export function chipiErrorHandler(err: unknown, req, res, next) { if (err instanceof ChipiAuthError) { return res.status(401).json({ error: err.code, message: err.message }); } if (err instanceof ChipiValidationError) { return res.status(400).json({ error: err.code, message: err.message }); } if (err instanceof ChipiApiError) { return res.status(err.status ?? 500).json({ error: err.code, message: err.message }); } if (err instanceof ChipiWalletError) { return res.status(409).json({ error: err.code, message: err.message }); } if (err instanceof ChipiError) { return res.status(500).json({ error: err.code, message: err.message }); } next(err); // not a Chipi error — let your generic handler take it } ``` > ✅ Verified against the live API on **2026-05-11**. # Upgrade Existing Wallets Source: https://docs.chipipay.com/services/gasless/migration Add recovery and multi-device support to existing wallets you created before v14.5.0. The wallet's address never changes — same balance, same history, same external integrations. If you created wallets before v14.5.0, those wallets are on the legacy CHIPI v29 contract. They work fine for transfers and session keys, but they can't use recovery, can't be controlled from a second device, and can't be co-signed. Upgrade them in place — keep the user's address, balance, and history — and unlock the modern flows. Available without advertisement until external audit closes. Migration shipped with SDK v14.5.0; mainnet smoke passed with a real on-chain receipt — see [scripts/receipts/cycle-2/](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts/cycle-2). ## What this is The upgrade is one on-chain call that flips the wallet's contract class from the legacy CHIPI v29 implementation to the modern SHHH V8.4 one, **without changing the wallet's address**. After upgrade: * All on-chain history points at the same address * USDC balance, NFT ownership, session-keys reference the same address * External integrators (gift recipients, x402 sellers, paymaster lanes) don't need to update anything * Recovery, threshold, multi-device, and signer-kind flexibility all become available Existing wallets keep working without migration — this is opt-in. New wallets created via SDK v14.5.0+ default to SHHH directly (see [overview](/services/gasless/overview)). ## When to migrate You'd migrate when: * A user asks to add recovery to an existing wallet (only path: migrate first, then add a guardian) * A user wants to import a hardware passkey as the wallet's primary owner (only path: migrate first, then `propose_add_owner` with `WEBAUTHN_P256`) * You want to defrag your tenancy onto one wallet class to simplify ops + paymaster reservations * You're shipping the [dashboard "Migrate to SHHH" button](https://github.com/chipi-pay/sdks/issues/29) to your users You'd NOT migrate when: * The wallet is undeployed (counterfactual) — there's nothing to upgrade; future deploys go straight to SHHH * The wallet is v28 or v33 (not v29) — those legacy versions need a separate path; see [chipi\_back\_migration\_inventory](https://github.com/chipi-pay/sdks/issues/28) * The user is happy with the legacy wallet and doesn't need recovery / threshold / pluggable signers ## What gets migrated | Component | Before (CHIPI v29) | After (SHHH V8.4) | | --------------------------------- | --------------------- | ------------------------------------------------------------------------- | | Wallet address | `0x…` | Same `0x…` (no change) | | Account class hash | `0x053f4f87…f459f2a` | `0x075dfb39…fa58a` | | Owner set | One STARK key | `[{ kind: "STARK", pubkey: , role: "OWNER", weight: 1 }]` | | Session keys (SNIP-9 / SNIP-163) | Registered | Preserved — V8.4 embeds the same SNIP-163 component | | `Account_public_key` storage slot | Populated | Read by bootstrap; cleared after | | Wallet's STARK private key | Encrypted client-side | Same key, same encryption — Chipi never sees the cleartext | | On-chain balances + receipts | Intact | Intact (same address) | ## The migration call ```ts theme={null} import { useMigrateWalletToShhh } from "@chipi-stack/chipi-react"; function MigrateButton({ wallet }) { const { migrateAsync, isLoading, error } = useMigrateWalletToShhh(); async function onClick() { const encryptKey = await promptForPin(); // or passkey-derived key const outcome = await migrateAsync({ walletId: wallet.id, externalUserId: user.id, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, }, encryptKey, bearerToken, }); if (outcome.kind === "MIGRATED") { showToast(`Wallet upgraded — tx ${outcome.transactionHash}`); if (outcome.partial) { showInfo("Backend will catch up shortly"); } } else if (outcome.kind === "ALREADY_MIGRATED") { showToast(`Wallet was already on V8.4 (primary: ${outcome.primaryKind})`); } } return ; } ``` **PIN is weak — not recommended for production.** A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator. **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret. Only prompt for a PIN as the encryption key when: * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or * The platform genuinely has no WebAuthn / biometric support available. If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs. Under the hood: 1. The SDK calls `chipi-back`'s `POST /chipi-wallets/prepare-migrate-to-shhh`, which returns the SNIP-12 typed data for the `[upgrade(V8.4), bootstrap_from_sessions]` multicall. 2. The hook unwraps the STARK private key client-side using `encryptKey` (cleartext never crosses the wire). 3. The hook signs the typed data locally with the STARK key, then calls `POST /chipi-wallets/execute-migrate-to-shhh` with the signature. 4. `chipi-back` relays the signed multicall through the paymaster. The on-chain wallet runs `upgrade` (class hash flip) + `bootstrap_from_sessions` (owner set rebuild) atomically. 5. If chipi-back's DB patch fails after the on-chain success, the execute endpoint still returns 200 with `partial: true` so the dashboard can show "wallet upgraded; backend will catch up". The reconciler cron does the catch-up. ## Two terminal outcomes ```ts theme={null} type MigrationOutcome = | { kind: "MIGRATED"; walletId: string; walletType: "SHHH"; signerKind: "STARK"; classHash: string; // SHHH V8.4 class hash migratedFromClassHash: string; // the legacy CHIPI v29 class hash transactionHash: string; partial?: boolean; // true = on-chain success, DB patch deferred } | { kind: "ALREADY_MIGRATED"; walletAddress: string; onChainClassHash: string; // what chipi-back observed on chain primaryKind: string; // the current SHHH primary signer kind }; ``` Handle both — `ALREADY_MIGRATED` fires when the user already migrated (e.g., raced with another tab, or the wallet was migrated via the dashboard before the SDK call). ## What can go wrong Errors the hook surfaces via `error`: * **`STRANDED`** — the wallet's `Account_public_key` slot is empty (rare; means the wallet was created in a non-standard way). Recovery path is different — out of scope for this PR. * **`UNDEPLOYED`** — wallet is counterfactual (never landed on chain). No migration needed; the next deploy goes straight to SHHH. * **`DEFERRED_LEGACY`** — wallet is v28 or v33. These need their own helper before standard migration runs; tracked separately. * **`DECRYPTION_FAILED`** — the supplied `encryptKey` doesn't decrypt the wallet's STARK key. Wrong PIN / wrong passkey. * **`MIGRATION_DISABLED`** — `chipi-back`'s `CHIPI_MIGRATION_ENABLED` env var is off (kill switch). ## Post-migration UX After migration: * The wallet is `walletType: "SHHH"`, `signerKind: "STARK"`. All single-owner SHHH operations work immediately. * To get the headline UX (passkey, recovery, threshold), the user adds those as additional owners via `propose_add_owner` (see [recovery](/services/gasless/recovery)). * The original STARK key is still the primary owner; you can rotate it out later via `propose_remove_owner` once a passkey owner is established. ## What's NOT migrated * **CHIPI v29 PRF-passkey wallets** keep their PRF-wrapping for the STARK key. Migrating that wallet to SHHH doesn't switch the auth model — the STARK key is still PRF-wrapped. To move the user to a true `WEBAUTHN_P256` primary, add a passkey owner via `propose_add_owner` after migration. * **Session keys with custom `valid_until`** preserve their on-chain entries but you should re-check them against your app's policy after migration; the SDK doesn't re-validate them. ## Related * [Overview](/services/gasless/overview) — what SHHH gives you in return * [Passkey reference](/sdk/passkeys/api) — what owners you can add after migration * [Recovery](/services/gasless/recovery) — the flows you unlock # Gasless Wallets — Node.js Source: https://docs.chipipay.com/services/gasless/node Create a wallet and execute a gasless USDC transfer from a Node.js server using @chipi-stack/backend. **Production apps should pair this with a passkey on the browser side.** This page uses a string `encryptKey` for the smoke-test scenario, which is the simplest possible flow. For shipping integrations, the `encryptKey` should come from a platform passkey — see the [passkey quickstart](/sdk/passkeys/quickstart) for the browser flow. The server pieces below are identical either way. This page shows the full create-wallet → transfer round trip, lifted from the Chipi staging smoke test that runs in CI on every release. Code is verbatim from the test, with secrets (`CHIPI_PUBLIC_KEY`, `CHIPI_SECRET_KEY`, `CHIPI_BASE_URL`, encrypted private keys) read from environment variables. ## Install ```bash npm theme={null} npm install @chipi-stack/backend ``` ```bash pnpm theme={null} pnpm add @chipi-stack/backend ``` ```bash yarn theme={null} yarn add @chipi-stack/backend ``` ## Initialise the SDK ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, // baseUrl is only needed if you're targeting staging or a self-hosted backend baseUrl: process.env.CHIPI_BASE_URL, // omit for production }); ``` ## Create a wallet `createWallet` provisions a fresh Starknet account, encrypts the private key with the `encryptKey` you provide, and returns the public key + encrypted private key. Chipi's paymaster covers the deploy transaction. ```ts theme={null} const externalUserId = `user-${Date.now()}`; const wallet = await sdk.createWallet({ params: { encryptKey: userPin, // user-chosen PIN or passkey-derived secret externalUserId, // your user identifier chain: "STARKNET", }, }); console.log(wallet.publicKey); // 0x... console.log(wallet.encryptedPrivateKey); // ciphertext you store alongside your user ``` Look it up later by your own user ID: ```ts theme={null} const retrieved = await sdk.getWallet({ externalUserId }); ``` ## Transfer USDC (gasless) ```ts theme={null} const txHash = await sdk.transfer({ params: { encryptKey: userPin, wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, }, token: "USDC", recipient: "0xRecipientAddress...", amount: 1000n, // 0.001 USDC (USDC has 6 decimals — adjust accordingly) }, }); console.log(txHash); // 0x... — settles on Starknet mainnet without the user paying gas ``` The transfer is paid for by your service wallet's USDC balance. Check that balance in the dashboard at `/configure/billing`. ## Putting it together End-to-end, mirroring the smoke test: ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; async function main() { const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, }); // 1. Create a wallet for your user const externalUserId = `user-${Date.now()}`; const wallet = await sdk.createWallet({ params: { encryptKey: process.env.USER_PIN!, externalUserId, chain: "STARKNET" }, }); // 2. Send 0.001 USDC from your funding wallet to the new wallet const txHash = await sdk.transfer({ params: { encryptKey: process.env.FUNDING_WALLET_ENCRYPT_KEY!, wallet: { publicKey: process.env.FUNDING_WALLET_PUBLIC_KEY!, encryptedPrivateKey: process.env.FUNDING_WALLET_ENCRYPTED_KEY!, }, token: "USDC", recipient: wallet.publicKey, amount: 1000n, }, }); console.log({ wallet: wallet.publicKey, txHash }); } main(); ``` > ✅ Verified against the live API on **2026-05-11**. ## What's next Reading token balances, PIN/passkey rotation, and session keys are covered by their own smoke tests (`staging-reads.test.ts`, `staging-update-encryption.test.ts`, `staging-sessions.test.ts`) and will land in follow-up doc PRs. # Gasless Wallets Source: https://docs.chipipay.com/services/gasless/overview Give your users a Starknet wallet they sign with Touch ID / Face ID / Windows Hello. They never type a password, never see a seed phrase, and never pay gas. A Chipi Gasless Wallet is a real Starknet account your user fully controls. Your app calls one method to create it; the user signs with biometrics; Chipi's paymaster pays the gas. You don't custody anything. ## What you actually build * **A "Sign up with Touch ID" button.** The user taps once, you get back a wallet address tied to their device's passkey. * **A "Send" button that just works.** No gas modal. No funding step. No PIN prompt. * **A recoverable account.** If the user loses their phone, they get back in — either from a second device they registered or from a recovery contact you set up at onboarding. ## What you do NOT build * A custody system. The user's signing key lives in their device's secure enclave; you can't move it, sign for it, or recover it without them. * A gas station. Chipi's paymaster spends from your service balance; users never see gas at all. * A seed-phrase backup flow. The user authenticates with biometrics every time — there's no phrase to copy down. ## Pick your starting point `@chipi-stack/backend` — the headline path. Server creates the wallet, browser passkey signs every transfer. `chipi-stack` on PyPI — same flow from a Python backend. Browser side. Register the passkey, sign the transaction. End-to-end example. Skip the biometric prompt for routine actions. The user signs once; subsequent transfers run automatically until the session expires. ## After onboarding | Need | Page | | ---------------------------------------- | ------------------------------------------------------- | | Read balances | [balance](/services/gasless/balance) | | List or query transactions | [transactions](/services/gasless/transactions) | | Help a user who lost access | [recover access](/services/gasless/recovery) | | Add a second device or co-signer | [add a second device](/services/gasless/threshold) | | Upgrade existing wallets to add recovery | [upgrade existing wallets](/services/gasless/migration) | | Handle errors gracefully | [errors](/services/gasless/errors) | ## A note on PINs The SDK accepts a PIN as the wallet's encryption key. **Don't lead with that flow** — a user-typed PIN is short, low-entropy, and trivially compromised by shoulder-surfing or phishing. The recommended default is the platform passkey (Touch ID / Face ID / Windows Hello / Android biometrics), where the signing key stays inside the device and never derives from anything the user types. PIN is supported as a fallback for users on devices that don't support WebAuthn, and as a recovery surface if the passkey ever fails. Both flows are documented in [recover access](/services/gasless/recovery). ## Operating from the dashboard Dashboard → Services → Gasless Wallets Once you've completed the 5 onboarding milestones (API keys → auth → first wallet → first transfer → webhook), the dashboard surfaces recent wallets, recent activity, and the upgrade-existing-wallets affordance. # Gasless Wallets — Python Source: https://docs.chipipay.com/services/gasless/python Create a wallet and look it up with the chipi-stack Python SDK. Code lifted from the Chipi staging smoke test that runs in CI. **Production apps should pair this with a passkey on the browser side.** This page uses a string `encrypt_key` for the smoke-test scenario, which is the simplest possible flow. For shipping integrations, the `encrypt_key` should come from a platform passkey via the browser-side flow — see the [passkey quickstart](/sdk/passkeys/quickstart). The Python pieces below are identical either way. This page shows wallet creation and retrieval with `chipi-stack` on PyPI, using code lifted verbatim from `python/tests/test_staging_smoke.py`. The Python SDK is published as **`chipi-stack`** on PyPI — matching the `@chipi-stack/*` npm scope. (The legacy `chipi-python` package is deprecated.) ## Install ```bash pip theme={null} pip install chipi-stack ``` ```bash uv theme={null} uv add chipi-stack ``` ```bash poetry theme={null} poetry add chipi-stack ``` Requires Python 3.11+. **Install vs import:** the PyPI package is `chipi-stack` but the Python module is `chipi_sdk`. After `pip install chipi-stack`, import with `from chipi_sdk import ...` (not `chipi_stack`). ## Initialise the SDK ```python theme={null} import os from chipi_sdk import ChipiSDK, ChipiSDKConfig sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key=os.environ["CHIPI_PUBLIC_KEY"], api_secret_key=os.environ["CHIPI_SECRET_KEY"], # alpha_url is only needed for staging or self-hosted backends alpha_url=os.environ.get("CHIPI_BASE_URL"), # omit for production ) ) ``` ## Create a wallet `create_wallet` provisions a fresh Starknet account, encrypts the private key with the `encrypt_key` you provide, and returns a flat response (no nested `.wallet` field — `public_key` and `encrypted_private_key` are at the root). ```python theme={null} from uuid import uuid4 from chipi_sdk import CreateWalletParams, WalletType external_user_id = f"user-{uuid4().hex[:12]}" result = sdk.create_wallet( params=CreateWalletParams( encrypt_key="user-chosen-pin", external_user_id=external_user_id, wallet_type=WalletType.CHIPI, ), bearer_token=os.environ["CHIPI_SECRET_KEY"], ) print(result.public_key) # 0x... print(result.wallet_type) # WalletType.CHIPI print(result.chain) # Chain.STARKNET print(result.is_deployed) # False initially; flips to True once the deploy tx confirms # result.encrypted_private_key is sensitive — store it in your user record # (e.g. Postgres) alongside the externalUserId. Never log it. store_in_db(external_user_id, result.encrypted_private_key) ``` ## Get a wallet Look it up later by your own user ID: ```python theme={null} from chipi_sdk import GetWalletParams retrieved = sdk.get_wallet( params=GetWalletParams(external_user_id=external_user_id), bearer_token=os.environ["CHIPI_SECRET_KEY"], ) assert retrieved.public_key == result.public_key assert retrieved.encrypted_private_key == result.encrypted_private_key ``` The response is the same flat shape — `public_key`, `encrypted_private_key`, `wallet_type`, `chain`, `is_deployed`, `id`, `external_user_id`, `created_at`, `updated_at` are all on the root object. ## Putting it together ```python theme={null} import os from uuid import uuid4 from chipi_sdk import ( ChipiSDK, ChipiSDKConfig, CreateWalletParams, GetWalletParams, WalletType, ) def main(): sdk = ChipiSDK( config=ChipiSDKConfig( api_public_key=os.environ["CHIPI_PUBLIC_KEY"], api_secret_key=os.environ["CHIPI_SECRET_KEY"], ) ) bearer = os.environ["CHIPI_SECRET_KEY"] external_user_id = f"user-{uuid4().hex[:12]}" # 1. Create created = sdk.create_wallet( params=CreateWalletParams( encrypt_key=os.environ["USER_PIN"], external_user_id=external_user_id, wallet_type=WalletType.CHIPI, ), bearer_token=bearer, ) # 2. Look it up by your own user ID retrieved = sdk.get_wallet( params=GetWalletParams(external_user_id=external_user_id), bearer_token=bearer, ) print({"public_key": created.public_key, "match": retrieved.public_key == created.public_key}) if __name__ == "__main__": main() ``` > ✅ Verified against the live API on **2026-05-11**. ## What's next Transfers, balance reads, PIN rotation, and session keys are exposed through the same SDK — coverage in the Python guide expands as we port more smoke tests. For the Node-equivalent flow today, see the [Node guide](/services/gasless/node). # Recover Access Source: https://docs.chipipay.com/services/gasless/recovery Help users who lost their device, switched browsers, or forgot their PIN. Two on-chain flows close the lost-key dead end plus a side door for rotating a forgotten PIN. A user who can't sign anymore — broken phone, new laptop, forgotten PIN — needs a path back into their wallet. This page covers three of them: 1. **Add a new device to a wallet they can still sign with** (48-hour timelock). The canonical "switching from old phone to new" case. 2. **Recover a wallet they can no longer sign with at all** (7-day timelock). A pre-registered recovery contact authorizes the rotation onto a fresh device. 3. **Change the PIN that encrypts their wallet's signing key** (instant). For users who remember the old PIN and just want to pick a new one — or upgrade to a passkey. The first two flows require a SHHH V8.4 wallet; both run on-chain through the wallet itself with no off-chain custodian. The third works on any wallet type. SHHH-side flows are available without advertisement until external audit closes. Both are on-chain since 2026-05-26 with shipped mainnet smokes — see [scripts/receipts/](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts). ## Flow 1 — Add device (user still has wallet access) Use when the user wants a second device controlling the same wallet (the canonical "switching from laptop to phone" case) or wants to add a guardian after onboarding. **Stages:** 1. **Propose** — the existing owner signs an outside execution containing `propose_add_owner`. The on-chain wallet records a pending op tagged with `OP_ADD_OWNER` and starts a 48-hour timelock. 2. **Wait** — the SDK exposes `useGuardianRecovery().isReady(validAfter)` and `secondsRemaining(validAfter)` to drive a countdown UI. The pending op state is read from chain — the SDK doesn't poll on your behalf. 3. **Execute** — after 48h, ANY OWNER (or anyone if the wallet is configured as such) calls `execute_add_owner` with the matching `op_id`. The wallet re-derives the proposed payload and asserts equality before mutating `owner_set`. ```tsx theme={null} import { Recover, useGuardianRecovery } from "@chipi-stack/chipi-react"; function AddDevice({ wallet, newOwnerPubkey }) { const recovery = useGuardianRecovery(); // pendingOp comes from your chain-read transport (multicall, indexer, RPC). const pendingOp = useChainState(wallet.address); return ( { const call = recovery.buildProposeAddOwner({ walletAddress: wallet.address, proposer: 0, newOwner: { kind: "STARK", pubkeyBytes: [newOwnerPubkey], role: "OWNER", weight: 1, label: "laptop" }, }); const txHash = await submitThroughPaymaster(call); return { opId, validAfter }; }} onExecute={async (opId) => { const call = recovery.buildExecuteAddOwner({ walletAddress: wallet.address, opId, newOwner: { kind: "STARK", pubkeyBytes: [newOwnerPubkey], role: "OWNER", weight: 1, label: "laptop" }, }); await submitThroughPaymaster(call); }} /> ); } ``` ## Flow 2 — Guardian recovery (user lost their key) Use when the user can no longer sign with any existing owner. A pre-registered guardian (a second user, a recovery email's WebAuthn credential, or a paid "Guardian-as-a-Service" provider) initiates recovery on the user's behalf. **Stages:** 1. **Initiate** — the **guardian** (NOT the lost owner) signs an outside execution calling `initiate_recovery`. The wallet starts a 7-day timelock. 2. **Wait** — 7 days. During this window the original owner can `cancel_recovery` if they regain access — this is the safety valve against a malicious guardian. 3. **Finalize** — after 7d, anyone can submit `finalize_recovery`. The wallet rotates `owner_set` to the new owner specified in step 1. ```tsx theme={null} { const call = recovery.buildInitiateRecovery({ walletAddress: lostWallet.address, proposer: guardianOwnerId, // owner_id of the guardian signing newOwner: newOwnerFromRecoveryFlow, }); // Guardian signs this OE, paymaster relays. const { opId, validAfter } = await submitGuardianSigned(call); return { opId, validAfter }; }} onExecute={async (opId) => { const call = recovery.buildFinalizeRecovery({ walletAddress: lostWallet.address, newOwner: newOwnerFromRecoveryFlow, }); await submitThroughPaymaster(call); }} /> ``` ## Cancel safety valve Within either timelock window, **any current owner** can cancel the pending op. The two flows take different on-chain selectors: * **Add-device** uses `cancel_pending_op(op_id)` (matches any pending `OP_ADD_OWNER` / `OP_REMOVE_OWNER` / `OP_ROTATE_OWNER` / `OP_SET_THRESHOLD`). * **Guardian recovery** uses `cancel_recovery(owner_id)` — keyed by which owner is being replaced, not by an op\_id, because recovery operates on the owner set directly rather than through the pending-op queue. This is the protection against a guardian going rogue (during recovery) or a stolen device proposing an attacker as a new owner. `` shows a Cancel button automatically when you pass an `onCancel` callback; pick the matching builder for the mode: ```tsx theme={null} // Add-device: { const call = recovery.buildCancelPendingOp({ walletAddress: wallet.address, opId, }); await submitThroughPaymaster(call); }} /> // Guardian recovery: { const call = recovery.buildCancelRecovery({ walletAddress: lostWallet.address, ownerId: ownerBeingReplaced, // not an op_id }); await submitThroughPaymaster(call); }} /> ``` ## Timelock constants | Op | Timelock | Default expiry | Why | | ------------------ | -------- | -------------- | ------------------------------------------------------------------------------- | | `OP_ADD_OWNER` | 48h | 14d | Time for the owner to notice an unexpected add-device proposal | | `OP_REMOVE_OWNER` | 24h | 14d | Same audit window but tighter — removal is reversible by adding back | | `OP_ROTATE_OWNER` | 24h | 14d | | | `OP_SET_THRESHOLD` | 48h | 14d | Threshold changes are governance-grade | | `RECOVERY` | 7d | n/a | Longest window in the system — the user must have a meaningful chance to cancel | These constants are re-exported on the hook return for UI display: `useGuardianRecovery().TIMELOCK_SECONDS` and `.DEFAULT_OP_EXPIRY_SECONDS`. ## Who acts as guardian? Three patterns we've seen integrators ship: 1. **Second wallet held by the user** — a second device or a paper-backup-rooted wallet. Simplest; no third party. 2. **Email-rooted WebAuthn credential** — register a passkey against the user's email-bound device at onboarding; that credential is the guardian. The user doesn't need a second wallet day-one. 3. **Guardian-as-a-Service** — a paid third party (could be Chipi, could be a partner) holds a guardian key, with a published policy on when they sign recoveries (e.g., on a 48h email + 2FA confirmation). This is on our roadmap; the productization design is internal-only for now. `role: "GUARDIAN"` is set at owner-add time; a guardian cannot initiate or sign transactions like an owner, only call `initiate_recovery` (and `cancel_recovery` if they want to withdraw). ## What does NOT recover Recovery rotates the wallet's **owner set**. It does NOT: * Reset session keys (those remain valid until their own `valid_until`) * Re-create CHIPI v29 STARK key encryption (those wallets don't have on-chain recovery at all — see [migration](/services/gasless/migration) to move to SHHH first) * Touch USDC balances or any other on-chain state — recovery is a key rotation, not a state rollback ## Change a PIN (any wallet type) The two on-chain flows above handle "user can't sign at all." A simpler flow handles "user remembers their PIN but wants to change it" — for example, they want to upgrade to a passkey, or they suspect their PIN was shoulder-surfed. This is **client-side re-encryption** of the same private key, then one backend call to store the new ciphertext. The wallet's address never changes. ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; // 1. Decrypt the existing private key with the CURRENT PIN (client-side). // The SDK exposes the decryption helper used by all signing flows. // 2. Re-encrypt the same private key with the NEW key (PIN, passkey-derived, // or any other encryption material). // 3. Push the new ciphertext via the wallets sub-client. await sdk.wallets.updateWalletEncryption( { externalUserId, newEncryptedPrivateKey, // publicKey?: optional, only needed when the SDK can't infer the wallet }, bearerToken, ); ``` The on-chain account is untouched — same address, same balances, same session keys. Only the encryption wrapper around the private key rotates. This flow CANNOT recover a user who forgot the old PIN (they need step 1's ciphertext to decrypt); for that case, use the guardian recovery flow above. For migrating a PIN-only wallet to a passkey, see the [`useMigrateWalletToPasskey`](/sdk/react/hooks/use-migrate-wallet-to-passkey) hook which wraps this same flow with the WebAuthn ceremony. ## Related * [Add a second device](/services/gasless/threshold) — proactively register a backup device before the user needs to recover * [Upgrade existing wallets](/services/gasless/migration) — get a CHIPI v29 wallet onto SHHH first to use the on-chain recovery flows * [When passkeys fail](/sdk/passkeys/when-passkeys-fail) — the matching browser-side playbook for passkey failure modes # Use Your Exported Key Source: https://docs.chipipay.com/services/gasless/self-custody Your wallet is self-custodial — you can export the owner private key and operate the account from your own code. This is the dangerous-but-sovereign path: how to sign for a SHHH account with the raw key, in TypeScript or Python. A Chipi wallet is self-custodial: only you can authorize transactions, and Chipi holds no key. For users who want full sovereignty, the wallet can **export the owner private key** (Security → Self-custody → Export private key). This page is for the developer who exported it and wants to use it from code — e.g. a script or backend — while still using the app normally. **The exported key can move all your funds.** Anyone who gets it can drain your wallet, and no one (not even Chipi) can recover or revoke it. Never paste it into a website, never share it, never commit it to a repo. Treat it like cash. ## The one thing to know first A Chipi wallet is a **SHHH smart account** (account abstraction), not a plain EOA like a MetaMask account. Its `__validate__` is **disabled on purpose** (`shhh-wallet-cairo:src/account.cairo` panics with `'SHHH: __validate__ disabled'`), so the standard starknet.js path throws: ```ts theme={null} // ❌ Throws `SHHH: __validate__ disabled` — direct invokes are turned off. const acct = new Account(provider, MY_ADDRESS, MY_PRIVATE_KEY); await acct.execute(calls); ``` Instead you **sign an OutsideExecution** and submit it through `execute_from_outside_v2`. Your exported key signs; the transaction can be paid by anyone (you or a relayer), because the OE is permissionless. ## Option A — Self-sovereign (no Chipi servers, you pay gas) You sign with your exported key, then submit the signed OE from any funded Starknet account you control. Chipi is not in the loop — this is what makes self-custody verifiable. In TypeScript, the `ShhhAccount` adapter wraps the whole flow so it reads like starknet.js. In Python, compose the same primitives directly. ```ts TypeScript theme={null} import { Account, RpcProvider, CallData } from "starknet"; import { ShhhAccount, submitViaAccount } from "@chipi-stack/backend"; const provider = new RpcProvider({ nodeUrl: RPC }); // A funded account that only pays gas — it never sees your owner key. const relayer = new Account(provider, RELAYER_ADDRESS, RELAYER_PRIVATE_KEY); const account = new ShhhAccount(provider, SHHH_ADDRESS, EXPORTED_PRIVATE_KEY, { submit: submitViaAccount(relayer), }); // Example: send 1 USDC (native USDC, 6 decimals). const USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb"; const calls = [ { contractAddress: USDC, entrypoint: "transfer", calldata: CallData.compile([RECIPIENT, "1000000", "0"]) }, ]; const { transactionHash } = await account.execute(calls); await provider.waitForTransaction(transactionHash); ``` ```python Python theme={null} import time from chipi_sdk.shhh import ( build_outside_execution, compute_oe_hash, build_stark_envelope, serialize_execute_from_outside_v2_calldata, ANY_CALLER_FELT, ) SHHH_ADDRESS = "0x..." # your account address EXPORTED_PRIVATE_KEY = "0x..." # the exported STARK key SN_MAIN = "0x534e5f4d41494e" # Starknet mainnet chain id # Example: send 1 USDC (native USDC, 6 decimals). USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb" calls = [{"to": USDC, "selector": "transfer", "calldata": [RECIPIENT, 1_000_000, 0]}] oe = build_outside_execution(nonce=hex(int(time.time() * 1000)), calls=calls, caller=ANY_CALLER_FELT) message_hash = compute_oe_hash(oe, SHHH_ADDRESS, SN_MAIN) signature = build_stark_envelope(private_key=EXPORTED_PRIVATE_KEY, message_hash=message_hash, owner_index=0) calldata = serialize_execute_from_outside_v2_calldata(outside_execution=oe, signature=signature) # Submit `execute_from_outside_v2(calldata)` on SHHH_ADDRESS from any funded # account (starknet.py Account.execute) — it only pays gas, it is not the signer. ``` ## Option B — Gasless, via Chipi's paymaster Same signing, but hand the serialized calldata to Chipi's relayer so you don't fund a gas account. You still hold the key; Chipi only relays. ```ts theme={null} // After producing `calldata` (above): const res = await fetch("https://api.chipipay.com/v1/transactions/execute-sponsored-raw", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "", "Authorization": "Bearer ", }, body: JSON.stringify({ accountAddress: SHHH_ADDRESS, calldata, signerKind: "STARK" }), }); const { transactionHash } = await res.json(); ``` ## Notes * **`ownerIndex` `0`** is your primary owner (the exported key). Devices you added later are other owners with their own non-extractable passkeys; exporting doesn't touch them. * **Additive, not destructive.** Using the key from code doesn't disable the app — you can keep using Face ID in the wallet and your key in code at the same time, like MetaMask. * **Mainnet only.** Use the mainnet chain id and native USDC (`0x033068…b35fb`, not USDC.e). * **Replay protection** is per-`nonce`; reusing a nonce reverts with `SRC9: duplicate nonce`. The `ShhhAccount` adapter generates a fresh nonce per call. * If you ever suspect the key leaked, move your funds to a fresh wallet — there is no revoke for a raw key. # Session Keys — Node.js Source: https://docs.chipipay.com/services/gasless/sessions Generate a short-lived session key, register it on-chain, execute transactions with it, and revoke. Lets your server act on a user's wallet for a bounded time without holding the user's PIN. Session keys are a SNIP-9 primitive: the user signs a delegation that authorises a server-held key to make specific calls (or any calls) on the user's wallet for a limited time. After it expires or is revoked, that key is dead. Use cases: * Server-driven trades or payments without prompting the user every time * Background workers that act on the user's behalf * Time-boxed automations (e.g. "rebalance once an hour for the next 24h") This page shows the full lifecycle (create → register → execute → revoke), with code lifted from `staging-integration/staging-sessions.test.ts` and `staging-session-execute.test.ts`. Session keys are powerful — anything the session can do, your server can do without the user present. Always set the shortest reasonable `validUntil`, restrict `allowedEntrypoints` when you can, and revoke as soon as you're done. ## Initialise ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; const sdk = new ChipiServerSDK({ apiPublicKey: process.env.CHIPI_PUBLIC_KEY!, apiSecretKey: process.env.CHIPI_SECRET_KEY!, baseUrl: process.env.CHIPI_BASE_URL, // omit for production }); ``` ## 1. Create a session key Generates a fresh key pair locally and encrypts the private key. Nothing on-chain yet. ```ts theme={null} const sessionKey = sdk.sessions.createSessionKey({ encryptKey: userPin, durationSeconds: 3600, // 1 hour }); console.log(sessionKey.publicKey); // 0x... console.log(sessionKey.validUntil); // unix timestamp (seconds) // sessionKey.encryptedPrivateKey is sensitive — keep it in memory or // in your secrets store, scoped to this session. Never log it. ``` ## 2. Register on-chain The user's wallet signs a transaction that authorises the session key. Chipi's paymaster covers gas. Wait for confirmation before using the key — Starknet blocks settle in \~20-40s. ```ts theme={null} const txHash = await sdk.sessions.addSessionKeyToContract( { encryptKey: userPin, wallet: userWallet, // { publicKey, encryptedPrivateKey } sessionConfig: { sessionPublicKey: sessionKey.publicKey, validUntil: sessionKey.validUntil, maxCalls: 5, // hard cap on calls during the window allowedEntrypoints: [], // empty = any entrypoint; pass selectors to restrict }, }, process.env.CHIPI_SECRET_KEY!, // session sub-service requires explicit bearer token ); console.log("Register tx:", txHash); // Wait for the register tx to land before executing with the session ``` ## 3. Execute a transaction with the session key Once registered, your server can call any allowed entrypoint without the user's PIN. Below: a self-transfer of 0.001 USDC. ```ts theme={null} import { STARKNET_CONTRACTS } from "@chipi-stack/types"; const usdc = STARKNET_CONTRACTS["USDC"]; const amount = "0x" + (1000).toString(16); // 0.001 USDC, 6 decimals const txHash = await sdk.executeTransactionWithSession({ params: { encryptKey: userPin, wallet: userWallet, session: sessionKey, calls: [ { contractAddress: usdc.contractAddress, entrypoint: "transfer", calldata: [recipientAddress, amount, "0x0"], }, ], }, }); console.log("Execute tx:", txHash); ``` You can pass multiple `calls` in the same `executeTransactionWithSession` invocation — they execute as a single multicall. ## 4. Revoke When you're done — or the user logs out — revoke immediately. The on-chain revocation makes the key unusable even if the encrypted private key leaks. ```ts theme={null} const revokeTxHash = await sdk.sessions.revokeSessionKey( { encryptKey: userPin, wallet: userWallet, sessionPublicKey: sessionKey.publicKey, }, process.env.CHIPI_SECRET_KEY!, ); console.log("Revoke tx:", revokeTxHash); // Optionally verify const data = await sdk.sessions.getSessionData({ walletAddress: userWallet.publicKey, sessionPublicKey: sessionKey.publicKey, }); console.log("isActive:", data.isActive); // false ``` `getSessionData` reads through a non-paymaster RPC, which can lag the paymaster RPC used for register/revoke by a few seconds. If you need a definitive on-chain confirmation, wait for the revoke tx to settle (e.g. via Voyager) before calling `getSessionData`. ## Putting it together ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; import { STARKNET_CONTRACTS } from "@chipi-stack/types"; async function timeboxedSendOnUsersBehalf( sdk: ChipiServerSDK, userPin: string, userWallet: { publicKey: string; encryptedPrivateKey: string }, recipient: string, amountUsdc: bigint, // raw units (6 decimals) ) { // 1. Create + register a 1-hour session const session = sdk.sessions.createSessionKey({ encryptKey: userPin, durationSeconds: 3600, }); const registerTxHash = await sdk.sessions.addSessionKeyToContract( { encryptKey: userPin, wallet: userWallet, sessionConfig: { sessionPublicKey: session.publicKey, validUntil: session.validUntil, maxCalls: 1, allowedEntrypoints: [], }, }, process.env.CHIPI_SECRET_KEY!, ); // 2. Wait for the register tx to confirm before executing — otherwise // the session may not yet be active on-chain. await waitForTxConfirmed(sdk, registerTxHash); // 3. Use the session to send const usdc = STARKNET_CONTRACTS["USDC"]; const txHash = await sdk.executeTransactionWithSession({ params: { encryptKey: userPin, wallet: userWallet, session, calls: [ { contractAddress: usdc.contractAddress, entrypoint: "transfer", calldata: [recipient, "0x" + amountUsdc.toString(16), "0x0"], }, ], }, }); // 4. Revoke immediately await sdk.sessions.revokeSessionKey( { encryptKey: userPin, wallet: userWallet, sessionPublicKey: session.publicKey, }, process.env.CHIPI_SECRET_KEY!, ); return txHash; } // Helper: poll Starknet until a tx reaches a terminal status. // Starknet blocks settle in ~20-40s, so plan for up to ~60s in practice. async function waitForTxConfirmed(sdk: ChipiServerSDK, txHash: string) { const deadline = Date.now() + 90_000; // 90s budget while (Date.now() < deadline) { const status = await sdk.getTransactionStatus({ txHash }); if (status.executionStatus === "SUCCEEDED") return; if (status.executionStatus === "REVERTED") { throw new Error(`tx ${txHash} reverted`); } await new Promise((r) => setTimeout(r, 3_000)); } throw new Error(`tx ${txHash} did not confirm within 90s`); } ``` For production, prefer a webhook (configured at `/configure/notifications`) over polling — you can register the session key, return a 202 to your user, and trigger `executeTransactionWithSession` from the webhook handler when the register tx confirms. > ✅ Verified against the live API on **2026-05-11**. # Add a Second Device or Co-Signer Source: https://docs.chipipay.com/services/gasless/threshold Let one wallet be controlled by more than one signer — a passkey on the phone plus a passkey on the laptop, or a user plus an agent. Same on-chain account, multiple ways to approve. A Chipi wallet starts with one owner: whatever passkey or signer the user registered at sign-up. This page covers the two ways to add more signers to that wallet: 1. **Additional devices that each act on their own.** Add the user's laptop passkey alongside their phone passkey. Either device can sign a transfer. This is what most consumer apps want. 2. **Co-signing where multiple signers must approve.** Require two signatures (or three of four, etc.) before any transfer goes through. This is what agent treasuries, shared wallets, and high-value accounts want. Both routes share the same on-chain account. You're not creating a second wallet — you're widening who controls the existing one. Available without advertisement until external audit closes. The on-chain logic shipped with SHHH V8.4 and has a mainnet smoke — see [scripts/receipts/](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts). ## When to use which | Your use case | Path | | ----------------------------------------------------------------------- | ------------------------------------------------------------------- | | User wants their phone + their laptop to both work | Independent owners (each device signs on its own) | | User wants a backup passkey on a second device "in case" | Independent owners; same as above | | Agent spends from a treasury you co-control | Co-signing — typically 2-of-2 above a spend threshold, 1-of-2 below | | Shared org wallet across finance + ops | Co-signing — set the threshold to match your governance | | High-value personal account where any single device shouldn't be enough | Co-signing — 2-of-3 between phone + laptop + recovery contact | Concrete examples in production today: 1. **Agent treasuries** — an automated agent spends up to \$X/day on its own, anything bigger requires a co-sign from the user's passkey. 2-of-2 above the threshold, 1-of-2 below. 2. **AI-API service accounts** — a service account shares custody with a human operator. Routine API spend doesn't prompt; settlement transfers do. 3. **Co-signed org treasuries** — Chipi credits balance is N-of-M with key roles distributed across finance + ops + the founder. ## Compared to single-owner Single-owner SHHH wallets sign via one V2\_SNIP12 envelope: ``` [V2_SNIP12_PREFIX, owner_id, kind_tag, ...payload] ``` A threshold wallet signs via a threshold envelope wrapping N inner envelopes: ``` [ V2_THRESHOLD_PREFIX, threshold, // N num_owner_sigs, // count of inner envelopes that follow ...inner_envelope_1, ...inner_envelope_2, ... ] ``` Each inner envelope can be a **different signer kind**. The wallet validates each inner envelope against its registered verifier class, counts the verified ones, and admits the call only when `verified_count ≥ threshold`. The set of owners and the threshold N live on-chain. ## Owner kinds within a threshold Anything that works as a single-owner signer kind works as a threshold owner. You can mix: * An EOA on MetaMask (`EIP191_SECP256K1`) co-signs with a passkey (`WEBAUTHN_P256`) * A Phantom / Solana wallet (`ED25519`) co-signs with a STARK key held server-side * A JWT\_ES256 service account co-signs with a STARK key held server-side * A guardian (`role: "GUARDIAN"`) does NOT count toward the threshold for normal transactions — guardians only initiate recovery, never co-sign ## Configuring N and M The wallet is created with N initial owners (M=N at start). Add or remove owners post-creation via the recovery flows in [recovery](/services/gasless/recovery): * `propose_add_owner` → `execute_add_owner` after 48h * `propose_remove_owner` → `execute_remove_owner` after 24h * `propose_set_threshold` → `execute_set_threshold` after 48h Threshold changes are timelocked because they're governance-grade — a malicious add-owner that immediately set threshold=1 would be a wallet takeover. The 48h window gives the other owners a chance to `cancel_pending_op`. ## SDK status | Surface | Status | Notes | | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Backend builders (`buildThresholdEnvelope`, `buildProposeSetThresholdCall`, `buildExecuteSetThresholdCall`) | Shipped | See `@chipi-stack/backend` exports | | Python builders | Shipped | Mirrors TS — see `chipi_sdk.shhh.threshold` | | React hook | `useGuardianRecovery().buildProposeSetThreshold` / `.buildExecuteSetThreshold` cover the governance side | The N-of-M signature assembly hook (`useThresholdSign`) ships next | | Mainnet smoke | Shipped (view-shape + parser) | Real-money smoke deferred until external audit closes | ```ts theme={null} import { buildThresholdEnvelope, buildStarkEnvelope, buildEip191EnvelopeFromSignature, } from "@chipi-stack/backend"; // Two owners co-sign the same OE. const innerStark = buildStarkEnvelope({ privateKey: serverStarkKey, messageHash: oeHash }); const innerEip191 = buildEip191EnvelopeFromSignature({ signatureHex: metaMaskPersonalSig, pubkey: { ethAddress: userMetaMaskAddress }, }); const envelope = buildThresholdEnvelope({ threshold: 2, innerEnvelopes: [innerStark, innerEip191], }); // Feed `envelope` into the same execute_from_outside_v2 calldata path // you'd use for a single-owner OE. ``` ### External wallets that don't expose a key (MetaMask, Phantom) MetaMask and Phantom never hand you a private key — the user signs in their wallet and you get a signature back. Each has a `*FromSignature` builder so you can wrap that signature into an inner envelope: | Wallet | Kind | Builder | Sync? | | --------------------------- | ------------------ | ----------------------------------- | ---------------------------- | | MetaMask / any EVM EOA | `EIP191_SECP256K1` | `buildEip191EnvelopeFromSignature` | sync | | Phantom / any Solana wallet | `ED25519` | `buildEd25519EnvelopeFromSignature` | **async** (Garaga WASM init) | For Phantom, the wallet must sign the **exact bytes** returned by `ed25519SignedBytes(messageHash)` — the 64-byte hex-ASCII encoding of the OE hash — not the raw 32-byte hash. The on-chain verifier reconstructs those same bytes, so signing anything else fails verification. ```ts theme={null} import { buildEd25519EnvelopeFromSignature, ed25519SignedBytes, } from "@chipi-stack/backend"; // 1. Connect Phantom and read its 32-byte Ed25519 pubkey. await window.solana.connect(); const publicKey = window.solana.publicKey.toBytes(); // 2. Sign the EXACT bytes the verifier expects (not the raw hash). const msg = ed25519SignedBytes(oeHash); const { signature } = await window.solana.signMessage(msg); // 64-byte R‖s // 3. Wrap into an inner envelope (async — initializes Garaga on first call). const innerEd25519 = await buildEd25519EnvelopeFromSignature({ signature, publicKey, messageHash: oeHash, }); // `innerEd25519` slots into `buildThresholdEnvelope({ innerEnvelopes: [...] })` // exactly like the EIP-191 envelope above. ``` ## Gas The paymaster sums the per-kind gas overhead across each inner envelope. A 2-of-2 STARK + EIP-191 OE budgets 2M + 10M = 12M l2\_gas for verification on top of the call's own cost. See the [passkey API reference](/sdk/passkeys/api) for the kinds available client-side; gas tables live with the backend builders. This composes — you don't budget gas yourself. The paymaster reserves it automatically based on the envelope shape. ## Threshold + recovery together The headline product story for SHHH V8.4 is threshold combined with guardian recovery: * **Day-to-day operations** require N-of-M signatures * **Lost-key recovery** still works because a guardian can initiate `initiate_recovery` even if N-1 owners have lost their keys A 2-of-3 wallet with one guardian = three signers required day-to-day; one guardian can recover the wallet to fresh owners if two are lost. This is the closest thing to "bank-grade custody without giving up custody" on Starknet today. ## Related * [Passkey reference](/sdk/passkeys/api) — what each owner can be on the browser side * [Recovery](/services/gasless/recovery) — add/remove owners + guardian recovery * [Migration](/services/gasless/migration) — getting from CHIPI v29 to SHHH first # Transaction History — Node.js Source: https://docs.chipipay.com/services/gasless/transactions Paginate a wallet's transaction history, filter by date, and stamp external transaction hashes into Chipi's records. Transactions executed through the SDK (`transfer`, `purchaseSku`, `executeTransactionWithSession`, etc.) are automatically recorded against the wallet. This page covers reading that history and recording transactions executed outside the SDK. ## List transactions `getTransactionList` returns a paginated history of transactions for a wallet. `walletAddress` is required; everything else is optional. ```ts theme={null} const result = await sdk.getTransactionList({ walletAddress: "0x...", page: 1, limit: 20, }); console.log(result.total); // total transactions for this wallet console.log(result.totalPages); console.log(result.data[0].transactionHash); ``` The response shape is `PaginatedResponse`: ```ts theme={null} { data: Transaction[]; total: number; page: number; limit: number; totalPages: number; } ``` ## Filter by date `year`, `month`, and `day` narrow the list to a specific calendar window. Useful for monthly reports or per-day audits. ```ts theme={null} const now = new Date(); // All transactions in the current month const thisMonth = await sdk.getTransactionList({ walletAddress: userWallet.publicKey, year: now.getFullYear(), month: now.getMonth() + 1, // 1-12, JS Date months are 0-indexed page: 1, limit: 100, }); // All transactions on a specific day const oneDay = await sdk.getTransactionList({ walletAddress: userWallet.publicKey, year: 2026, month: 5, day: 7, }); ``` You can also filter by the called function with `calledFunction` (e.g., `"transfer"`, `"approve"`). ## Record an external transaction `recordSendTransaction` stamps a transaction that was **executed outside** the SDK into Chipi's records. Use it when a wallet sent a transfer through some other path (a wallet UI, another service) and you want it to show up in `getTransactionList` and the dashboard. ```ts theme={null} import { Chain, ChainToken } from "@chipi-stack/backend"; const result = await sdk.recordSendTransaction({ params: { transactionHash: "0x...", // the on-chain tx hash expectedSender: senderPublicKey, expectedRecipient: recipientPublicKey, expectedToken: ChainToken.USDC, expectedAmount: "1.5", // human-formatted decimal string chain: Chain.STARKNET, }, }); ``` `expectedAmount` is a **decimal string in human units** (e.g., `"1.5"` for 1.5 USDC), not raw token units — different from `getTokenBalance.balance` which returns raw units. The endpoint validates the on-chain `Transfer` event matches your `expected*` fields. If the tx hash isn't yet confirmed, or the actual transfer doesn't match what you claimed, the call rejects. ## Putting it together A monthly statement endpoint for a user's gasless wallet: ```ts theme={null} import { ChipiServerSDK } from "@chipi-stack/backend"; async function monthlyStatement( sdk: ChipiServerSDK, walletAddress: string, year: number, month: number, ) { const all: Awaited>["data"] = []; let page = 1; let totalPages = 1; while (page <= totalPages) { const r = await sdk.getTransactionList({ walletAddress, year, month, page, limit: 100, }); all.push(...r.data); totalPages = r.totalPages; page++; } return { walletAddress, year, month, count: all.length, transactions: all }; } ``` > ✅ Verified against the live API on **2026-05-11**. # Cross-Chain (CCTP) Source: https://docs.chipipay.com/services/gift-cards/cross-chain How gift cards bridge USDC from Starknet to Solana via Circle's CCTP V2. ## How it works Gift card credits live on Starknet. When a recipient redeems to Solana, USDC is bridged via [Circle's CCTP V2](https://developers.circle.com/stablecoins/cctp-getting-started) (Cross-Chain Transfer Protocol): ``` 1. Starknet: USDC burned via TokenMessenger.depositForBurn 2. Circle: attestation signers verify the burn (~5 sec fast mode) 3. Solana: USDC minted to recipient's wallet via receiveMessage ``` Total time: **\~24 seconds** end-to-end (burn + attestation + Solana confirmation). ## Fast vs Standard mode Each Gift carries a `cctpFast` flag that controls Solana redeem speed: | Mode | Attestation time | Fee | Best for | | -------------------------- | ---------------- | ------------------- | ---------------------------------------------- | | `cctpFast: true` (default) | \~5 seconds | \~\$0.01 per redeem | Real-time claims, good UX | | `cctpFast: false` | 4-8 hours | Free | Batch distributions where speed doesn't matter | Fast mode pays a small fee to Circle's attestation service for priority. Standard mode waits for Starknet's ZK proof to post to Ethereum L1. Single-create (`POST /v1/gifts`) does **not** accept `cctpFast` in the request today. The gift is always created with `cctpFast: true` (the schema default). To set `cctpFast: false`, use the bulk endpoint (`POST /v1/gifts/bulk`) which accepts the flag, or the dashboard's bulk-airdrop flow. ## Supported chains | Destination | Status | Time | | ------------ | ----------- | ------------------------------- | | **Starknet** | Live | Instant (direct ERC20 transfer) | | **Solana** | Live | \~24 sec (CCTP V2) | | Base | Coming soon | — | | Arbitrum | Coming soon | — | | Ethereum | Coming soon | — | ## Redeem flow for Solana ```typescript theme={null} // 1. Redeem — returns 202 with redeemId const res = await fetch("https://api.chipipay.com/v1/gifts/claim/CODE/redeem", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ walletAddress: "FZQq6KCS...", // Solana wallet pubkey chain: "solana", }), }); const { redeemId, status } = await res.json(); // status = "BRIDGING" // 2. Poll until SUCCESS while (true) { const statusRes = await fetch( `https://api.chipipay.com/v1/gifts/claim/CODE/status/${redeemId}` ); const data = await statusRes.json(); if (data.status === "SUCCESS") { console.log("USDC received on Solana:", data.receiveChainTxHash); break; } if (data.status === "FAILED") { console.error("Bridge failed. Burn tx:", data.cctpBurnTxHash); break; } await new Promise(r => setTimeout(r, 5000)); // poll every 5 sec } ``` ## Status flow ``` PENDING → BRIDGING → ATTESTED → SUCCESS ↘ FAILED (if bridge times out or Solana tx fails) ``` | Status | Meaning | | ---------- | ---------------------------------------------------------------------- | | `PENDING` | Redeem record created, not yet sent | | `BRIDGING` | CCTP burn submitted on Starknet, waiting for Circle attestation | | `ATTESTED` | Circle attestation ready, Solana receive pending | | `SUCCESS` | USDC delivered to recipient's wallet | | `FAILED` | Something went wrong — check `cctpBurnTxHash` for the Starknet burn tx | ## Technical details * **CCTP V2 contracts**: TokenMessenger on Starknet (`0x07d421...`), MessageTransmitter on Solana (`CCTPV2Sm4...`) * **Domain IDs**: Starknet = 25, Solana = 5 * **Mint recipient**: must be the Solana USDC Associated Token Account (ATA), not the wallet pubkey * **Circle attestation API**: `https://iris-api.circle.com/v2/messages/25?transactionHash={txHash}` * **Server relay**: Chipi's server submits the `receiveMessage` transaction on Solana and pays the SOL gas fee The recipient doesn't need SOL for gas — Chipi's relay pays for the Solana transaction. # Dashboard & Bulk Airdrop Source: https://docs.chipipay.com/services/gift-cards/dashboard Create gift codes, send bulk emails, and manage campaigns from the dashboard — no code needed. ## Dashboard overview The Gift Cards dashboard at [dashboard.chipipay.com](https://dashboard.chipipay.com) lets you create, manage, and monitor gift codes without writing code. Navigate to your org → **Gift Cards** service → you'll see three tabs: ### Usage tab Real-time metrics for your gift card activity: * **Distributed (\$)** — total USDC sent to recipients * **Redeemed** — number of successful redemptions * \*\*Fees Paid ($)** — total redeem fees ($0.01 per redeem) * **Codes Active** — gift codes that are still redeemable * **By chain** — Starknet vs Solana breakdown * **Recent redeems** — table with wallet, chain, amount, status, and transaction link ### Codes tab All your gift codes in one table: * **Utilization** — progress bar showing `redeems/maxRedeems` * **Status** — Active, Expired, Exhausted, or Inactive * **Copy link** — one-click copy of the claim URL * **Create Gift** button — opens the create dialog ### Integration tab Code snippets for the full flow: 1. Create a gift 2. Validate a code 3. Create wallet (if needed) 4. Redeem to Starknet (instant) 5. Redeem to Solana (CCTP) 6. Poll cross-chain status *** ## Creating a single gift Click **Create Gift** in the Codes tab → **Single** mode: | Field | Description | | --------------- | ------------------------------------------------------------- | | Code | Custom code (e.g. `WELCOME5`) or leave empty to auto-generate | | Name | Human-readable name (e.g. "Welcome Bonus") | | Amount | USDC per redemption | | Max Redeems | Total number of times the code can be used | | Recipient Email | Optional — sends a claim email to this address | After creating, the **claim URL is copied to your clipboard** automatically. *** ## Bulk airdrop Click **Create Gift** → **Bulk Airdrop** mode: 1. Enter a **campaign name** (e.g. "May Airdrop") 2. Set the **amount per recipient** 3. Upload a **CSV file** with an `email` column (optional `code` column for custom codes) ```csv theme={null} email,code alice@example.com, bob@example.com,BOB_SPECIAL charlie@example.com, ``` 4. Preview the recipients and total cost 5. Click **Send N Gift Cards** Each recipient receives an email: > 🎁 You received \$5.00 USDC from **Your App Name** > > \[Claim Your Gift] The email links to `dashboard.chipipay.com/claim/CODE` — a public page where they enter their wallet address and claim. *** ## Claim page The claim page at `/claim/CODE` is **public** — no login, no app download, no API key needed. The flow for the recipient: 1. Opens the claim link (from email, SMS, QR code, or social media) 2. Sees the gift amount and sender name 3. Enters their wallet address (Starknet or Solana) 4. Chooses a chain 5. Clicks Claim → USDC arrives in their wallet **Starknet:** instant (\~3 seconds) **Solana:** \~24 seconds via CCTP bridge *** ## Costs | Action | Cost | | ------------------------- | ------------------------------------------------------------ | | Create a gift | Free (credits held, not spent) | | Bulk create + email | Free (credits held, email via Resend) | | Each redemption | **\$0.01 flat fee** + gift amount consumed from held credits | | Delete unused gift | Free (held credits released) | | Validate / status poll | Free | | Cross-chain (Solana fast) | \~\$0.01 additional CCTP fee | *** ## Limits Admins can set per-org limits: | Limit | Description | Default | | ------------------ | ---------------------------------- | -------- | | Max value per code | Maximum USDC amount per gift | No limit | | Monthly cap | Maximum USDC distributed per month | No limit | | Redeem fee | Fee per redemption | \$0.01 | These are configured by the Chipi admin. Contact support to adjust. *** ## Compliance Gift card distributions may be subject to money transmission, KYC, and tax reporting requirements in your jurisdiction. Chipi collects the following data for compliance purposes: * Recipient wallet address * Destination chain * IP address (retained for 90 days) * Transaction hashes (permanent) * Redeem fees (permanent) Compliance flags are generated automatically for: * Single gifts > \$100 * Org distributions > \$1,000/month * Cross-chain transfers > \$50 * Same wallet redeeming > 5 times in 7 days These flags are reviewed by the Chipi compliance team and do not block redemptions. # Endpoints Source: https://docs.chipipay.com/services/gift-cards/endpoints All Gift Cards API endpoints — create, validate, redeem, bulk, status. Base URL: `https://api.chipipay.com/v1` *** ## POST /gifts Create a redeemable gift code. Credits are held from your org balance. **Auth:** `Authorization: Bearer sk_...` or `x-api-key: pk_...` **Cost:** No per-call fee. Credits held = `amount × maxRedeems + gas estimate`. ```bash Request theme={null} curl -X POST https://api.chipipay.com/v1/gifts \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "WELCOME5", "name": "Welcome Bonus", "amount": 5, "token": "USDC", "chain": "STARKNET", "type": "GIFT_CARD", "maxRedeems": 100, "maxRedeemsPerUser": 1, "startsAt": "2026-01-01T00:00:00Z", "isActive": true }' ``` ```json Response (201) theme={null} { "id": "a9bf75ca-de8b-489a-bdb0-88767bd7a47a", "code": "WELCOME5", "name": "Welcome Bonus", "amount": "5", "token": "USDC", "chain": "STARKNET", "type": "GIFT_CARD", "maxRedeems": 100, "maxRedeemsPerUser": 1, "totalRedeems": 0, "startsAt": "2026-01-01T00:00:00.000Z", "expiresAt": null, "isActive": true, "cctpFast": true, "holdLedgerEntryId": null, "totalHeldUsd": null, "totalRedeemedUsd": null, "recipientEmail": null, "campaignName": null } ``` | Field | Type | Default | Description | | ------------------- | ------- | ------------- | --------------------------------------------------------- | | `code` | string | Required | Unique gift code. Auto-generated if using bulk. | | `name` | string | Required | Human-readable name. | | `amount` | number | Required | USDC amount per redemption. | | `token` | string | `"USDC"` | Only USDC supported. | | `chain` | string | `"STARKNET"` | Source chain for credits. | | `type` | string | `"GIFT_CARD"` | GIFT\_CARD, PROMOTIONAL, REFERRAL, LOYALTY, COMPENSATION. | | `maxRedeems` | number | Required | Total redemption limit across all users. | | `maxRedeemsPerUser` | number | Required | Max redeems per wallet address. | | `startsAt` | string | Required | ISO 8601 date when the code becomes active. | | `expiresAt` | string | null | Optional expiration date. | | `isActive` | boolean | Required | Can be toggled to disable the code. | `cctpFast` (fast vs standard CCTP for Solana redeems) is **not configurable on single-create** today. The Gift row defaults to `cctpFast: true` server-side. To override, use `POST /gifts/bulk` (which does accept `cctpFast` in the request body) or the dashboard's bulk-airdrop flow. *** ## POST /gifts/bulk Create multiple gift codes + send claim emails. One gift per recipient. **Auth:** `Authorization: Bearer sk_...` or `x-api-key: pk_...` **Max:** 500 recipients per batch. ```bash Request theme={null} curl -X POST https://api.chipipay.com/v1/gifts/bulk \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "May Airdrop", "amount": 5, "token": "USDC", "chain": "STARKNET", "type": "GIFT_CARD", "cctpFast": true, "recipients": [ {"email": "alice@example.com"}, {"email": "bob@example.com", "code": "BOB_SPECIAL"} ] }' ``` ```json Response (201) theme={null} { "created": 2, "campaignName": "May Airdrop", "totalHeldUsd": 10.01, "gifts": [ {"code": "A1B2C3D4", "email": "alice@example.com", "claimUrl": "https://dashboard.chipipay.com/claim/A1B2C3D4"}, {"code": "BOB_SPECIAL", "email": "bob@example.com", "claimUrl": "https://dashboard.chipipay.com/claim/BOB_SPECIAL"} ] } ``` Each recipient receives an email with a "Claim Your Gift" button linking to the claim page. Codes are auto-generated (8-char uppercase) unless you provide a custom code. *** ## GET /gifts/claim/:code Validate a gift code. **No auth required** — the code is the credential. Used by the claim page to show "You received \$5 USDC from Org Name" before the user enters their wallet. ```bash theme={null} curl https://api.chipipay.com/v1/gifts/claim/WELCOME5 ``` ```json Response (200) theme={null} { "valid": true, "gift": { "code": "WELCOME5", "name": "Welcome Bonus", "amount": "5", "token": "USDC", "chain": "STARKNET" }, "orgName": "My App" } ``` Invalid codes return: ```json theme={null} {"valid": false, "reason": "Gift has expired"} {"valid": false, "reason": "Gift redemption limit reached"} {"valid": false, "reason": "Gift not found"} ``` Returns `{ "valid": false, "reason": "Gift has expired" }` if the code is invalid. *** ## POST /gifts/claim/:code/redeem Redeem a gift code — sends USDC to the provided wallet address. **No auth required.** ```bash Starknet (instant) theme={null} curl -X POST https://api.chipipay.com/v1/gifts/claim/WELCOME5/redeem \ -H "Content-Type: application/json" \ -d '{"walletAddress": "0x...", "chain": "starknet"}' ``` ```bash Solana (CCTP, ~24 sec) theme={null} curl -X POST https://api.chipipay.com/v1/gifts/claim/WELCOME5/redeem \ -H "Content-Type: application/json" \ -d '{"walletAddress": "FZQq...", "chain": "solana"}' ``` | Chain | Response | Time | | ---------- | ---------------------------------- | -------- | | `starknet` | 201 — instant, tx hash in response | \~3 sec | | `solana` | 202 — CCTP bridge, poll status | \~24 sec | **Starknet response (201):** ```json theme={null} { "giftRedeem": { "id": "...", "status": "SUCCESS", "transactionHash": "0x...", "walletAddress": "0x...", "amount": "5" } } ``` **Solana response (202):** ```json theme={null} { "redeemId": "...", "status": "BRIDGING", "destinationChain": "solana", "cctpBurnTxHash": "0x...", "estimatedSeconds": 24 } ``` *** ## GET /gifts/claim/:code/status/:redeemId Poll cross-chain redeem status. **No auth required.** ```bash theme={null} curl https://api.chipipay.com/v1/gifts/claim/WELCOME5/status/REDEEM_ID ``` ```json theme={null} { "id": "...", "status": "SUCCESS", "destinationChain": "solana", "cctpBurnTxHash": "0x...", "receiveChainTxHash": "3xxK3y...", "amount": 5 } ``` Status flow: `PENDING → BRIDGING → ATTESTED → SUCCESS` If `FAILED`: the Starknet burn tx is in `cctpBurnTxHash` — the funds may still be recoverable. *** ## GET /gifts List all gift codes for your org. **Auth:** Required (sk\_/pk\_) ```bash theme={null} curl https://api.chipipay.com/v1/gifts \ -H "Authorization: Bearer sk_prod_YOUR_KEY" ``` *** ## PATCH /gifts/:id Update a gift (toggle active, change name, expiry). **Auth:** Required (sk\_/pk\_) *** ## DELETE /gifts/:id Delete a gift and release held credits. **Auth:** Required (sk\_/pk\_) *** ## Error handling | HTTP Status | Meaning | | ----------- | ------------------------------------------------- | | 201 | Gift created / Starknet redeem success | | 202 | Cross-chain redeem initiated (poll status) | | 400 | Invalid code, limit reached, insufficient credits | | 404 | Gift or redeem not found | | 500 | Server error (check manager wallet balance) | # Gift Cards Source: https://docs.chipipay.com/services/gift-cards/overview Send USDC to anyone via a redeemable code. Funded from your Chipi credits balance; recipient claims on Starknet or Solana — cross-chain bridging happens automatically. Gift Cards are **live**. You pre-fund Chipi credits in USDC, mint a redeemable `code`, share it however you want, and the recipient claims into a Starknet or Solana wallet. Cross-chain delivery uses Circle's CCTP bridge under the hood. ## At a glance USDC held on Chipi's Starknet treasury; created code is a claim on that hold Recipient picks Starknet or Solana at redeem time `maxRedeems` (total) + `maxRedeemsPerUser` PROMOTIONAL, GIFT\_CARD, REFERRAL, LOYALTY, COMPENSATION ## How it works 1. **You create a gift code** via `POST /v1/gifts` with an amount, expiry, and per-user/total redeem caps. Chipi holds the total potential payout (`amount × maxRedeems`) from your credits balance on creation, so no over-spend. 2. **You share the code** any way you like — email, link in your app, QR. The `code` is the redeem key. 3. **A recipient redeems** via `POST /v1/gifts/claim/:code/redeem` with their wallet address and target chain. Starknet recipients receive instantly; Solana recipients go through CCTP burn → Circle attestation → mint on Solana (status transitions visible via the redeem polling endpoint). The redeem endpoint is public (no API key required) so the code itself is the access token — share carefully. ## Endpoints All under `/v1/gifts/*` on `https://api.chipipay.com`. Org-facing endpoints (create, list, stats) use `ApiKeyGuard` (sk\_ in `Authorization` OR pk\_ in `x-api-key`); claim/status endpoints are public. | Method + path | Auth | Purpose | | -------------------------------------------- | ----------- | -------------------------------------------------- | | `POST /v1/gifts` | ApiKeyGuard | Create a gift code | | `POST /v1/gifts/bulk` | ApiKeyGuard | Create N codes in one call | | `GET /v1/gifts` | ApiKeyGuard | List your org's codes | | `GET /v1/gifts/:id` | ApiKeyGuard | Single code detail | | `GET /v1/gifts/usage` | ApiKeyGuard | Aggregate redeem stats for your org | | `GET /v1/gifts/history/redeems` | ApiKeyGuard | List redeems against your codes | | `GET /v1/gifts/history/redeems/:id` | ApiKeyGuard | Single redeem detail | | `GET /v1/gifts/claim/:code` | Public | Recipient previews the code (amount, expiry, etc.) | | `POST /v1/gifts/claim/:code/redeem` | Public | Recipient claims into their wallet | | `GET /v1/gifts/claim/:code/status/:redeemId` | Public | Recipient polls redeem status | | `GET /v1/gifts/redeems/:id/status` | ApiKeyGuard | Org-side polling on a specific redeem | ## Cross-chain delivery | Recipient picks | Path | Typical latency | | ------------------- | ----------------------------------------------------------- | -------------------------------------------- | | `chain: "starknet"` | Direct transfer from Chipi's Starknet treasury | Seconds (one Starknet tx) | | `chain: "solana"` | CCTP burn on Starknet → Circle attestation → mint on Solana | 60–120 seconds (Circle attestation gates it) | The Solana path tracks state through `GiftRedeemStatus`: `PENDING` → `BRIDGING` (CCTP burn submitted) → `ATTESTED` (attestation received from Circle) → `SUCCESS` (USDC minted on Solana). Recipients poll the public status endpoint. If anything fails between BRIDGING and SUCCESS, the redeem moves to `FAILED` and the hold against your credits is released. You're not charged for failed redeems. ## Gift types The `type` field on every gift is one of: * `PROMOTIONAL` — marketing campaigns, sign-up bonuses * `GIFT_CARD` — explicit gift product (e.g. a \$25 USDC card) * `REFERRAL` — referral rewards * `LOYALTY` — repeat-customer rewards * `COMPENSATION` — refunds or make-goods Chipi treats them identically functionally; the field exists for your own analytics + compliance reporting. ## Where to go next * **[Quickstart](/services/gift-cards/quickstart)** — first `POST /v1/gifts` call to create a code, then claim it. * **[Endpoints](/services/gift-cards/endpoints)** — full request/response shapes per route. * **[Cross-chain](/services/gift-cards/cross-chain)** — CCTP wire details, Solana recipient flow. * **[Dashboard](/services/gift-cards/dashboard)** — org-side admin: bulk create, view stats, retire codes. # Quickstart Source: https://docs.chipipay.com/services/gift-cards/quickstart Send USDC to anyone via a redeemable code. No SDK needed — just HTTP. ## 1. Get your API key Go to [dashboard.chipipay.com](https://dashboard.chipipay.com), create an org, and copy your secret key from the Credentials page. ## 2. Create a gift code ```bash cURL theme={null} curl -X POST https://api.chipipay.com/v1/gifts \ -H "Authorization: Bearer sk_prod_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "WELCOME5", "name": "Welcome Bonus", "amount": 5, "token": "USDC", "chain": "STARKNET", "type": "GIFT_CARD", "maxRedeems": 100, "maxRedeemsPerUser": 1, "startsAt": "2026-01-01T00:00:00Z", "isActive": true }' ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.chipipay.com/v1/gifts", { method: "POST", headers: { "Authorization": "Bearer sk_prod_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ code: "WELCOME5", name: "Welcome Bonus", amount: 5, token: "USDC", chain: "STARKNET", type: "GIFT_CARD", maxRedeems: 100, maxRedeemsPerUser: 1, startsAt: new Date().toISOString(), isActive: true, }), }); const gift = await response.json(); console.log("Claim URL:", `https://dashboard.chipipay.com/claim/${gift.code}`); ``` Credits are held from your org balance when you create the gift. They're consumed one-by-one as recipients redeem. ## 3. Share the claim link Send your recipients this URL: ``` https://dashboard.chipipay.com/claim/WELCOME5 ``` They'll see the gift amount, enter their wallet address, choose a chain, and claim — no account or app download needed. **Building wallet creation into your app?** Use the [@chipi-stack/chipi-passkey](/sdk/passkeys/overview) package to create self-custodial wallets with biometrics. Recipients get a wallet + their gift in one flow. ## 4. Or redeem via API If you're building the redemption into your own app: ```bash theme={null} # 1. Validate the code (show "You have $5 USDC!") curl https://api.chipipay.com/v1/gifts/claim/WELCOME5 # 2. Redeem to a wallet curl -X POST https://api.chipipay.com/v1/gifts/claim/WELCOME5/redeem \ -H "Content-Type: application/json" \ -d '{"walletAddress": "0x...", "chain": "starknet"}' ``` Starknet redeems are instant. Solana redeems take \~24 seconds via CCTP. ## 5. Buy credits Gift card redeems debit from your org's credit balance. Buy a credit pack ($2 / $5 / $25 / $50 / \$100) at [dashboard.chipipay.com/configure/billing](https://dashboard.chipipay.com/configure/billing) by sending USDC. ## What you can build | Use case | How | | ------------------------ | -------------------------------------- | | User onboarding | Airdrop \$5 USDC to new signups | | Referral rewards | Give referrers a redeemable code | | Loyalty program | Monthly USDC rewards for active users | | Promotional campaigns | Bulk email 500 recipients via CSV | | Cross-chain distribution | Recipients claim on Starknet or Solana | ## Auth | Method | Header | Best for | | ---------- | ----------------------------------- | ----------------------------------------- | | Secret key | `Authorization: Bearer sk_prod_...` | Server-side (create, list, manage) | | No auth | None | Public claim endpoints (validate, redeem) | The claim endpoints (`/gifts/claim/:code/*`) are public — the code itself is the credential. Keep your gift codes as secret as you'd keep a password. ## Next * [All endpoints](/services/gift-cards/endpoints) — create, validate, redeem, bulk, status * [Cross-chain (CCTP)](/services/gift-cards/cross-chain) — how Starknet → Solana bridging works * [Dashboard & bulk airdrop](/services/gift-cards/dashboard) — create + manage from the UI