# OnChainDB - Collective Intelligence Database on Celestia OnChainDB is a decentralized database system built on Celestia's Data Availability layer. If Ethereum is a World VM, OnChainDB is a Collective Intelligence Database - a single, queryable data layer where apps collaborate, earn together, and grow the pie instead of fighting over slices. It provides a complete TypeScript SDK with broker protocol integration, advanced query capabilities, and automatic transaction management. ## What is OnChainDB? OnChainDB combines traditional database developer experience with blockchain-native storage, enabling cross-app data joins and automatic revenue sharing: - **Blockchain-native**: All data stored on Celestia with cryptographic verification - **Dual-Key Authentication**: App keys for writes, user keys for Auto-Pay reads - **HTTP 402 protocol**: Pay-per-query monetization for read operations - **x402 Payment Flow**: Automatic payment handling with callbacks - **Advanced queries**: Fluent query builder with complex logical operators - **Blob Storage**: Upload and retrieve binary files (images, videos, documents) - **Async operations**: Task-based processing with real-time status tracking - **Full type safety**: Complete TypeScript support with IntelliSense - **Event-driven**: Transaction and operation events for monitoring - **Broker integration**: Seamless integration with OnChainDB broker protocol ### Use Cases OnChainDB enables you to build: - E-commerce platforms with built-in revenue sharing - Ticketing and booking systems - Marketplace applications with automatic commission splits - Subscription services with verifiable payments - Decentralized applications (dApps) requiring transparent data storage - Systems needing cryptographic proof of data integrity - Applications requiring immutable audit trails ## Architecture ### Core Components ``` Application Layer ↓ @onchaindb/sdk (TypeScript SDK) ↓ OnChainDB Broker API ↓ Celestia Data Availability Layer ``` ### Key Features 1. **Type-Safe SDK**: Full TypeScript support with complete type definitions 2. **Dual-Key Auth**: App key (X-App-Key) for writes, User key (X-User-Key) for Auto-Pay 3. **Prisma-Style CRUD**: Familiar methods like createDocument, findUnique, updateDocument 4. **Query Builder with JOINs**: Fluent API with joinOne/joinMany for relational queries 5. **Aggregations**: Built-in count, sum, avg, min, max, distinctBy, and groupBy operations 6. **Materialized Views**: Pre-computed views with JOINs and GROUP BY aggregations 7. **PriceIndex**: Value-based payment model with automatic revenue splitting 8. **x402 Payment Callbacks**: Automatic payment flow for store operations 9. **Blob Storage**: Upload/retrieve binary files with metadata queries 10. **Transaction Management**: Automatic blockchain transaction handling with waitForConfirmation 11. **Event System**: Real-time events for transaction status tracking 12. **Batch Operations**: Efficient bulk data operations with progress tracking 13. **Task System**: Async operations with ticket-based status monitoring ## Quick Start ### Step 1: Create Your App 1. Go to [dashboard.onchaindb.io](https://dashboard.onchaindb.io) 2. Create a new App to get your `appId` 3. Navigate to the "Security" tab to generate your `appKey` ### Step 2: Install the SDK ```bash npm install @onchaindb/sdk # or yarn add @onchaindb/sdk # Alternative: install directly from GitHub npm install git+https://github.com/onchaindb/sdk-ts.git ``` ### Step 3: Create Indexes Creating indexes automatically creates the collections and sets up the data structure: ```typescript import { createClient } from '@onchaindb/sdk'; const client = createClient({ endpoint: 'https://api.onchaindb.io', appId: 'your-app-id', appKey: 'your-app-key' }); // Get database manager const db = client.database('your-app-id'); // Create indexes - this also creates the collections await db.createIndex({ name: 'idx_users_email', collection: 'users', field_name: 'email', index_type: 'hash', options: { unique: true } }); await db.createIndex({ name: 'idx_users_createdAt', collection: 'users', field_name: 'createdAt', index_type: 'btree' }); ``` **IMPORTANT: Indexes are Required for Production Use** You MUST create at least one index per collection before storing data. Without indexes: - Queries will be extremely slow (full collection scans) - The OnChainDB dashboard cannot display your collections - Your application will run in an unoptimized state Always create indexes on fields you will query frequently (e.g., user IDs, timestamps, foreign keys). ### Step 4: Store Data ```typescript const result = await client.store({ collection: 'users', data: [{ email: 'alice@example.com', name: 'Alice', createdAt: new Date().toISOString() }] }); console.log('Stored at block:', result.block_height); ``` ### Step 5: Query Data ```typescript const users = await client.query({ collection: 'users', limit: 10 }); console.log(`Found ${users.total} users`); ``` ### That's It! You now have a fully functional onchain database with indexed queries, automatic transaction management, and blockchain-backed data storage. --- ## Authentication ### Dual-Key System OnChainDB uses a dual-key authentication system: **App Key (X-App-Key header)** - Required for write operations - Identifies the application - Used for app-level permissions **User Key (X-User-Key header)** - Optional, enables Auto-Pay functionality - When user has granted authz to the broker, payments happen automatically - No payment callbacks needed when Auto-Pay is enabled ```typescript const client = createClient({ endpoint: 'http://localhost:9092', appId: 'my_app', appKey: 'app_xxx...', // For writes userKey: 'user_yyy...' // For Auto-Pay reads/writes }); ``` ## Database Management ### Collections & Indexes OnChainDB uses collections to organize data, similar to tables in traditional databases: ```typescript import { createClient } from '@onchaindb/sdk'; const client = createClient({ endpoint: 'http://localhost:9092', appKey: 'your-app-key', appId: 'your-app-id' }); // Get database manager const db = client.database('your-app-id'); // Create collection await db.createCollection('users', { namespace: 'users_ns', primary_column: 'id', sort_column: 'createdAt' }); // Create indexes for faster queries await db.createIndex({ name: 'idx_users_email', collection: 'users', field_name: 'email', index_type: 'hash', options: { unique: true } }); ``` ### Why Indexes Are Required **CRITICAL: Every collection MUST have at least one index for production use.** Without indexes: - **Slow queries**: All reads perform full collection scans, resulting in poor performance - **Dashboard invisible**: The OnChainDB dashboard discovers collections through index configurations - collections without indexes will not appear - **Unoptimized costs**: Full scans consume more resources and may cost more **Index Types:** - `hash` - Best for equality lookups (e.g., `WHERE email = 'user@example.com'`) - `btree` - Best for range queries and sorting (e.g., `WHERE createdAt > '2024-01-01'`) - `Price` - Special type for value-based payment models (PriceIndex) **When to create indexes:** 1. Create indexes BEFORE storing any data 2. Index all fields used in `whereField()` queries 3. Index foreign key fields used in relations 4. Index fields used for sorting ## Core Concepts ### Data Storage with x402 Payment Callback Store JSON documents on Celestia blockchain with automatic payment handling. **How it works:** The SDK automatically handles HTTP 402 responses internally. When you pass a payment callback as the second parameter to `store()`, the SDK: 1. Attempts the store operation 2. If the server returns 402 (payment required), the SDK invokes your callback with the payment quote 3. Your callback executes the blockchain payment and returns `{ txHash, network }` 4. The SDK automatically retries the store operation with the payment proof You do NOT need to catch 402 errors or make a second API call - the SDK handles the entire flow. ```typescript // Store with payment callback (x402 flow) const result = await client.store( { collection: 'posts', data: [{ title: 'My First Post', content: 'Stored on blockchain!', author: 'alice' }] }, // Payment callback - SDK invokes this when server returns 402 // quote is X402Quote with camelCase fields async (quote) => { console.log('Payment required:', quote.totalCostTia, 'TIA'); console.log('Pay to:', quote.brokerAddress); // Execute blockchain payment (e.g., via Keplr) const txHash = await wallet.sendTokens( quote.brokerAddress, quote.totalCostTia ); // Return format is required - SDK uses this to retry the store return { txHash: txHash, network: 'mocha-4' // or 'celestia' for mainnet }; }, true // waitForConfirmation ); // SDK automatically retried with payment proof - result contains confirmed data console.log(`Confirmed at block ${result.block_height}`); ``` ### Browser Environment: CORS Considerations When using the SDK in a browser, Celestia RPC/REST endpoints don't include CORS headers. You must create server-side API routes to proxy blockchain requests: ```typescript // Required API routes for browser-based apps: // /api/wallet/account-info - Proxy to Celestia REST API for account info // /api/wallet/broadcast - Proxy to Celestia REST API for tx broadcast // Example Next.js API route: /api/wallet/broadcast export async function POST(request: Request) { const { tx_bytes, mode } = await request.json(); const response = await fetch('https://api-mocha.pops.one/cosmos/tx/v1beta1/txs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tx_bytes, mode }) }); return Response.json(await response.json()); } ``` ### Keplr Wallet Integration When integrating with Keplr wallet for payments: ```typescript import { Registry } from '@cosmjs/proto-signing'; import { defaultRegistryTypes } from '@cosmjs/stargate'; import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; // Use Amino signer (more compatible) const signer = await window.keplr.getOfflineSignerOnlyAmino(chainId); const accounts = await signer.getAccounts(); // Create and sign transaction const registry = new Registry(defaultRegistryTypes); const signedTx = await client.sign( accounts[0].address, [sendMsg], fee, memo ); // Convert to protobuf and broadcast via REST API const txBytes = TxRaw.encode(signedTx).finish(); const txBytesBase64 = Buffer.from(txBytes).toString('base64'); // Broadcast through your API route (avoids CORS) const result = await fetch('/api/wallet/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tx_bytes: txBytesBase64, mode: 'BROADCAST_MODE_SYNC' }) }); ``` ### Store Without Payment Callback If Auto-Pay is enabled (userKey with authz), no callback needed: ```typescript // With Auto-Pay enabled, payment happens automatically const result = await client.store({ collection: 'data', data: [{ example: 'data' }] }); ``` ### Wait for Confirmation The `waitForConfirmation` option ensures data is confirmed on-chain before returning: ```typescript // Without confirmation (returns immediately with ticket) const quick = await client.store( { collection: 'products', data: products }, paymentCallback, false // Returns immediately ); console.log('Task ticket:', quick.ticket_id); // Check status later const status = await client.getTaskStatus(quick.ticket_id); console.log('Status:', status.status); // With confirmation (waits for blockchain) const confirmed = await client.store( { collection: 'products', data: products }, paymentCallback, true // Waits for on-chain confirmation ); console.log('Data confirmed at height:', confirmed.block_height); ``` ### Payment Flows OnChainDB supports multiple payment methods: ```typescript // Method 1: x402 Payment Callback (recommended) // quote is X402Quote with camelCase fields await client.store( { collection: 'data', data: [{ content: 'example' }] }, async (quote) => { const txHash = await wallet.pay(quote.brokerAddress, quote.totalCostTia); return { txHash, network: 'mocha-4' }; } ); // Method 2: Auto-Pay (when userKey has authz) // Just store without callback - payment is automatic await client.store({ collection: 'data', data: [{ content: 'example' }] }); // Method 3: Pre-paid with payment_tx_hash await client.store({ collection: 'data', data: [{ content: 'example' }], payment_tx_hash: 'ABC123...' }); // Method 4: Signed transaction (before broadcast) await client.store({ collection: 'data', data: [{ content: 'example' }], signed_payment_tx: { signed_tx_bytes: 'base64_encoded_tx', user_address: 'celestia1...', broker_address: 'celestia1broker...', amount_utia: 100000, purpose: 'data_storage' } }); ``` ## Blob Storage (Binary Files) OnChainDB supports uploading and retrieving binary files like images, videos, and documents. ### Upload Blob ```typescript // Browser upload with File object const fileInput = document.querySelector('input[type="file"]'); const file = fileInput.files[0]; const uploadResult = await client.uploadBlob({ collection: 'avatars', blob: file, metadata: { user_address: 'celestia1abc...', uploaded_by: 'alice', is_primary: true }, payment_tx_hash: 'ABC123...', user_address: 'celestia1abc...', broker_address: 'celestia1broker...', amount_utia: 100000 }); console.log('Blob ID:', uploadResult.blob_id); console.log('Track upload:', uploadResult.ticket_id); // Wait for upload completion const task = await client.waitForTaskCompletion(uploadResult.ticket_id); console.log('Upload complete!', task); ``` ### Node.js Upload with Buffer ```typescript const fs = require('fs'); const fileBuffer = fs.readFileSync('./image.jpg'); const uploadResult = await client.uploadBlob({ collection: 'images', blob: fileBuffer, metadata: { description: 'Product photo' }, payment_tx_hash: 'ABC123...', user_address: 'celestia1abc...', broker_address: 'celestia1broker...', amount_utia: 150000 }); ``` ### Retrieve Blob ```typescript // Retrieve and display image in browser const blob = await client.retrieveBlob({ collection: 'avatars', blob_id: 'blob_abc123' }); // Create object URL for displaying image const imageUrl = URL.createObjectURL(blob); document.querySelector('img').src = imageUrl; // Retrieve and save file in Node.js const buffer = await client.retrieveBlob({ collection: 'documents', blob_id: 'blob_xyz789' }); fs.writeFileSync('./downloaded-file.pdf', buffer); ``` ### Query Blob Metadata ```typescript // Query all blobs by user const userBlobs = await client.queryBlobMetadata('avatars', { user_address: 'celestia1abc...' }); // Access blob metadata for (const blob of userBlobs) { console.log('Blob ID:', blob.blob_id); console.log('Size:', blob.size_bytes); console.log('Type:', blob.content_type); console.log('Custom fields:', blob); } ``` ## Querying Data OnChainDB provides two approaches for querying data: 1. **Query Builder (Main API)** - Compose complex queries with full control over conditions, joins, aggregations, and selections 2. **Helper Methods** - Pre-built convenience methods like `findMany()`, `findUnique()` for common operations ### Helper Methods (Simple Queries) ```typescript // Query all records in a collection const allPosts = await client.query({ collection: 'posts' }); // Query with filters const alicePosts = await client.query({ collection: 'posts', filters: { author: 'alice' } }); // Pagination const page1 = await client.query({ collection: 'posts', limit: 10, offset: 0 }); ``` ### Query Builder (Main API) Use the fluent query builder for complex queries with full control over conditions, joins, aggregations, and selections: ```typescript import { LogicalOperator } from '@onchaindb/sdk'; // Complex query with logical operators const results = await client.queryBuilder() .collection('posts') .find(builder => LogicalOperator.And([ LogicalOperator.Condition(builder.field('published').equals(true)), LogicalOperator.Or([ LogicalOperator.Condition(builder.field('category').equals('tech')), LogicalOperator.Condition(builder.field('tags').contains('blockchain')) ]) ]) ) .select(selection => selection.field('title').field('content').field('author') ) .limit(20) .execute(); // Quick field queries const activeUsers = await client.queryBuilder() .collection('users') .whereField('status').equals('active') .selectFields(['id', 'name', 'email']) .limit(50) .execute(); ``` ## Aggregations QueryBuilder provides built-in aggregation methods for counting, summing, averaging, and grouping data. ### Basic Aggregations ```typescript // Count records const activeUsers = await client.queryBuilder() .collection('users') .whereField('active').equals(true) .count(); // Returns: number // Sum a numeric field const totalRevenue = await client.queryBuilder() .collection('orders') .whereField('status').equals('completed') .sumBy('amount'); // Returns: number // Calculate average const avgPrice = await client.queryBuilder() .collection('products') .whereField('category').equals('electronics') .avgBy('price'); // Returns: number // Find maximum value const highestPrice = await client.queryBuilder() .collection('products') .maxBy('price'); // Returns: T | null // Find minimum value const lowestPrice = await client.queryBuilder() .collection('products') .minBy('price'); // Returns: T | null // Get distinct values const categories = await client.queryBuilder() .collection('products') .distinctBy('category'); // Returns: string[] // Count distinct values const uniqueCategories = await client.queryBuilder() .collection('products') .countDistinct('category'); // Returns: number ``` ### Grouped Aggregations Use `groupBy()` to perform aggregations on groups of records: ```typescript // Count users by country const usersByCountry = await client.queryBuilder() .collection('users') .groupBy('country') .count(); // Returns: { "USA": 150, "UK": 75, "Germany": 50 } // Sum order amounts by category const salesByCategory = await client.queryBuilder() .collection('orders') .whereField('status').equals('completed') .groupBy('category') .sumBy('amount'); // Returns: { "electronics": 50000, "clothing": 25000 } // Average rating by product const avgRatingByProduct = await client.queryBuilder() .collection('reviews') .groupBy('productId') .avgBy('rating'); // Returns: { "prod_1": 4.5, "prod_2": 3.8 } // Max/Min by group const maxPriceByCategory = await client.queryBuilder() .collection('products') .groupBy('category') .maxBy('price'); // Returns: { "electronics": 999, "books": 49 } ``` ### Nested Field Grouping GroupBy supports nested field paths: ```typescript // Group by nested field const ordersByRegion = await client.queryBuilder() .collection('orders') .groupBy('customer.address.region') .sumBy('total'); // Returns: { "West": 10000, "East": 8500 } ``` ### executeUnique() Returns the latest record by metadata timestamp (`updatedAt` or `createdAt`). Useful for finding a single record when multiple versions may exist. ```typescript // Find the latest user record by email const user = await client.queryBuilder() .collection('users') .whereField('email').equals('alice@example.com') .executeUnique(); // Returns: User | null (latest version by timestamp) ``` ## Data JOINs Data JOINs execute on the backend in a single request. Use `$data.fieldname` to reference parent record fields. ### joinOne (One-to-One) Returns a single object or null. Use when you expect at most one related record. ```typescript const result = await client.queryBuilder() .collection('tweets') .joinOne('author_info', 'users') .onField('address').equals('$data.author') .selectFields(['display_name', 'avatar_url', 'verified']) .build() .selectAll() .execute(); // Result: { id, content, author, author_info: { display_name, ... } | null } ``` ### joinMany (One-to-Many) Returns an array of related records. Use when you expect multiple related records. ```typescript const result = await client.queryBuilder() .collection('users') .joinMany('tweets', 'tweets') .onField('author').equals('$data.address') .selectFields(['id', 'content', 'created_at']) .build() .selectAll() .execute(); // Result: { address, name, tweets: [{ id, content, ... }, ...] } ``` ### Multiple JOINs ```typescript const result = await client.queryBuilder() .collection('tweets') .whereField('reply_to_id').isNull() // Author profile (one-to-one) .joinOne('author_info', 'users') .onField('address').equals('$data.author') .selectFields(['display_name', 'avatar_url', 'verified']) .build() // All likes (one-to-many) .joinMany('likes', 'likes') .onField('tweet_id').equals('$data.id') .selectFields(['user', 'created_at']) .build() // All replies (one-to-many) .joinMany('replies', 'tweets') .onField('reply_to_id').equals('$data.id') .selectFields(['id', 'author', 'content']) .build() .selectAll() .limit(20) .execute(); ``` ### Nested JOINs Chain JOINs before calling `.build()` to create nested relationships: ```typescript const result = await client.queryBuilder() .collection('tweets') .whereField('id').equals(tweetId) // Get replies with their authors .joinMany('replies', 'tweets') .onField('reply_to_id').equals('$data.id') .selectAll() // Nested: get author for each reply .joinOne('author_info', 'users') .onField('address').equals('$data.author') .selectFields(['display_name', 'avatar_url']) .build() .build() .selectAll() .execute(); ``` ### Self-Referential JOINs ```typescript // Get tweets with quoted tweet info const result = await client.queryBuilder() .collection('tweets') .whereField('quote_tweet_id').isNotNull() .joinOne('quote_tweet', 'tweets') .onField('id').equals('$data.quote_tweet_id') .selectFields(['id', 'content', 'author', 'created_at']) .joinOne('author_info', 'users') .onField('address').equals('$data.author') .selectFields(['display_name', 'avatar_url']) .build() .build() .selectAll() .execute(); ``` ### JoinBuilder Operators Available on `onField()`: ```typescript .equals(value) .in(values) .greaterThan(value) .lessThan(value) .isNull() .isNotNull() ``` ## Materialized Views Create pre-computed views with JOINs and aggregations for instant query performance: ### Creating Views ```typescript // Get database manager const db = client.database('your-app-id'); // Create a materialized view with SDK await db.createView('topSellers', ['products', 'orders'], { select: ['id', 'name', 'price', 'salesCount'], where: { status: 'active' }, orderBy: { salesCount: 'desc' }, limit: 100 }); // Or build complex view with JOINs using query builder const viewQuery = client.queryBuilder() .collection('user_top_tracks') .joinOne('user', 'users') .onField('user_id').equals('$data.user_id') .selectFields(['country']) .build() .orderBy('playcount') .selectAll() .limit(10000) .getQueryRequest(); await db.createView('top_tracks_with_countries', ['user_top_tracks', 'users'], viewQuery); // List all views const views = await db.listViews(); // Get specific view const view = await db.getView('topSellers'); // Refresh view data await db.refreshView('topSellers'); // Delete view await db.deleteView('topSellers'); ``` ### Aggregated Views Create views with GROUP BY for dashboard analytics: ```typescript // View with aggregation const aggregatedView = { name: 'plays_by_country', source_collections: ['top_tracks_with_countries'], query: { find: {}, select: {}, group_by: ['country'], aggregate: { total_plays: { '$sum': 'playcount' }, unique_tracks: { '$countDistinct': 'track_name' }, unique_artists: { '$countDistinct': 'artist_name' } }, sort_by: ['total_plays'], limit: 100 } }; ``` ### Querying Views Query materialized views like regular collections - data is pre-computed and instant: ```typescript // Query the view (no blockchain fetch needed) const results = await client.query({ collection: 'top_tracks_with_countries', limit: 1000 }); // View results already include JOINed data results.records.forEach(record => { console.log(`Track: ${record.track_name}`); console.log(`Country: ${record.user.country}`); console.log(`Plays: ${record.playcount}`); }); ``` ### Aggregation Operators - **`$sum`** - Sum values across grouped records - **`$avg`** - Average values - **`$count`** - Count records in group - **`$countDistinct`** - Count unique values - **`$min`** - Minimum value in group - **`$max`** - Maximum value in group ## Prisma-Style CRUD Operations (Helper Methods) OnChainDB SDK provides familiar Prisma-style helper methods for common document operations. ### Understanding Append-Only Storage OnChainDB is built on Celestia blockchain and uses **append-only storage**. This differs from traditional databases: - **Create**: Appends a new record to the blockchain with a unique ID and timestamp - **Read**: Queries return the latest version of records by default (based on timestamp) - **Update**: Does NOT modify existing data. Instead, appends a NEW record with the same ID but a newer timestamp. The SDK's `findUnique()` and `findMany()` automatically return the latest version - **Delete**: Performs a **soft delete** by appending a new record with `deleted: true`. The original data remains on the blockchain but is excluded from query results by default **Benefits of append-only**: - Complete audit trail - all historical versions are preserved on-chain - Immutable history - past states cannot be altered - Blockchain verifiability - every change is recorded in Celestia **Implications**: - Storage grows with each "update" (new record appended) - Historical versions can be queried if needed - Data is never physically removed from the blockchain ### Create Document ```typescript const user = await client.createDocument( 'users', { email: 'alice@example.com', name: 'Alice', active: true }, { payment_tx_hash: 'tx_hash', user_address: 'celestia1...', broker_address: 'celestia1broker...', amount_utia: 100000 }, { idGenerator: () => 'custom_id_123' // Optional: custom ID generator } ); // Auto-adds: id, createdAt, updatedAt console.log(user.id); // 'custom_id_123' console.log(user.createdAt); // '2024-01-01T00:00:00.000Z' ``` ### Find Unique ```typescript // Find single document by any field const user = await client.findUnique('users', { email: 'alice@example.com' }); if (user) { console.log('Found user:', user.name); } else { console.log('User not found'); } ``` ### Find Many ```typescript // Find multiple documents with options const activeUsers = await client.findMany( 'users', { active: true }, { limit: 10, sort: { field: 'createdAt', order: 'desc' } } ); console.log(`Found ${activeUsers.length} active users`); ``` ### Update Document ```typescript // Note: This appends a NEW record with the same ID and newer timestamp // The original record remains on the blockchain for audit purposes const updated = await client.updateDocument( 'users', { email: 'alice@example.com' }, // where { name: 'Alice Smith', active: false }, // data to update { payment_tx_hash: 'tx_hash', user_address: 'celestia1...', broker_address: 'celestia1broker...', amount_utia: 50000 } ); // Auto-updates: updatedAt (new record has newer timestamp) if (updated) { console.log('Updated user:', updated.name); } else { console.log('User not found'); } ``` ### Upsert Document ```typescript // Create if not exists, update if exists const user = await client.upsertDocument( 'users', { email: 'bob@example.com' }, // where { email: 'bob@example.com', name: 'Bob', active: true }, // create { active: true }, // update paymentProof ); console.log('User upserted:', user.id); ``` ### Delete Document ```typescript // Soft delete: appends a NEW record with deleted=true // The original data remains on the blockchain but is excluded from queries const deleted = await client.deleteDocument( 'users', { email: 'alice@example.com' }, paymentProof ); console.log('Deleted:', deleted); // true if found and soft-deleted ``` ### Count Documents ```typescript // Count matching documents const count = await client.countDocuments('users', { active: true }); console.log(`Active users: ${count}`); ``` ## HTTP 402 Payment-Required Reads OnChainDB supports paid read operations using the HTTP 402 Payment Required protocol. When a query requires payment, the SDK returns a quote instead of data, allowing you to pay and retry. ### How It Works **Option 1: Using QueryBuilder (Recommended)** `QueryBuilder.execute()` throws `PaymentRequiredError` when payment is required. The error contains an `X402Quote` with all payment details: ```typescript import { createClient, PaymentRequiredError, X402Quote } from '@onchaindb/sdk'; try { const result = await client.queryBuilder() .collection('premium_data') .selectAll() .execute(); // Query succeeded - process records console.log('Records:', result.records); } catch (error) { if (error instanceof PaymentRequiredError) { const quote: X402Quote = error.quote; console.log('Payment required!'); console.log('Quote ID:', quote.quoteId); console.log('Total cost:', quote.totalCostTia, 'TIA'); console.log('Pay to:', quote.brokerAddress); console.log('Network:', quote.network); console.log('Quote expires at:', new Date(quote.expiresAt * 1000)); } } ``` **Step 2: Pay and re-query with proof** After paying, include the quote_id and payment proof in your query: ```typescript // User makes payment to brokerAddress from X402Quote const paymentTxHash = await makePayment( quote.brokerAddress, quote.totalCostTia ); // Re-query with payment proof const paidResult = await client.query({ collection: 'premium_data', select: { sensitive_field: true, another_field: true }, // Add payment field (API uses snake_case) quote_id: quote.quoteId, payment_proof: paymentTxHash }); // Now you get the actual data console.log('Records:', paidResult.records); ``` ### Complete Payment Flow Example ```typescript import { createClient, PaymentRequiredError, X402Quote } from '@onchaindb/sdk'; const client = createClient({ endpoint: 'http://localhost:9092', appKey: 'your-app-key', appId: 'your-app-id' }); async function queryWithPayment() { try { // Query using QueryBuilder const result = await client.queryBuilder() .collection('premium_data') .whereField('category').equals('exclusive') .selectFields(['title', 'content', 'price']) .limit(10) .execute(); // No payment required - free query console.log(`Received ${result.records.length} records`); return result.records; } catch (error) { if (error instanceof PaymentRequiredError) { const quote: X402Quote = error.quote; console.log(`Payment required: ${quote.totalCostTia} TIA`); console.log(`Pay to: ${quote.brokerAddress}`); // Make payment using your wallet const paymentTx = await wallet.sendTokens( quote.brokerAddress, `${quote.totalCostTia}tia`, 'OnChainDB Read Payment' ); // Re-query with payment proof const paidResult = await client.query({ collection: 'premium_data', find: { category: { is: 'exclusive' } }, select: { title: true, content: true, price: true }, limit: 10, quote_id: quote.quoteId, payment_proof: paymentTx.transactionHash }); console.log(`Received ${paidResult.records.length} records`); return paidResult.records; } throw error; } } ``` ### Error Handling for Payment-Required The SDK throws `PaymentRequiredError` when a query requires payment. The error contains an `X402Quote` with all payment details: ```typescript import { PaymentRequiredError, PaymentVerificationError, OnChainDBError, X402Quote } from '@onchaindb/sdk'; try { const result = await client.queryBuilder() .collection('paid_content') .selectAll() .execute(); } catch (error) { if (error instanceof PaymentRequiredError) { // Access the X402Quote with payment details const quote: X402Quote = error.quote; console.log('Payment required'); console.log('Quote ID:', quote.quoteId); console.log('Total cost:', quote.totalCostTia, 'TIA'); console.log('Pay to:', quote.brokerAddress); console.log('Network:', quote.network); console.log('Expires at:', new Date(quote.expiresAt * 1000)); console.log('All payment options:', quote.allOptions); } else if (error instanceof PaymentVerificationError) { console.log('Payment verification failed'); console.log('Transaction hash:', error.txHash); console.log('Error:', error.message); } else if (error instanceof OnChainDBError) { console.log('OnChainDB error:', error.code, error.statusCode); } } ``` ### Type-Safe Query Results Use try/catch with `PaymentRequiredError` for type-safe payment handling: ```typescript import { PaymentRequiredError, X402Quote, QueryResponse } from '@onchaindb/sdk'; async function handleQuery>(): Promise { try { const result = await client.queryBuilder() .collection('data') .selectAll() .execute(); return result.records; } catch (error) { if (error instanceof PaymentRequiredError) { const quote: X402Quote = error.quote; // Handle payment flow with type-safe quote console.log('Payment required:', quote.totalCostTia, 'TIA'); return null; } throw error; } } ``` ### Use Cases for Paid Reads - **Premium Content**: Charge for access to exclusive data - **API Monetization**: Monetize data access with per-query pricing - **Cost Recovery**: Recover computational costs for expensive queries - **Tiered Access**: Free basic queries, paid for detailed field access - **Data Marketplace**: Build marketplaces where data providers set prices ## Cost Estimation Estimate operation costs before executing them using `getPricingQuote()`. All prices are returned in TIA (Celestia's native token). ### Get Pricing Quote ```typescript // Estimate cost for a write operation const quote = await client.getPricingQuote({ app_id: 'my_app', operation_type: 'write', size_kb: 50, collection: 'users', monthly_volume_kb: 1000 // Optional: for volume tier discounts }); console.log('Total cost:', quote.total_cost, 'TIA'); console.log('Total cost:', quote.total_cost_utia, 'utia'); console.log('Celestia fee:', quote.base_celestia_cost, 'TIA'); console.log('Broker fee:', quote.broker_fee, 'TIA'); console.log('Indexing costs:', quote.indexing_costs); ``` ### PricingQuoteResponse Structure ```typescript interface PricingQuoteResponse { type: 'write_quote_with_indexing' | 'read_quote'; // Base costs (TIA) base_celestia_cost: number; base_celestia_cost_utia: number; broker_fee: number; broker_fee_utia: number; // Indexing costs per field indexing_costs: Record; // field -> cost in TIA indexing_costs_utia: Record; // field -> cost in utia // Total before price index fees base_total_cost: number; base_total_cost_utia: number; // Final total (includes price index fees if any) total_cost: number; total_cost_utia: number; // Metadata indexed_fields_count: number; request: PricingQuoteRequest; monthly_volume_kb: number; currency: string; // Always "TIA" // Optional: creator premium breakdown (if collection has premium) creator_premium?: { ... }; // Optional: price index breakdown (if collection has Price index) price?: { ... }; } ``` ### Cost Estimation Before Large Uploads ```typescript // Estimate cost for large file upload const fileSizeKb = Math.ceil(fileBuffer.length / 1024); const quote = await client.getPricingQuote({ app_id: 'my_app', operation_type: 'write', size_kb: fileSizeKb, collection: 'documents' }); // Check if user wants to proceed if (quote.total_cost > maxBudgetTia) { console.log(`Upload would cost ${quote.total_cost} TIA, exceeds budget`); return; } // Proceed with upload await client.store({ collection: 'documents', data: fileData }, paymentCallback); ``` ## Transaction Tracking Monitor transaction status with events: ```typescript import { createClient } from '@onchaindb/sdk'; const client = createClient({ endpoint: 'http://localhost:9092', appKey: 'your-app-key' }); // Listen for transaction events client.on('transaction:queued', (ticket) => { console.log(`Task ${ticket.ticket_id} queued: ${ticket.message}`); }); client.on('transaction:pending', (tx) => { console.log(`Transaction ${tx.id} is pending...`); }); client.on('transaction:confirmed', (tx) => { console.log(`Transaction ${tx.id} confirmed at block ${tx.block_height}`); }); client.on('transaction:failed', (tx) => { console.error(`Transaction ${tx.id} failed: ${tx.error}`); }); // Store data - events will be emitted automatically await client.store( { collection: 'messages', data: [{ message: 'This will trigger events' }] }, paymentCallback ); ``` ## Batch Operations Efficient bulk operations with progress tracking: ```typescript import { BulkBuilder } from '@onchaindb/sdk'; // Build batch of records const builder = new BulkBuilder() .collection('tweets') .add({ message: 'Tweet 1', author: 'alice' }) .add({ message: 'Tweet 2', author: 'bob' }) .add({ message: 'Tweet 3', author: 'charlie' }); // Execute batch with progress tracking const batch = client.batch(); const results = await batch.store(builder.build(), { concurrency: 5, waitForConfirmation: true, onProgress: (completed, total) => { console.log(`Progress: ${completed}/${total}`); } }); console.log(`Successfully stored ${results.length} records`); ``` ## Async Task Management OnChainDB supports async operations with ticket-based status tracking: ```typescript // Store operation returns a ticket when waitForConfirmation is false const response = await client.store( { collection: 'large_dataset', data: [/* large amount of data */] }, paymentCallback, false // Don't wait for confirmation ); console.log('Task ticket:', response.ticket_id); // Wait for task completion const task = await client.waitForTaskCompletion(response.ticket_id); console.log('Task completed:', task.status); // Or check task status manually const status = await client.getTaskStatus(response.ticket_id); console.log('Current status:', status.status); // Get all tasks for a user const userTasks = await client.getUserTasks('celestia1...'); console.log(`User has ${userTasks.total_tasks} tasks`); ``` ### Task Status Types ```typescript type TaskStatus = | "Pending" | "PaymentBroadcast" | "PaymentConfirming" | "PaymentConfirmed" | "StoringData" | "Completed" | { Failed: { error: string } }; ``` ## Error Handling Comprehensive error handling with specific error types: ```typescript import { OnChainDBError, TransactionError, ValidationError, PaymentRequiredError, PaymentVerificationError, X402Quote } from '@onchaindb/sdk'; try { await client.store( { collection: 'test', data: [{ test: 'data' }] }, paymentCallback ); } catch (error) { if (error instanceof ValidationError) { console.log('Validation failed:', error.message); console.log('Details:', error.details); } else if (error instanceof TransactionError) { console.log('Transaction failed:', error.transactionId); } else if (error instanceof PaymentRequiredError) { // Access the X402Quote with payment details const quote: X402Quote = error.quote; console.log('Payment required'); console.log('Quote ID:', quote.quoteId); console.log('Total cost:', quote.totalCostTia, 'TIA'); console.log('Pay to:', quote.brokerAddress); } else if (error instanceof PaymentVerificationError) { console.log('Payment verification failed:', error.txHash); } else if (error instanceof OnChainDBError) { console.log('OnChainDB error:', error.code, error.statusCode); } } ``` ## Health Check Verify SDK connection to OnChainDB: ```typescript const health = await client.health(); console.log('Status:', health.status); console.log('Version:', health.version); ``` ## PriceIndex - Value-Based Payment Model PriceIndex is a special index type that changes how payments work, enabling revenue-sharing business models for applications built on OnChainDB. ### Traditional vs PriceIndex Payment **Traditional Model (BTree/Hash index):** - Payment based on data size: `cost = data_size_kb × rate_per_kb` - Fixed pricing, no revenue sharing - Example: 10 KB order costs based on storage size (~0.001 TIA) **PriceIndex Model (Price index):** - Payment based on field value: `cost = field_value` - Automatic revenue splitting between application and platform - Example: $100 order costs 100 TIA, automatically split 70/30 - Revenue split is configured server-side (typically 70% app, 30% platform) ### How It Works ``` User Creates Order totalPrice: 100000000 utia (100 TIA) ↓ Payment Required: 100 TIA ↓ Automatic Revenue Split (Server-Side) ↓ ┌────┴────┐ ↓ ↓ 70 TIA 30 TIA App Platform Wallet (OnChainDB) ``` ### Creating a PriceIndex ```typescript const db = client.database('your-app-id'); // Create PriceIndex on totalPrice field await db.createIndex({ name: 'idx_orders_totalPrice', collection: 'orders', field_name: 'totalPrice', index_type: 'Price' }); // Revenue split is configured server-side // Typically: 70% to app wallet, 30% to platform ``` ### Using PriceIndex in Applications ```typescript // Calculate order total const orderTotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0 ); // Create order with PriceIndex payment await client.createDocument( 'orders', { customerAddress: userWallet, items: orderItems, totalPrice: orderTotal, // This field has PriceIndex! status: 'confirmed' }, { payment_mode: 'pay_on_write', payment_tx_hash: userPaymentTx, user_address: userWallet, broker_address: brokerAddress, amount_utia: orderTotal // User pays order total, not storage cost } ); // Backend automatically: // 1. Detects PriceIndex on totalPrice field // 2. Uses totalPrice value as payment amount // 3. Splits payment automatically (e.g., 70% app, 30% platform) // 4. Credits app wallet balance ``` ### Use Cases for PriceIndex PriceIndex enables you to build applications with built-in revenue sharing: **Perfect for building:** - E-commerce applications (payment = order total) - Ticketing platforms (payment = ticket price) - Subscription services (payment = plan price) - Marketplace applications (payment = transaction amount) - Service platforms with fees **Not suitable for:** - Product catalogs (use BTree for filtering by price range) - Free/public data storage - Internal analytics data - User-generated content without pricing ### Checking App Wallet Balance Your application's revenue share is credited to the app wallet balance and can be withdrawn later. ```typescript // Check app wallet balance const balance = await fetch( `${endpoint}/api/apps/${appId}/wallet/balance` ); console.log('App balance:', balance.amount_utia, 'utia'); console.log('App balance:', balance.amount_utia / 1_000_000, 'TIA'); ``` ## API Reference ### createClient Function ```typescript import { createClient } from '@onchaindb/sdk'; const client = createClient({ endpoint: string; // OnChainDB server endpoint appKey?: string; // App API key for writes (X-App-Key header) userKey?: string; // User API key for Auto-Pay (X-User-Key header) appId?: string; // Application ID for automatic root building timeout?: number; // Request timeout (default: 30000ms) retryCount?: number; // Retry attempts (default: 3) retryDelay?: number; // Retry delay (default: 1000ms) }); ``` ### Client Methods **Core Operations:** - `store(request, paymentCallback?, waitForConfirmation?)` - Store data on blockchain with x402 payment flow - `storeAndConfirm(request)` - Store and always wait for confirmation - `query(request)` - Query data with filters - `getPricingQuote(request)` - Get cost estimate before operations (returns TIA pricing) - `health()` - Health check - `database(appId)` - Get database manager **Blob Storage:** - `uploadBlob(request)` - Upload binary file with metadata - `retrieveBlob(request)` - Download binary file by blob_id - `queryBlobMetadata(collection, where?)` - Query blob metadata **Prisma-Style CRUD:** - `createDocument(collection, data, paymentProof, options?)` - Create document with auto ID/timestamps - `findUnique(collection, where)` - Find single document - `findMany(collection, where, options?)` - Find multiple documents - `updateDocument(collection, where, data, paymentProof)` - Update document - `upsertDocument(collection, where, create, update, paymentProof, options?)` - Create or update - `deleteDocument(collection, where, paymentProof)` - Soft delete document - `countDocuments(collection, where?)` - Count matching documents - `generateId()` - Generate unique ID (can be overridden) **Query Builder:** - `queryBuilder()` - Create fluent query builder - `whereField(field)` - Quick field condition - `selectFields(fields)` - Quick field selection - `selectAll()` - Select all fields **Batch Operations:** - `batch()` - Create batch operations instance **Task Management:** - `getTaskStatus(ticketId)` - Get task status - `waitForTaskCompletion(ticketId, pollInterval?, maxWaitTime?)` - Wait for task - `getUserTasks(address)` - Get user's tasks - `waitForConfirmation(txHash, maxWaitTime?)` - Wait for transaction confirmation **Relations:** - `createRelation(request)` - Create collection relations - `createIndex(request)` - Create index on collection **DatabaseManager (via client.database(appId)):** - `createCollection(name, options)` - Create a new collection - `createIndex(request)` - Create index on collection - `createView(name, sourceCollections, query)` - Create materialized view - `listViews()` - List all views for the app - `getView(name)` - Get specific view - `deleteView(name)` - Delete a view - `refreshView(name)` - Refresh/rebuild view data **Events:** - `on('transaction:queued', callback)` - Task queued with ticket - `on('transaction:pending', callback)` - Transaction pending - `on('transaction:confirmed', callback)` - Transaction confirmed - `on('transaction:failed', callback)` - Transaction failed - `on('error', callback)` - Error occurred ### HTTP 402 Payment Types ```typescript // X402Quote - Standardized payment quote format (camelCase) // Used by QueryBuilder.execute() via PaymentRequiredError interface X402Quote { quoteId: string; totalCostTia: number; amountRaw: string; // Amount in smallest units brokerAddress: string; description: string; expiresAt: number; chainType: 'cosmos' | 'evm' | 'solana'; network: string; asset: string; // Asset identifier (e.g., "utia", USDC address) tokenSymbol: string; tokenDecimals: number; paymentMethod: 'native' | 'x402-facilitator'; facilitator?: string; // Facilitator URL (if applicable) allOptions: X402PaymentRequirement[]; // All payment options from broker } // X402PaymentRequirement - Individual payment option from broker interface X402PaymentRequirement { scheme: 'exact'; network: string; maxAmountRequired: string; payTo: string; asset: string; resource: string; description: string; mimeType: string; maxTimeoutSeconds: number; extra?: X402Extra; } // PaymentRequiredError - thrown when payment is required // Contains X402Quote with all payment details class PaymentRequiredError extends OnChainDBError { quote: X402Quote; constructor(message: string, quote: X402Quote, details?: any); } // Query with payment proof interface ReadQueryWithPayment { // Original query fields root?: string; collection?: string; find?: any; select?: any; limit?: number; offset?: number; sort?: string[]; // Payment field quote_id: string; } ``` ### Blob Types ```typescript interface UploadBlobRequest { collection: string; blob: File | Blob | Buffer; metadata?: Record; payment_tx_hash: string; user_address: string; broker_address: string; amount_utia: number; } interface UploadBlobResponse { ticket_id: string; blob_id: string; status: string; message: string; } interface BlobMetadata { blob_id: string; content_type: string; size_bytes: number; uploaded_at: string; tx_hash: string; celestia_height: number; [key: string]: any; // Custom fields } ``` ## Advanced Features ### Relations Between Collections Create relations between collections for better data organization: ```typescript // Create a relation between parent and child collections const relation = await client.createRelation({ parent_collection: 'users', parent_field: 'id', child_collection: 'posts', child_field: 'author_id' }); console.log('Relation created:', relation); console.log('Indexes created:', relation.indexes_created); ``` ### Field Condition Operators The query builder supports 50+ field condition operators: **Comparison:** - `equals(value)` - Field equals value - `notEquals(value)` - Field does not equal value - `greaterThan(value)` - Field > value - `lessThan(value)` - Field < value - `greaterThanOrEqual(value)` - Field >= value - `lessThanOrEqual(value)` - Field <= value - `between(min, max)` - Field value between min and max - `notBetween(min, max)` - Field value not between min and max **String Operators:** - `contains(value)` - Field contains substring - `startsWith(value)` - Field starts with string - `endsWith(value)` - Field ends with string - `regExpMatches(pattern)` - Field matches regex pattern - `includesCaseInsensitive(value)` - Case-insensitive contains - `startsWithCaseInsensitive(value)` - Case-insensitive starts with - `endsWithCaseInsensitive(value)` - Case-insensitive ends with - `wildcard(pattern)` - Wildcard pattern matching - `glob(pattern)` - Glob pattern matching **Date/Time Operators:** - `dateEquals(date)` - Date equals specific date - `dateBefore(date)` - Date before specific date - `dateAfter(date)` - Date after specific date - `dateBetween(start, end)` - Date within range - `dateToday()` - Date is today - `dateThisWeek()` - Date is within this week - `dateThisMonth()` - Date is within this month - `dateThisYear()` - Date is within this year **Array Operators:** - `in(values)` - Field value in array - `notIn(values)` - Field value not in array - `arrayLength(length)` - Array has specific length - `arrayContains(value)` - Array contains value - `arrayNotContains(value)` - Array doesn't contain value **Boolean:** - `isTrue()` - Field is true - `isFalse()` - Field is false **Existence Operators:** - `isNull()` - Field is null - `isNotNull()` - Field is not null - `exists()` - Field exists - `notExists()` - Field does not exist - `isEmpty()` - Field is empty - `isNotEmpty()` - Field is not empty **Type Checking Operators:** - `isString()` - Value is string - `isNumber()` - Value is number - `isBoolean()` - Value is boolean - `isArray()` - Value is array - `isObject()` - Value is object **IP Address Operators:** - `ipEquals(ip)` - IP equals specific address - `ipInRange(cidr)` - IP in CIDR range - `ipInCountry(country)` - IP in country - `isLocalIp()` - Field is a local IP address - `isExternalIp()` - Field is an external IP address **Geographical Operators:** - `geoWithinRadius(lat, lng, radius)` - Within radius of point - `geoWithinBounds(ne, sw)` - Within geographic bounds - `inCountry(code)` - Field matches country code **Example Usage:** ```typescript // Case-insensitive search (commonly used for search) const products = await client.queryBuilder() .collection('products') .whereField('productDisplayName') .includesCaseInsensitive('shirt') .limit(20) .execute(); // Range query const affordableProducts = await client.queryBuilder() .collection('products') .whereField('price') .between(1000000, 5000000) // 1-5 TIA .execute(); // Boolean check const activeUsers = await client.queryBuilder() .collection('users') .whereField('active') .isTrue() .execute(); // Date query const recentOrders = await client.queryBuilder() .collection('orders') .whereField('createdAt') .dateThisMonth() .execute(); // Geo query const nearbyStores = await client.queryBuilder() .collection('stores') .whereField('location') .geoWithinRadius(37.7749, -122.4194, 10) // 10km from San Francisco .execute(); ``` ### Nested Field Queries Query nested objects using dot notation or the nested builder: ```typescript // Dot notation approach const users = await client.queryBuilder() .collection('users') .whereField('user.profile.bio') .contains('developer') .execute(); // ORM-like nested builder approach const results = await client.queryBuilder() .collection('users') .find(builder => builder.nested('user', nested => nested.andGroup(() => [ LogicalOperator.Condition( nested.field('profile').field('name').equals('John') ), LogicalOperator.Condition( nested.field('settings').field('theme').equals('dark') ) ]) ) ) .execute(); ``` ### Complex Logical Operations ```typescript import { LogicalOperator, FieldConditionBuilder } from '@onchaindb/sdk'; const response = await client.queryBuilder() .collection('users') .find(builder => builder.orGroup(() => [ // Admins or moderators LogicalOperator.And([ LogicalOperator.Condition( new FieldConditionBuilder('role').in(['admin', 'moderator']) ), LogicalOperator.Condition( new FieldConditionBuilder('active').equals(true) ) ]), // Premium users LogicalOperator.And([ LogicalOperator.Condition( new FieldConditionBuilder('subscription').equals('premium') ), LogicalOperator.Condition( new FieldConditionBuilder('verified').equals(true) ) ]) ]) ) .execute(); ``` ### QueryResult Utilities Process query results with built-in utilities: ```typescript import { createQueryResult } from '@onchaindb/sdk'; const result = createQueryResult(response.records); // Basic utilities result.len() // Count of records result.isEmpty() // Check if empty result.first() // First record or undefined result.last() // Last record or undefined result.get(5) // Get record at index // Iteration result.any(r => r.active) // Any match predicate? result.all(r => r.verified) // All match predicate? result.find(r => r.id === '123') // Find first matching result.filter(r => r.status === 'active') result.map(r => r.name) result.forEach((r, i) => console.log(i, r)) result.reduce((acc, r) => acc + r.amount, 0) // Aggregation result.groupBy('department') // { 'sales': [...], 'engineering': [...] } result.pluck('name') // ['Alice', 'Bob', ...] result.pluckStrings('name') // Type-safe string pluck result.pluckNumbers('age') // Type-safe number pluck result.countBy('status') // { 'active': 10, 'inactive': 3 } // Numeric summary const stats = result.summarizeNumeric('salary'); // { count, sum, mean, median, min, max, standardDeviation, variance } // Sorting result.sortBy('name') // Ascending result.sortBy('created_at', false) // Descending result.sortByMultiple([ { field: 'department', ascending: true }, { field: 'salary', ascending: false } ]) // Pagination result.paginate(2, 20) // Page 2, 20 items per page result.chunk(10) // Split into chunks of 10 // Distinct result.distinctBy('email') // Unique by field result.unique() // Unique records // Joining (client-side) const departments = createQueryResult(deptData); result.innerJoin(departments, 'dept_id', 'id') result.leftJoin(departments, 'dept_id', 'id') // Export result.toCsv() // CSV string result.toCsv(';') // Custom delimiter result.toJSON() // JSON string result.toArray() // Plain array // Debugging result.inspect() // Log to console result.inspect(5) // Log first 5 records ``` ### SelectionBuilder ```typescript import { SelectionBuilder } from '@onchaindb/sdk'; // Select specific fields const selection = new SelectionBuilder() .field('id') .field('name') .field('email') .build(); // Select multiple fields at once new SelectionBuilder() .fields(['id', 'name', 'email', 'created_at']) .build(); // Nested field selection new SelectionBuilder() .field('id') .nested('profile', nested => nested.field('bio').field('avatar')) .build(); // Exclude fields new SelectionBuilder() .excludeFields(['password', 'secret_key']) .build(); // Select all fields SelectionBuilder.all(); // Returns {} ``` ## Glossary **OnChainDB** A decentralized database system built on Celestia's Data Availability layer with TypeScript SDK support. **Celestia** A modular blockchain network providing data availability as a service. OnChainDB stores all data on Celestia with cryptographic verification. **Collection** A logical grouping of documents, similar to a table in traditional databases. **Document** A JSON record stored in a collection on the blockchain. **App Key (X-App-Key)** API key for application-level operations, required for write operations. Sent via X-App-Key header. **User Key (X-User-Key)** API key for user-level operations, enables Auto-Pay when user has granted authz. Sent via X-User-Key header. **Auto-Pay** Automatic payment flow when userKey is provided and user has granted authz to the broker. No payment callback needed. **x402 Payment Flow** Payment callback pattern where store() receives a callback function that's invoked when server returns HTTP 402, allowing the client to handle payment. **Index (Required)** Data structure that enables efficient query performance. REQUIRED for production use - every collection MUST have at least one index. Without indexes, queries perform slow full collection scans and collections are not visible in the OnChainDB dashboard. Supports hash (for equality lookups) and btree (for range queries) index types with optional unique constraints. Always create indexes BEFORE storing data. **Namespace** Logical grouping mechanism in Celestia that isolates data for different collections. **Payment Proof** Transaction verification required for write operations, containing tx hash, addresses, and payment amount. **Broker** Intermediary service that handles payment processing and revenue distribution for OnChainDB operations. **PriceIndex** Special index type that changes payment model from data-size-based to field-value-based, enabling revenue-sharing business models for applications built on OnChainDB. Perfect for building e-commerce platforms, ticketing systems, and marketplace applications. **Revenue Split** Automatic distribution of payments when using PriceIndex, configured server-side (typically 70% to app wallet and 30% to platform). Not user-configurable. **HTTP 402 Payment Required** Protocol for paid read operations where queries return a quote before data access, enabling pay-per-query monetization. **X402Quote** Standardized payment quote format (camelCase) used by QueryBuilder.execute() via PaymentRequiredError. Contains quoteId, totalCostTia, brokerAddress, network, chainType, and all payment options. Supports multi-chain payments (Cosmos, EVM, Solana). **PaymentRequiredError** SDK error thrown by QueryBuilder.execute() when a query requires payment. Contains an X402Quote object with all payment details. Catch this error to handle payment flows in applications. **Quote ID** Unique identifier for a payment quote that must be included when re-querying after payment. **Task Ticket** Unique identifier for async operations, used to track operation status and completion. **Blob Storage** Binary file storage capability for images, videos, documents up to 2MB with custom metadata. **Query Builder** Fluent API for constructing complex queries with logical operators and field conditions. **Batch Operations** Bulk data operations that process multiple records efficiently with progress tracking. **TIA** Native token of Celestia network, used for data availability payments (measured in utia - micro TIA). **Data Availability (DA)** Guarantee that published data is available for download and verification on the blockchain. **LogicalOperator** SDK component for combining query conditions using And, Or, and Not operators. **DatabaseManager** SDK component for managing collections, indexes, relations, and materialized views within an application. Access via `client.database(appId)`. **Materialized View** Pre-computed query result that updates automatically when source data changes. Created via `db.createView(name, sourceCollections, query)`. Use for complex aggregations, JOINs, and dashboard analytics. ## Best Practices 1. **Use Dual-Key Auth** - Use appKey for writes, userKey for Auto-Pay reads 2. **Handle x402 Callbacks** - Implement payment callbacks for store operations 3. **Use Collections** - Organize data into collections for better querying and indexing 4. **Handle Errors** - Always implement proper error handling with specific error types 5. **Monitor Transactions** - Use event listeners to track transaction status in real-time 6. **Batch Operations** - Use batch operations for large datasets to improve efficiency 7. **Create Indexes (REQUIRED)** - You MUST create at least one index per collection. Without indexes, queries perform full collection scans and the dashboard cannot display collections. Create indexes BEFORE storing data 8. **Set Timeouts** - Configure appropriate timeouts based on your use case 9. **Use TypeScript** - Leverage full type safety for better development experience 10. **Cost Estimation** - Get pricing quotes before operations to estimate costs 11. **Async Operations** - Use task tickets for long-running operations and monitor progress 12. **Handle HTTP 402** - Always check query results for payment quotes and handle the payment flow 13. **Catch PaymentRequiredError** - Use try/catch with PaymentRequiredError to handle payment flows with X402Quote 14. **PriceIndex for Commerce** - Use PriceIndex when building commerce platforms, ticketing systems, or apps with revenue sharing 15. **Case-Insensitive Search** - Use includesCaseInsensitive() for user-facing search features 16. **Prisma Methods** - Use createDocument/findUnique/updateDocument for single-record operations 17. **Blob Storage** - Use uploadBlob/retrieveBlob for binary files, queryBlobMetadata for listing ## SDK Information **Package Name:** `@onchaindb/sdk` **Language:** TypeScript **Runtime:** Node.js 18+ / Browser **License:** MIT ### Key Dependencies - axios - HTTP client - eventemitter3 - Event handling ### Installation ```bash npm install @onchaindb/sdk ``` --- For more information, visit [OnChainDB Documentation](https://onchaindb.io)