# Building with LLMs Source: https://docs.onebalance.io/ai/building-with-llms Use LLMs in your OneBalance integration workflow. You can use large language models (LLMs) to assist in the building of OneBalance integrations. We provide a set of tools and best practices if you use LLMs during development. ## Plain text docs You can access all of our documentation as plain text markdown files by adding `.md` to the end of any URL. For example, you can find the plain text version of this page itself at [/ai/building-with-llms.md](/ai/building-with-llms.md). This helps AI tools and agents consume our content and allows you to copy and paste the entire contents of a doc into an LLM. This format is preferable to scraping or copying from our HTML and JavaScript-rendered pages because: * Plain text contains fewer formatting tokens. * Content that isn't rendered in the default view (for example, it's hidden in a tab) of a given page is rendered in the plain text version. * LLMs can parse and understand markdown hierarchy. We also host the [/llms.txt](https://docs.onebalance.io/llms.txt) and [/llms-full.txt](https://docs.onebalance.io/llms-full.txt) files which instructs AI tools and agents how to retrieve the plain text versions of our pages. The `/llms.txt` file is an [emerging standard](https://llmstxt.org) for making websites and content more accessible to LLMs. # MCP Server Setup Source: https://docs.onebalance.io/ai/mcp-server-setup Setting up a Model Context Protocol server for advanced agent interactions ## MCP Server Configuration **Coming Soon**: Detailed MCP server setup instructions will be published here. We're preparing documentation for: * **Server installation** and configuration steps * **Authentication setup** for secure agent interactions * **API integration** patterns for MCP clients * **Example configurations** for popular AI frameworks ## Current AI Integration Learn about current AI integration capabilities with OneBalance For early access to MCP server documentation, contact us at [support@onebalance.io](mailto:support@onebalance.io). # Predict smart account address Source: https://docs.onebalance.io/api-reference/account/predict-address post /account/predict-address Predicts the address for a new OneBalance smart account based on the provided EVM-compatible signer addresses (session and admin). This allows you to determine the smart account's on-chain address before its actual deployment. This predicted address serves as a primary identifier for the user within the OneBalance ecosystem. # List supported aggregated assets Source: https://docs.onebalance.io/api-reference/assets/list get /assets/list Retrieves a list of all aggregated assets currently supported and enabled on the OneBalance platform. Each aggregated asset includes details such as its unique ID, symbol, name, decimals, and the underlying individual assets (chain-specific tokens) that constitute it, identified by their CAIP-19 asset types. # Authentication Source: https://docs.onebalance.io/api-reference/authentication How to authenticate requests to the OneBalance API The OneBalance API uses API keys to authenticate requests. All API requests require authentication using an API key passed in the `x-api-key` header: ```bash Terminal curl -X 'GET' \ 'https://be.onebalance.io/api/assets/list' \ -H 'x-api-key: f9703eaqsbma20tmtphg2jirm0hk8z8v2hkodrfrvhfm6ziesi7p38u991bnih5f' ``` A public API key is available for testing purposes with limited usage: **f9703eaqsbma20tmtphg2jirm0hk8z8v2hkodrfrvhfm6ziesi7p38u991bnih5f** All API requests must be made over [HTTPS](https://en.wikipedia.org/wiki/HTTPS). Calls made over plain HTTP will fail. API requests without authentication will also fail. # Get aggregated balance Source: https://docs.onebalance.io/api-reference/balances/aggregated-balance get /v2/balances/aggregated-balance Fetches the aggregated balance of all supported assets for a given smart account address, with an option to filter by specific `assetId`s. # List supported chains Source: https://docs.onebalance.io/api-reference/chains/list get /chains/supported-list Retrieves a list of all blockchain networks currently integrated with and supported by the OneBalance platform. # Cross Origin Resource Sharing Source: https://docs.onebalance.io/api-reference/cors Information about CORS support in the OneBalance API OneBalance API implements Cross Origin Resource Sharing (CORS) to allow requests from browsers on different domains. ## Overview CORS support enables web applications to make AJAX requests to the OneBalance API from domains different from where the API is hosted. This is essential for implementing features like dashboards or control panels that utilize the API. ## How CORS Works When a browser makes a cross-origin request: 1. For non-simple requests (like PUT, DELETE, or with custom headers), the browser first sends a "preflight" request using the OPTIONS method 2. This preflight request includes the `Origin` header identifying the requesting domain 3. The server responds with headers describing the allowed constraints 4. If the actual request falls within these constraints, the browser proceeds with the actual request ## CORS Headers The OneBalance API responds with the following CORS headers: | Header | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | `Access-Control-Allow-Origin` | The domain that sent the request (from the `Origin` header) | | `Access-Control-Allow-Methods` | The HTTP methods allowed for cross-origin requests (typically includes all available methods) | | `Access-Control-Expose-Headers` | Headers that will be accessible to requests from the origin domain | | `Access-Control-Max-Age` | How long (in seconds) the preflight response can be cached before another preflight is needed | | `Access-Control-Allow-Credentials` | Set to `true` to allow sending authentication credentials (like Access tokens) with the request | ## Client Implementation Most browsers handle CORS details automatically. Your JavaScript code can make requests to the OneBalance API endpoints as it would to any API, and the browser will manage the preflight requests and handle the CORS headers. ```javascript request.js // Example of a cross-origin fetch request fetch('https://be.onebalance.io/api/assets/list', { method: 'GET', headers: { 'x-api-key': 'your-api-key', 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ``` ## Server Implementation in Next.js In a Next.js application using the App Router, you can set [CORS headers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers#cors) for a specific Route Handler using the standard Web API methods: ```typescript route.ts // app/api/[...path]/route.ts import { NextRequest, NextResponse } from 'next/server'; import { API_BASE_URL, API_KEY } from '@/lib/constants'; export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const { path } = await params; const pathString = path.join('/'); const searchParams = request.nextUrl.searchParams; try { // Build the API URL with any query parameters const apiUrl = new URL(`/api/${pathString}`, API_BASE_URL); searchParams.forEach((value, key) => { apiUrl.searchParams.append(key, value); }); const response = await fetch(apiUrl.toString(), { headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, }); const data = await response.json(); return NextResponse.json(data); } catch (error) { return NextResponse.json({ message: 'Failed to fetch data', error }, { status: 400 }); } } export async function POST( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const { path } = await params; const pathString = path.join('/'); try { const body = await request.json(); const response = await fetch(`${API_BASE_URL}/api/${pathString}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, body: JSON.stringify(body), }); const data = await response.json(); return NextResponse.json(data); } catch (error) { return NextResponse.json({ message: 'Failed to fetch data', error }, { status: 400 }); } } ``` # Errors Source: https://docs.onebalance.io/api-reference/errors Complete list of error codes with explanations and troubleshooting tips OneBalance uses conventional HTTP response codes to indicate the success or failure of an API request. In general: Codes in the `2xx` range indicate success. Codes in the `4xx` range indicate an error that failed given the information provided (e.g., a required parameter was omitted, a charge failed, etc.). Codes in the `5xx` range indicate an error with OneBalance's servers. ## HTTP Status Codes | Status Code | Type | Description | | ------------------ | ----------------- | ------------------------------------------------------------------------------------------------ | | 200 | OK | Everything worked as expected. | | 400 | Bad Request | The request was unacceptable, often due to missing a required parameter. | | 401 | Unauthorized | No valid API key provided. | | 402 | Request Failed | The parameters were valid but the request failed. | | 403 | Forbidden | The API key doesn't have permissions to perform the request. | | 404 | Not Found | The requested resource doesn't exist. | | 429 | Too Many Requests | Too many requests hit the API too quickly. We recommend an exponential backoff of your requests. | | 500, 502, 503, 504 | Server Errors | Something went wrong on OneBalance's end. | ## Error Response Format Both `400` and `500` level errors return a JSON object in the response body, containing specific attributes detailing the error. | Name | Type | Description | | ---------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | error | string | A short identifier corresponding to the HTTP status code returned. For example, the ID for a response returning a 404 status code would be "not\_found." | | message | string | A message providing additional information about the error, including details to help resolve it when possible. | | statusCode | number | HTTP status code. | | timestamp | string | Timestamp of the error. | | path | string | Path of the request that caused the error. | ## Example Error Response HTTP/1.1 401 Unauthorized ```json Unauthorized { "error": "UnauthorizedException", "message": "Invalid or missing API key", "statusCode": 401, "timestamp": "2024-12-18T14:38:24.793Z", "path": "/api/xxx/xxx" } ``` ## Error Handling Best Practices * Always check for error responses in your API integrations * Use the `statusCode` to determine the type of error * Look at the `message` field for specific details on how to resolve the issue * Include the error `id` when contacting support about any API issues # Introduction Source: https://docs.onebalance.io/api-reference/introduction Overview of the OneBalance API, versioning, and general conventions The OneBalance API provides a programmatic interface for managing OneBalance resources using standard HTTP requests. The API documentation includes a design and technology overview, followed by detailed endpoint information. ## Features OneBalance API provides a complete chain abstraction toolkit: * **Smart Account Management**: Predict addresses and deploy accounts * **Cross-Chain Balances**: View aggregated balances across multiple chains * **Multichain Transactions**: Execute transfers and swaps using aggregated balances * **Arbitrary Contract Executions**: Execute any smart contract function across multiple chains using aggregated assets * **Transaction Status**: Track multichain transaction history and status * **Token Aggregation**: Access unified token lists and pricing information **Enterprise API Access**: Need higher rate limits, dedicated infrastructure, or custom SLAs? [Contact our sales team](mailto:sales@onebalance.io) for enterprise API solutions. ## Base URL OneBalance API is built on REST principles and is served over HTTPS. To ensure data privacy, unencrypted HTTP is not supported. The Base URL for all API endpoints is: ```bash Terminal https://be.onebalance.io/api ``` ## Quickstart Try our reference application at [app.onebalance.io](https://app.onebalance.io) to see the API in action or jump straight into our [API Reference](/api-reference/account/predict-address) to start building. # Pagination Source: https://docs.onebalance.io/api-reference/pagination How to work with paginated API responses OneBalance API uses cursor-based pagination for endpoints that return large collections of items. This approach provides efficient navigation through result sets. ## How Pagination Works The `/status/get-tx-history` endpoint supports pagination through the following parameters: | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------- | | `limit` | string | Yes | Maximum number of transactions to return in a single request (e.g., `10`) | | `continuation` | string | No | A cursor value received from a previous response used to fetch the next page | ## Response Format Paginated responses include: ```json Paginated Response { "transactions": [ // Array of transaction objects ], "continuation": "txn_00123456789abcdef" // Token to fetch the next page } ``` The `continuation` property will be present when more results are available beyond the current page. If it's absent or null, you've reached the end of the results. ## Pagination Example ### Initial Request ```bash Initial Request GET /api/status/get-tx-history?user=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045&limit=10 ``` ### Response ```json Paginated Response { "transactions": [ // 10 transaction objects ], "continuation": "txn_00123456789abcdef" } ``` ### Subsequent Request To fetch the next page, include the `continuation` token from the previous response: ```bash Subsequent Request GET /api/status/get-tx-history?user=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045&limit=10&continuation=txn_00123456789abcdef ``` ## Pagination Best Practices 1. **Always specify a limit** - This controls how many items you receive per page. 2. **Store the continuation token** - Save the token from each response to allow users to navigate to the next page. 3. **Check for continuation** - If a response doesn't include a continuation token, it means you've reached the end of the results. 4. **Handle pagination in your UI** - Implement "Next" and "Previous" buttons that appear or disappear based on the availability of continuation tokens. ## Limits and Constraints * The `limit` parameter must be a positive number. * For optimal performance, we recommend using reasonable limit values (10-50). * The maximum number of results you can request per page may be capped by the server. * Continuation tokens are temporary and may expire after a certain period of time. # Execute quote Source: https://docs.onebalance.io/api-reference/quotes/execute-quote post /quotes/execute-quote Execute a previously generated quote to transfer funds. # Get call quote Source: https://docs.onebalance.io/api-reference/quotes/get-call-quote post /quotes/call-quote Gets a quote for arbitrary contract call operations on a specific chain. This endpoint handles the execution of pre-signed user operations with typed data signatures, supporting interactions with smart contracts. # Get quote Source: https://docs.onebalance.io/api-reference/quotes/get-quote post /v1/quote Request a quote for transferring or swapping assets. # Prepare call quote Source: https://docs.onebalance.io/api-reference/quotes/prepare-call-quote post /quotes/prepare-call-quote Prepares the necessary data structure for executing one or more contract calls on a target chain. This endpoint helps construct the appropriate user operation, handles token allowance requirements, and generates a signed quote ready for execution. # Rate Limits Source: https://docs.onebalance.io/api-reference/rate-limits Information about API rate limits and how to handle them The OneBalance API implements rate limiting to ensure fair usage and availability of the service for all users. Rate limits are applied on a per-API key basis. ## Current Rate Limits | User Type | Request Rate Limit | Concurrent Connections | | ------------------- | ---------------------- | ---------------------- | | Public API Key | 60 requests per minute | 1 per IP address | | Authenticated Users | Custom rate limits | Custom limits | Authenticated users receive higher rate limits based on their specific needs. Please contact our team if you require increased limits for your production application. Once you exceed your limit, your requests will be temporarily rejected until the rate limit window resets. ## Rate Limit Headers The rate limiting information is included in the response headers of each request: | Header | Description | | ----------------------- | ---------------------------------------------------------------------------- | | `x-ratelimit-limit` | The maximum number of requests you're permitted to make per minute | | `x-ratelimit-remaining` | The number of requests remaining in the current rate limit window | | `x-ratelimit-reset` | The time at which the current rate limit window resets in Unix epoch seconds | As long as the `x-ratelimit-remaining` count is above zero, you'll be able to make additional requests. ## How Rate Limiting Works Each request contributes toward your rate limit count for one complete minute. This means that the entire rate limit doesn't reset at once. Rather, each request expires individually one minute after it was made. The value of the `x-ratelimit-reset` header indicates when the oldest request will expire and no longer count toward your limit. ## Handling Rate Limits If you exceed the rate limit, the API will return a `429 Too Many Requests` status code. We recommend implementing the following strategies to handle rate limits effectively: 1. **Monitor the rate limit headers** in your API responses to track your usage 2. **Implement exponential backoff** when receiving 429 responses 3. **Pace your requests** to avoid hitting the limits, especially for batch operations 4. **Cache responses** when possible to reduce the number of API calls ## Sample Rate Limit Headers ```bash Terminal x-ratelimit-limit: 60 x-ratelimit-remaining: 58 x-ratelimit-reset: 60 ``` ## Sample Rate Limit Exceeded Response ```json Rate Limit Exceeded { "error": "TooManyRequests", "message": "Rate limit exceeded. Please retry after 45 seconds.", "statusCode": 429, "timestamp": "2024-12-18T14:38:24.793Z", "path": "/api/assets/list" } ``` ## Best Practices * Space out requests that would otherwise be issued in bursts * Implement retry logic with exponential backoff when receiving 429 responses * For high-volume operations, consider batching requests where appropriate * Use the public API key for testing and development only, as it has lower rate limits Some endpoints may have special rate limit requirements that are independent of the general limits defined above. # Get quote status Source: https://docs.onebalance.io/api-reference/status/get-quote-status get /status/get-execution-status Retrieves the execution status of a quote by its quote ID as well as transaction hashes for user operations # Get transaction history Source: https://docs.onebalance.io/api-reference/status/get-transaction-history get /status/get-tx-history Retrieves the transaction history for an address, including the transaction type and token details. # Changelog Source: https://docs.onebalance.io/changelog Latest updates and changes to OneBalance export const ChangelogItem = ({label, children, link, imageUrl}) => { return
{label}
{imageUrl && OneBalance} {children}
; }; ## OneBalance Documentation is Live Our new developer documentation is now available! Get started with step-by-step guides, interactive API reference, and AI integration examples. # Account Models Source: https://docs.onebalance.io/concepts/accounts Understanding OneBalance account models and their capabilities. ## Available Configurations for EVM (details below) | Configuration | Validator | Version | Deployment | Resource Lock Options | | :------------ | :----------------- | :--------- | :-------------------------------------------------- | :-------------------- | | Basic | ECDSAValidator | Kernel 3.1 | [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) | Can be enabled | | EIP-7702 | ECDSAValidator | Kernel 3.3 | [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) | Can be enabled | | Role-Based | RoleBasedValidator | Kernel 3.1 | [EIP-4337](https://eips.ethereum.org/EIPS/eip-4337) | Always enabled | ![Screenshot2025 05 29at12 32 52 Pn](https://mintlify.s3.us-west-1.amazonaws.com/onebalance-d7e5d4d0/images/Screenshot2025-05-29at12.32.52.png) OneBalance supports a modular architecture designed to accommodate various account types. We are happy to support other account versions on demand or advise which one works best for your application. ## Choosing the Right Configuration When selecting an account model for your integration, consider your specific requirements for compatibility, performance, and access control. | Configuration | Pros | Cons | | :------------ | :----------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | | Basic | - Most adopted 4337-type account
- Simplest in use and UI/UX
- Easy to integrate, if already in use within the application | - Contract upgrades, if required, are cumbersome | | Role-Based | - Secure in emergency cases related to WaaS/TEE | - Consumes slightly more gas | | EIP-7702 | - More gas efficient
- Easier future contract upgrades
- Can act as both EOA and Smart Account | - Recently introduced by the Ethereum account standard, not yet widely battle-proven
- Not yet supported on all EVMs | ## Getting Started with Account Models Follow the [Getting Started guide](/getting-started/introduction), which uses Role-Based configuration. ## Signer Compatibility The OneBalance Toolkit is designed to work with various signer providers: | Signer | Account | Standard Path (no RL) | Enabling Resource Lock | | :------------ | :------------- | :---------------------- | :---------------------- | | `turnkey` | `Basic` | 🟒 Available | 🟒 Available | | | `EIP-7702` | 🟒 Available | 🟒 Available | | | `Role-Based` | RL can't be disabled | 🟒 Available | | | `solana` | Early Q3 | Early Q3 | | `privy` | `Basic` | 🟒 Available | 🟒 Available | | | `EIP-7702` | 🟒 Available | In design | | | `Role-Based` | RL can't be disabled | 🟒 Available | | | `solana` | Early Q3 | In design | | Other signers | Other accounts | On demand, case by case | On demand, case by case | ## Account Components Explanation The OneBalance account system consists of several key components that can be combined in different ways: **Authentication, Access Control, Permissions** Validator is a module of smart account **Optimized for various deployment methods** Smart Contract Implementation **How accounts are initialized**\ Directly affects user deposit (account) address **Allows fast path cross-chain execution** Requires gatekeeping at the account/signing level ### Supported Validator Types Validators are responsible for authenticating transactions and managing account access: Uses standard Elliptic Curve Digital Signature Algorithm for authentication, compatible with most existing wallet infrastructures. Allows to have a `user_admin` role (think user cold wallet) next to the signer role to rotate keys and execute trustless rage quit in emergency case. ### Supported Smart Account Versions **Compatible with:** Both ECDSAValidator and RoleBasedValidator The versatile version that supports multiple validator types and is widely compatible across different configurations. **Optimized for:** EIP-7702 deployments with ECDSAValidator A specialized version designed to take advantage of the latest EIP-7702 standard for improved gas efficiency. These account versions are based on an open-source smart contract account [implementation](https://github.com/zerodevapp/kernel). ### Deployment Methods The established standard for smart contract accounts across EVM chains. User address in the UI is the Smart Account address, the signer existance is hidden. A newer, optimized approach for account abstraction with potential gas savings. User addres in the UI is the EOA address with imlementation being referenced as a universal smart contract. ### Resource Lock options for Account Execution happens only traditional way requiring sequential cross-chain transactions. Transactions can be sent both with OneBalance API and standrard JSON-RPC. Enhanced execution using [Resource Locks](/concepts/resource-locks) for near-instant cross-chain transactions. Once enabled, transactions can be sent only through OneBalance API. To learn more about how Resource Locks enable Fast Path execution and improve transaction speed\ and reliability, see our detailed [Resource Locks](/concepts/resource-locks) documentation. ## Resources * [EIP-4337: Account Abstraction Using Alt Mempool](https://eips.ethereum.org/EIPS/eip-4337) * [EIP-7702: Delegated Smart Contract Authentication](https://eips.ethereum.org/EIPS/eip-7702) * [ZeroDev Kernel Repository](https://github.com/zerodevapp/kernel) - The underlying smart contract account implementation used by OneBalance * [ERC-4337 Official Website](https://www.erc4337.io/) * [EIP-7702 Official Website](https://eip7702.io/) # Aggregated Assets Source: https://docs.onebalance.io/concepts/aggregated-assets Understanding OneBalance's unified token representation across multiple chains Aggregated assets are OneBalance's solution for representing the same token across multiple blockchain networks as a single unified asset. Instead of managing USDC on Ethereum, USDC on Polygon, and USDC on Arbitrum separately, you work with one aggregated `ds:usdc` asset. ## How Aggregated Assets Work When you interact with an aggregated asset like `ds:usdc`, OneBalance automatically: 1. **Aggregates balances** across all supported chains where you hold that token 2. **Optimizes routing** to spend from the most efficient chain for your transaction 3. **Handles bridging** automatically when cross-chain operations are needed 4. **Provides unified pricing** and fiat value calculations ## Supported Aggregated Assets Each aggregated asset includes: * **Unique ID**: Like `ds:usdc` or `ds:eth` * **Symbol and name**: Human-readable identifiers * **Decimals**: Precision for the aggregated asset * **Individual assets**: The specific chain tokens that make up the aggregated asset Retrieve all supported aggregated assets and their underlying chain-specific tokens ### Example: USDT Aggregated Asset ```json { "aggregatedAssetId": "ds:usdt", "symbol": "USDT", "name": "Tether USD", "decimals": 6, "aggregatedEntities": [ { "assetType": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7", "decimals": 6, "name": "Tether USD", "symbol": "USDT" }, { "assetType": "eip155:42161/erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", "decimals": 6, "name": "Tether USD", "symbol": "USDT" } ] } ``` ## Asset Identification Formats OneBalance supports two types of asset identifiers: ### Aggregated Assets * **Format**: `ds:tokenSymbol` (e.g., `ds:usdc`, `ds:eth`) * **Purpose**: Unified representation across all supported chains * **Usage**: Optimal for most applications wanting chain abstraction ### Specific Assets * **Format**: CAIP-19 standard (e.g., `eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) * **Purpose**: Target specific chain implementations * **Usage**: When you need precise control over which chain to use View all blockchain networks integrated with OneBalance platform ## Working with Aggregated Assets Get unified token balances across all supported chains for any address Request quotes for swaps and transfers using aggregated assets ### API Examples **Listing Available Assets:** ```bash curl -X 'GET' \ 'https://be.onebalance.io/api/assets/list' \ -H 'x-api-key: ONEBALANCE_API_KEY' ``` **Checking Aggregated Balances:** ```bash curl -X 'GET' \ 'https://be.onebalance.io/api/v2/balances/aggregated-balance?address=0x123...' \ -H 'x-api-key: ONEBALANCE_API_KEY' ``` **Creating Quotes with Aggregated Assets:** ```json { "from": { "account": { /* account details */ }, "asset": { "assetId": "ds:eth" }, "amount": "1000000000000000000" }, "to": { "asset": { "assetId": "ds:usdc" } } } ``` ## Advanced Operations Prepare contract interactions using aggregated assets as funding Execute prepared quotes for aggregated asset operations ## Benefits of Aggregated Assets Work with one asset ID instead of managing multiple chain-specific tokens OneBalance routes transactions through the most efficient chains See total holdings across all chains in one view Cross-chain operations happen automatically when needed ## Monitoring and Status Monitor execution progress of aggregated asset transactions View complete history of aggregated asset operations ## Best Practices 1. **Use aggregated assets by default** for the best user experience 2. **Fall back to specific assets** only when you need chain-specific behavior 3. **Check supported assets** regularly as new tokens and chains are added 4. **Monitor aggregated balances** to understand your users' cross-chain holdings ## Related Resources Set up smart accounts for aggregated asset operations API key setup and authentication for accessing aggregated assets Common issues and troubleshooting for aggregated asset operations API usage guidelines and rate limiting information # Fees & Monetization Source: https://docs.onebalance.io/concepts/fees How user fees are calculated and paid to applications using OneBalance ## Overview OneBalance enables applications to monetize their services through transparent and flexible fee structures. This page explains how fees work, who pays them, and how to configure them for your application. User pays **one consolidated fee** to the app, Toolkit helps app to handle the underlying gas & paymaster costs. ## Fee Types and Responsibilities | **Category** | **Fee Type** | **Who Pays** | **Description** | | :----------- | :---------------------- | :----------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | | Gas Fees | Same-Chain Execution | Application | The app pays the bill to the paymaster service in USD for same-chain execution routes | | Gas Fees | Cross-Chain Source | Application | The app pays the source chain gas cost to the paymaster service in USD | | Gas Fees | Cross-Chain Destination | User | This fee is included in the quote the solver provides for execution | | Monetization | Swaps & Transfers | User | App configures the fees and collects them to a dedicated address. The fee is paid in the input token and delivered with every user transaction | | Monetization | Function Calls | User | App calculates the fee it is willing to take and encodes this into the desired calldata to be executed as a transfer | ## Monetization Fees for Swaps and Transfers Our transactional API lets you monetize every on-chain action initiated by your end users. Each time an end user sends a transaction through your application **one transparent service fee** is calculated, composed of: 1. **Percentage fee** – a fixed percentage of the asset amount 2. **Network fee** – a flat USD-denominated amount that offsets per-chain operational costs ### Fee Parameters | **Field** | **Type** | **Description** | | :------------------ | :------- | :------------------------------------------------------------------------------------------------------------ | | `percentageBps` | `uint16` | Fee in basis points (1 bp = 0.01%) | | `usdBaseMultiplier` | `uint8` | Multiplier applied to the operational cost table | | `beneficiary` | `string` | The EVM address that collects the fees. Once non-EVM chains are supported, additional addresses will be added | ### Chain-Specific Fee Configuration The following table shows a config example of base costs for different chains. These values are multiplied by the `usdBaseMultiplier` to calculate the final network fee. | **Chain** | **CAIP-2 ID** | **Base Cost (USD)** | | :-------------- | :------------- | :------------------ | | Ethereum | `eip155:1` | \$3.00 | | Avalanche | `eip155:43114` | \$0.20 | | BNB Smart Chain | `eip155:56` | \$0.10 | | Linea | `eip155:59144` | \$0.5 | | Base | `eip155:8453` | \$0.02 | | Arbitrum | `eip155:42161` | \$0.02 | | Polygon PoS | `eip155:137` | \$0.02 | | Blast | `eip155:81457` | \$0.02 | | Optimism | `eip155:10` | \$0.02 | ### How Fees Are Calculated | **Component** | **Formula** | | :-------------------- | :-------------------------------------------------------------- | | **Percent component** | `percentFee = (amount Γ— percentageBps) / 10,000` | | **USD component** | `usdFee = Ξ£(chainCostUsd[chain] Γ— usdBaseMultiplier)` | | **Conversion** | USD fee is converted to the input asset using a live price feed | | **Total fee** | `totalFee = percentFee + usdFeeAssetEquivalent` | ### Example Calculation **1 ETH to USDC Swap on Arbitrum** Given: * `percentageBps` = 30 (0.3%) * `usdBaseMultiplier` = 2 * `1 ETH = 2,500 USDC` ```typescript percentFee = 1 ETH Γ— 0.3% = 0.003 ETH usdFee = 0.03 USD Γ— 2 = 0.06 USD β†’ 0.000024 ETH ------------------------------------------------ totalFee = 0.003024 ETH ``` ## Fee Settlement * Fees are transferred to the `beneficiary` address in the same token used for the transaction * For bridge + swap operations, network fees are summed across all chains involved ## Managing Fee Configuration 1. Fee configuration updates are necessary to reflect significant shifts in gas prices or native token values. 2. To update the configuration app needs to contact OneBalance team. ## Coming Soon 1. **Dynamic Fee Adjustment** - Fees will automatically adjust to market shifts based on average gas prices and native token prices in real-time 2. **Self-Service Updates** - Configuration will be updatable via APIs or through the admin dashboard UI **Custom Fee Structures**: Need specialized pricing models or volume discounts? [Contact our sales team](mailto:sales@onebalance.io) to discuss custom fee arrangements for your business. # Resource Locks Source: https://docs.onebalance.io/concepts/resource-locks How Resource Locks enable fast cross-chain transactions by eliminating finality wait times and preventing double-spending ## Overview Resource Locks are OneBalance's innovative solution to the inherent latency in cross-chain transactions. It enables cross-chain transactions to execute at the speed of the destination chain, regardless of source chain finality times. ## How Resource Locks Work Resource Locks are managed by OneBalance's RL Service, which performs four critical functions: Validates user on-chain and locked balances Adds signatures to operations, ensures control over account state transition and prevents double-spending Issues security attestations to solvers for immediate delivery, given delayed settlement Ensures not yet settled balance follows risk policies to maintain system integrity This system enables asynchronous execution where intent fulfillment is separated from settlement, dramatically improving user experience. ## Evolution of Cross-Chain Bridging

Critical Path:

Source chain finality + Destination chain inclusion

Typical Duration:

3 minutes or more

Critical Path:

Source chain inclusion + Routing + Destination chain inclusion

Typical Duration:

15-45 seconds

Critical Path:

Lock verification + Routing + Destination chain inclusion

Typical Duration:

5-10 seconds

### Classical Bridge: Source Chain Finality + Destination Chain Inclusion Traditional bridges require waiting for source chain finality before processing on the destination chain, resulting in significant delays. ![Classical Bridge Flow](https://storage.googleapis.com/onebalance-public-assets/docs/concepts/ClassicalBridgeFlow.png) ### Intent Bridge: Solver Assumes Finality Risk Intent-based bridges improve speed by having solvers take on the finality risk, but still require source chain inclusion. ![Intent Bridge Flow](https://storage.googleapis.com/onebalance-public-assets/docs/concepts/IntentBridgeFlow.png) ### Fast Intent Bridge: Lock Provider Eliminates Double-Spend and Finality Risk OneBalance's Fast Intent Bridge achieves near-instant cross-chain execution by removing both double-spend risk and finality waiting times through Resource Locks. ## Types of Resource Locks Resource Locks fall into two main categories: 1. **Account-based locks** - Locks are implemented at the account level, allowing seamless compatibility with existing user flows and wallet interactions. 2. **Escrow-based locks** - Instead of locking accounts, this approach implements an escrow smart contract where users deposit tokens for cross-chain operations. Currently, OneBalance primarily uses account-based locks for optimal user experience and compatibility. Once Resource Lock is enabled, transactions can be sent only via OneBalance Toolkit, as otherwise it is impossible to prevent the double-spending in asynchronious execution environment. For detailed integration instructions, see our [Getting Started](/getting-started/introduction) guide. # Transaction Lifecycle Source: https://docs.onebalance.io/concepts/transaction-lifecycle Understanding how transactions flow through the OneBalance API The numbers shown below are indicative and strongly depend on the use case conditions. Not sure about the term? Visit [Glossary](/resources/glossary) to see all the definitions. ![Screenshot2025 05 28at21 11 09 Pn](https://mintlify.s3.us-west-1.amazonaws.com/onebalance-d7e5d4d0/images/Screenshot2025-05-28at21.11.09.png) ## Transaction Types and Properties Swap and transfer any token on any supported chain. Execute any smart contract calls on any chain, spending the aggregated balance while preserving `msg.sender` **Fast Path** for cross-chain execution removes source chain txs latency. **Standard Path** is for cross-chain is a basic sequential execution Read more on Fast path and Resource locks [here](/concepts/resource-locks). ## Transaction Execution Sequence (Swap example) ```mermaid sequenceDiagram participant User participant OneBackend participant RL Service participant Solver participant SourceChain participant TargetChain User->>OneBackend: Submit request OneBackend->>Solver: Request quote Solver-->>OneBackend: Return quote OneBackend->>User: Return quote User->>User: Sign source chain operation User->>OneBackend: Submit signed operation OneBackend->>RL Service: Forward signed operation RL Service->>RL Service: Verify & cosign operation RL Service->>OneBackend: Return operation & guarantees OneBackend->>Solver: Submit operation & guarantees Note over User,TargetChain: Fast Path via same chain Solver->>TargetChain: Execute target chain operation OneBackend->>TargetChain: Query execution result OneBackend->>User: Confirm success Note over User,TargetChain: Fast Path via cross-chain Solver->>TargetChain: Execute target chain operation OneBackend->>TargetChain: Query execution result OneBackend->>User: Confirm success OneBackend->>SourceChain: Execute source chain operation Note over User,TargetChain: Standard Path OneBackend->>SourceChain: Execute source chain operation Solver->>SourceChain: Query execution result Solver->>TargetChain: Execute target chain operation OneBackend->>TargetChain: Query execution result OneBackend->>User: Confirm success ``` ## Execution Flow Walkthrough ### 1. Initial Request Phase * User submits an intent (e.g., swap ETH aggregated across chains for SOL on Solana) * OneBackend handles the request, performing routing and other optimizations * Note that spending can happen from multiple chains within the same intent without affecting the flow. Our service determines how many funds are available on each chain and whether they are eligible for Fast or Standard Path execution. ### 2. Providing the Quote * The solver system provides a quote that specifies pricing, including gas and swap fees * The user receives the quote and decides whether to proceed * The user signs the operation with their session key and sends it back to OneBackend ### 3. Validation and Security Phase * OneBackend forwards the signed operation to our RL Service * The RL Service verifies the user's request and provides security attestation * Based on security status, the transaction follows one of these paths: * Fast Path via same-chain execution * Fast Path via cross-chain execution (immediate execution with security guarantees) * Standard Path (execution after the escrow transaction is confirmed) ### 4. Execution Phase OneBackend submits operations and security guarantees to the solver. The execution then follows one of three paths: Same-chain Fast Path is the quickest execution method, completing in seconds. 1. Solver executes the operation on the target chain 2. OneBackend verifies execution and confirms success to the user 3. OneBackend executes the operation on the source chain Cross-chain Fast Path provides immediate execution with security guarantees, bypassing source chain finality wait times. 1. Solver executes the operation on the target chain 2. OneBackend verifies execution and confirms success to the user 3. OneBackend executes the operation on the source chain Standard Path follows traditional sequential execution and is used when Fast Path conditions are not met. This path is usually at least 2x slower than the Fast Path due to waiting for source chain finality. 1. OneBackend executes the operation on the source chain 2. Solver verifies execution on the source chain 3. Solver executes the operation on the target chain 4. OneBackend verifies execution and confirms success to the user ## Fast Path Execution Criteria A multichain intent leverages the Fast Path if it's routed on the same chain **OR** if all of the following conditions are met: 1. User has enough finalized, non-pre-approved on-chain balance (aggregated across chains) that is not yet locked (spent but not yet settled) 2. There is no source-chain swap in the execution flow 3. RL Service does not exceed the total open (not yet settled) position according to the risk policy # FAQ Source: https://docs.onebalance.io/faq Common questions and answers about OneBalance platform and API Get answers to the most frequently asked questions about OneBalance. Interacting with OneBalance API General questions about OneBalance # FAQ: API Source: https://docs.onebalance.io/faq/api Interacting with OneBalance API ## How can I generate an API access token? The OneBalance API uses API keys to authenticate requests. All API requests require authentication using an API key passed in the `x-api-key` header: ```bash Terminal curl -X 'GET' \ 'https://be.onebalance.io/api/assets/list' \ -H 'x-api-key: f9703eaqsbma20tmtphg2jirm0hk8z8v2hkodrfrvhfm6ziesi7p38u991bnih5f' ``` A public API key is available for testing purposes with limited usage: **f9703eaqsbma20tmtphg2jirm0hk8z8v2hkodrfrvhfm6ziesi7p38u991bnih5f** For production use, contact [support@onebalance.io](mailto:support@onebalance.io) to get your custom API token with higher rate limits and full access to all features. All API requests must be made over [HTTPS](https://en.wikipedia.org/wiki/HTTPS). Calls made over plain HTTP will fail. API requests without authentication will also fail. ## What is a base URL? OneBalance API is built on REST principles and is served over HTTPS. To ensure data privacy, unencrypted HTTP is not supported. The Base URL for all API endpoints is: ```bash Terminal https://be.onebalance.io/api ``` ## How to fix CORS errors? OneBalance API implements Cross Origin Resource Sharing (CORS) to allow requests from browsers on different domains. CORS is generally handled by the server, not the client. If you're experiencing CORS errors in your browser application, you have several options: 1. **Use a server-side proxy** - Create a backend service that makes requests to OneBalance API and returns the data to your frontend 2. **Implement a middleware in Next.js** - If using Next.js, create route handlers that proxy the requests to the API 3. **Contact us** - If you need your domain allowed for direct browser access Learn more about CORS in the [API Reference](/api-reference/cors). ## What are the rate limits? The OneBalance API implements rate limiting to ensure fair usage and availability of the service for all users: * Public API Key: 100 requests per minute * Authenticated Users: 400 requests per minute If you exceed the rate limit, the API will return a `429 Too Many Requests` response. To handle rate limits effectively, monitor the rate limit headers in your API responses: ```bash Terminal X-RateLimit-Limit: 400 X-RateLimit-Remaining: 397 X-RateLimit-Reset: 1678364102 ``` Learn more in the [Rate Limits](/api-reference/rate-limits) documentation. ## How do I handle API errors? The API uses standard HTTP status codes to indicate success or failure: * `2xx` - Success * `4xx` - Client error (like invalid parameters) * `5xx` - Server error For errors, the API returns a JSON response with details about what went wrong: ```json Error Response { "id": "some_error_id", "error": "ErrorType", "message": "A description of what went wrong", "statusCode": 400, "timestamp": "2024-12-18T14:38:24.793Z", "path": "/api/endpoint" } ``` Always check the `message` field for details on how to resolve the issue, and include the `id` when contacting support. See the [Errors](/api-reference/errors) documentation for more information. ## How can I paginate results? For endpoints that return large collections of items, the API uses cursor-based pagination. To paginate results: 1. Specify a `limit` parameter to control the number of items per page 2. Use the `continuation` token from the response to fetch the next page ```bash // First request GET /api/status/get-tx-history?user=0x123...&limit=10 // Next page - use continuation token from previous response GET /api/status/get-tx-history?user=0x123...&limit=10&continuation=txn_abc123 ``` Check the [Pagination](/api-reference/pagination) documentation for details. # FAQ: General Source: https://docs.onebalance.io/faq/general General questions about OneBalance ## What is OneBalance? OneBalance is a comprehensive chain abstraction toolkit that enables developers to create seamless blockchain experiences. It provides a set of APIs for managing smart accounts, tracking cross-chain balances, and executing multichain transactions, all through a unified interface. The platform simplifies complex blockchain operations by abstracting away the technical details, allowing developers to focus on building great user experiences. ## What problems does OneBalance solve? OneBalance solves several key challenges in blockchain development: * **Fragmented Balances**: Consolidates assets across multiple chains into a single aggregated balance * **Complex Transactions**: Simplifies cross-chain transfers and swaps * **Account Management**: Streamlines the creation and management of smart contract wallets * **Development Complexity**: Reduces the technical overhead of supporting multiple blockchains By addressing these issues, OneBalance makes blockchain applications more accessible and user-friendly. ## Which networks does OneBalance support? OneBalance supports a growing list of networks, including: * Ethereum (Mainnet) * Arbitrum * Optimism * Base * Polygon * Linea * Avalanche * Solana (Soon) * Bitcoin (Soon) For the most up-to-date list of supported chains, you can check the [supported networks](/overview/supported-networks) page. ## What is an aggregated asset? An aggregated asset in OneBalance represents a collection of the same token across multiple blockchains. For example, USDC exists on Ethereum, Arbitrum, Base, and other networks. OneBalance treats these as a single aggregated asset with the identifier `ds:usdc`. This approach allows users to: * View total balances across all chains * Spend from the aggregated balance without worrying about which chain the tokens are on * Simplify transfers and swaps using unified token identifiers You can see the full list of aggregated assets via the `/assets/list` endpoint in our [API Reference](/api-reference/assets/list). ## How does pricing work for OneBalance? OneBalance offers flexible pricing based on usage and features: * **API Access**: Different tiers based on request volume and features * **Transaction Processing**: Competitive fees for cross-chain operations * **Enterprise Solutions**: Custom pricing for high-volume users and specialized requirements For detailed pricing information and to discuss your specific needs, [contact our sales team](mailto:sales@onebalance.io). ## How do I get started with OneBalance? The fastest way to get started is to follow our step-by-step [Onboarding Checklist](/overview/onboarding-checklist), which takes you from zero to production-ready in under an hour. For a quick overview, here's the basic flow: 1. **Understand the Basics**: Review [What is OneBalance](/overview/what-is-onebalance) and [How it Works](/overview/how-onebalance-works) 2. **Build a PoC**: Follow our [Getting Started](/getting-started/introduction) guide to make your first API call 3. **Explore Examples**: Check out our [guides](/guides/overview) and [API Reference](/api-reference/introduction) 4. **Go to Production**: Contact us for production API keys and deployment support ## Where can I get help with OneBalance? If you need assistance with OneBalance, we offer several support channels: * **Documentation**: Comprehensive guides available at [docs.onebalance.io](https://docs.onebalance.io) * **Community Support**: Follow us on [X/Twitter](https://x.com/OneBalance_io) for updates and community assistance * **Email Support**: Contact us at [support@onebalance.io](mailto:support@onebalance.io) for technical help * **Enterprise Support**: Enterprise customers receive dedicated support channels We're committed to helping developers successfully implement OneBalance in their applications. # Chain-Abstracted Swap Source: https://docs.onebalance.io/getting-started/chain-abstracted-swap Execute seamless chain-abstracted swaps with OneBalance and Privy Now that we've set up our project with Privy and OneBalance, let's create a user interface that allows users to perform chain-abstracted swaps with just a few clicks. This is where the "wow" factor comes in - users can swap tokens across chains without even knowing they're dealing with multiple networks, gas fees, or bridges. **Simplified UX**: By combining Privy's seamless wallet experience with OneBalance's chain abstraction, users don't need to worry about networks, gas, or bridging. ## Type Definitions First, let's create type definitions for our quotes and operations. Create a new file at `src/lib/types/quote.ts`: ```typescript quote.ts // src/lib/types/quote.ts export interface Account { sessionAddress: string; adminAddress: string; accountAddress: string; } export interface TokenInfo { aggregatedAssetId: string; amount: string; assetType: string | string[]; fiatValue: never; } export interface ChainOperation { userOp: { sender: string; nonce: string; callData: string; callGasLimit: string; verificationGasLimit: string; preVerificationGas: string; maxFeePerGas: string; maxPriorityFeePerGas: string; paymaster: string; paymasterVerificationGasLimit: string; paymasterPostOpGasLimit: string; paymasterData: string; signature: string; }; typedDataToSign: { domain: unknown; types: unknown; primaryType: string; message: unknown; }; assetType: string; amount: string; } export interface Quote { id: string; account: Account; originToken: TokenInfo; destinationToken: TokenInfo; expirationTimestamp: string; tamperProofSignature: string; originChainsOperations: ChainOperation[]; destinationChainOperation?: ChainOperation; } export interface QuoteStatus { quoteId: string; status: { status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'IN_PROGRESS' | 'REFUNDED'; }; user: string; recipientAccountId: string; originChainOperations: { hash: string; chainId: number; explorerUrl: string; }[]; destinationChainOperations: { hash: string; chainId: number; explorerUrl: string; }[]; } ``` ## Setting Up the Signing Utilities Now, let's set up some helper functions for signing operations with the Privy wallet. Create a new file at `src/lib/privySigningUtils.ts`: ```typescript privySigningUtils.ts // src/lib/privySigningUtils.ts import { Address, createWalletClient, custom, Hash } from 'viem'; import { ConnectedWallet } from '@privy-io/react-auth'; import { ChainOperation, Quote } from '@/lib/types/quote'; // Create a function to sign typed data with Privy wallet export const signTypedDataWithPrivy = (embeddedWallet: ConnectedWallet) => async (typedData: any): Promise => { const provider = await embeddedWallet.getEthereumProvider(); const walletClient = createWalletClient({ transport: custom(provider), account: embeddedWallet.address as Address, }); return walletClient.signTypedData(typedData); }; // Create a function to sign a chain operation export const signOperation = (embeddedWallet: ConnectedWallet) => (operation: ChainOperation): (() => Promise) => async () => { const signature = await signTypedDataWithPrivy(embeddedWallet)(operation.typedDataToSign); return { ...operation, userOp: { ...operation.userOp, signature }, }; }; // Create a function to sign the entire quote export const signQuote = async (quote: Quote, embeddedWallet: ConnectedWallet) => { const signWithEmbeddedWallet = signOperation(embeddedWallet); const signedQuote = { ...quote, }; // Sign all operations in sequence if (quote.originChainsOperations) { signedQuote.originChainsOperations = await sequentialPromises( quote.originChainsOperations.map(signWithEmbeddedWallet) ); } if (quote.destinationChainOperation) { signedQuote.destinationChainOperation = await signWithEmbeddedWallet( quote.destinationChainOperation )(); } return signedQuote; }; // Helper to run an array of lazy promises in sequence export const sequentialPromises = (promises: (() => Promise)[]): Promise => { return promises.reduce>( (acc, curr) => acc.then(results => curr().then(result => [...results, result])), Promise.resolve([]) ); }; ``` When a user initiates a swap, they'll be prompted to sign the transaction with their Privy wallet: ![Privy Signing Message](https://storage.googleapis.com/onebalance-public-assets/docs/getting-started/sign-message.png) ## Using OneBalance API Functions For our chain-abstracted swap interface, we'll use the OneBalance API functions we previously implemented in the [setup section](/getting-started/setup#prerequisites): This implementation uses three key endpoints: * [Get Quote](/api-reference/quotes/get-quote) - Get a quote for cross-chain swaps or transfers * [ExecuteQuote](/api-reference/quotes/execute-quote) - Submit a signed quote for execution * [Check Status](/api-reference/status/get-quote-status) - Track the transaction status ## Creating the Dashboard Page Now, let's create a dashboard that allows users to swap USDC for ETH (and vice versa) with built-in transaction status polling: ```tsx dashboard.tsx // src/app/dashboard/page.tsx 'use client'; import { useEffect, useState, useRef, ChangeEvent } from 'react'; import { usePrivy, useWallets } from '@privy-io/react-auth'; import { getQuote, executeQuote, checkTransactionStatus, predictAccountAddress, getAggregatedBalance } from '@/lib/onebalance'; import { Quote } from '@/lib/types/quote'; import { signQuote } from '@/lib/privySigningUtils'; import { formatUnits } from 'viem'; import { useRouter } from 'next/navigation'; export default function Dashboard() { const router = useRouter(); const { user, ready, authenticated, logout } = usePrivy(); const { wallets } = useWallets(); const [loading, setLoading] = useState(false); const [swapping, setSwapping] = useState(false); const [status, setStatus] = useState(null); const [accountAddress, setAccountAddress] = useState(null); const [usdcBalance, setUsdcBalance] = useState(null); const [ethBalance, setEthBalance] = useState(null); const [usdcChainBalances, setUsdcChainBalances] = useState>([]); const [ethChainBalances, setEthChainBalances] = useState>([]); const [quote, setQuote] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const statusPollingRef = useRef(null); const [isPolling, setIsPolling] = useState(false); const [swapAmount, setSwapAmount] = useState('5.00'); const [estimatedAmount, setEstimatedAmount] = useState(null); const [fetchingQuote, setFetchingQuote] = useState(false); const [swapDirection, setSwapDirection] = useState<'USDC_TO_ETH' | 'ETH_TO_USDC'>('USDC_TO_ETH'); const [showUsdcChainDetails, setShowUsdcChainDetails] = useState(false); const [showEthChainDetails, setShowEthChainDetails] = useState(false); // Helper function to get chain name from chain ID const getChainName = (chainId: string): string => { const chainMap: Record = { '1': 'Ethereum', '137': 'Polygon', '42161': 'Arbitrum', '10': 'Optimism', '8453': 'Base', '59144': 'Linea', '43114': 'Avalanche' }; return chainMap[chainId] || `Chain ${chainId}`; }; const embeddedWallet = wallets.find(wallet => wallet.walletClientType === 'privy'); // Handle logout and redirect to home page const handleLogout = async () => { await logout(); router.push('/'); }; // Get OneBalance account address based on Privy wallet useEffect(() => { async function setupAccount() { if (embeddedWallet && embeddedWallet.address) { try { // For this demo, we'll use the same address as both session and admin const predictedAddress = await predictAccountAddress( embeddedWallet.address, embeddedWallet.address ); setAccountAddress(predictedAddress); // Get aggregated balance for USDC and ETH fetchBalances(predictedAddress); } catch (err) { console.error('Error setting up account:', err); setError('Failed to set up OneBalance account'); } } } if (ready && authenticated) { setupAccount(); } // Clean up polling interval on unmount return () => { if (statusPollingRef.current) { clearInterval(statusPollingRef.current); } }; }, [embeddedWallet, ready, authenticated]); // Handle swap direction toggle const toggleSwapDirection = () => { // Reset current quote and estimated amount setQuote(null); setEstimatedAmount(null); // Toggle direction setSwapDirection(prevDirection => prevDirection === 'USDC_TO_ETH' ? 'ETH_TO_USDC' : 'USDC_TO_ETH' ); // Reset swap amount to a sensible default based on direction if (swapDirection === 'USDC_TO_ETH') { // Switching to ETH -> USDC, set a default ETH amount (0.001 ETH) setSwapAmount('0.001'); } else { // Switching to USDC -> ETH, set a default USDC amount (5 USDC) setSwapAmount('5.00'); } // Fetch a new quote after a brief delay to ensure state is updated setTimeout(() => { if (accountAddress && embeddedWallet) { fetchEstimatedQuote(swapDirection === 'USDC_TO_ETH' ? '0.001' : '5.00'); } }, 300); }; // Handle swap amount change const handleSwapAmountChange = async (e: ChangeEvent) => { const value = e.target.value; if (isNaN(Number(value)) || Number(value) <= 0) { return; } setSwapAmount(value); // Reset estimated amount when input changes setEstimatedAmount(null); // If we have a valid amount and an account, fetch a new quote if (Number(value) > 0 && accountAddress && embeddedWallet) { fetchEstimatedQuote(value); } }; // Fetch a quote for estimation purposes const fetchEstimatedQuote = async (amountStr: string) => { if (!accountAddress || !embeddedWallet) return; try { setFetchingQuote(true); // Convert to smallest unit based on direction // USDC has 6 decimals, ETH has 18 decimals const amount = swapDirection === 'USDC_TO_ETH' ? (parseFloat(amountStr) * 1_000_000).toString() // USDC -> ETH (6 decimals) : (parseFloat(amountStr) * 1e18).toString(); // ETH -> USDC (18 decimals) const quoteRequest = { from: { account: { sessionAddress: embeddedWallet.address, adminAddress: embeddedWallet.address, accountAddress: accountAddress, }, asset: { assetId: swapDirection === 'USDC_TO_ETH' ? 'ds:usdc' : 'ds:eth', }, amount, }, to: { asset: { assetId: swapDirection === 'USDC_TO_ETH' ? 'ds:eth' : 'ds:usdc', }, }, }; const quoteResponse = await getQuote(quoteRequest); setQuote(quoteResponse); // Extract estimated amount from the quote if (quoteResponse.destinationToken && quoteResponse.destinationToken.amount) { // Format based on direction if (swapDirection === 'USDC_TO_ETH') { // USDC -> ETH (ETH has 18 decimals) const ethAmount = parseFloat(formatUnits(BigInt(quoteResponse.destinationToken.amount), 18)).toFixed(6); setEstimatedAmount(ethAmount); } else { // ETH -> USDC (USDC has 6 decimals) const usdcAmount = parseFloat(formatUnits(BigInt(quoteResponse.destinationToken.amount), 6)).toFixed(2); setEstimatedAmount(usdcAmount); } } } catch (err) { console.error('Error fetching quote for estimation:', err); setEstimatedAmount(null); } finally { setFetchingQuote(false); } }; // Fetch USDC and ETH balances const fetchBalances = async (address: string) => { try { const balanceData = await getAggregatedBalance(address); // Find USDC in the balance data const usdcAsset = balanceData.balanceByAggregatedAsset.find( (asset: any) => asset.aggregatedAssetId === 'ds:usdc' ); // Find ETH in the balance data const ethAsset = balanceData.balanceByAggregatedAsset.find( (asset: any) => asset.aggregatedAssetId === 'ds:eth' ); if (usdcAsset) { // Format the balance (USDC has 6 decimals) const formattedBalance = parseFloat(formatUnits(BigInt(usdcAsset.balance), 6)).toFixed(2); setUsdcBalance(formattedBalance); // Extract individual chain balances for USDC if (usdcAsset.individualAssetBalances && usdcAsset.individualAssetBalances.length > 0) { const chainBalances = usdcAsset.individualAssetBalances .map((chainBalance: any) => { // Extract chain ID from assetType (format: eip155:CHAIN_ID/...) const chainId = chainBalance.assetType.split(':')[1].split('/')[0]; const formattedBalance = parseFloat(formatUnits(BigInt(chainBalance.balance), 6)).toFixed(2); return { chainId, balance: formattedBalance, numericBalance: parseFloat(formattedBalance), // For sorting assetType: chainBalance.assetType }; }) // Filter out zero balances .filter((chainBalance: {numericBalance: number}) => chainBalance.numericBalance > 0) // Sort by balance in descending order .sort((a: {numericBalance: number}, b: {numericBalance: number}) => b.numericBalance - a.numericBalance); setUsdcChainBalances(chainBalances); } } else { setUsdcBalance('0.00'); setUsdcChainBalances([]); } if (ethAsset) { // Format the balance (ETH has 18 decimals) const formattedBalance = parseFloat(formatUnits(BigInt(ethAsset.balance), 18)).toFixed(6); setEthBalance(formattedBalance); // Extract individual chain balances for ETH if (ethAsset.individualAssetBalances && ethAsset.individualAssetBalances.length > 0) { const chainBalances = ethAsset.individualAssetBalances .map((chainBalance: any) => { // Extract chain ID from assetType (format: eip155:CHAIN_ID/...) const chainId = chainBalance.assetType.split(':')[1].split('/')[0]; const formattedBalance = parseFloat(formatUnits(BigInt(chainBalance.balance), 18)).toFixed(6); return { chainId, balance: formattedBalance, numericBalance: parseFloat(formattedBalance), // For sorting assetType: chainBalance.assetType }; }) // Filter out zero balances .filter((chainBalance: {numericBalance: number}) => chainBalance.numericBalance > 0) // Sort by balance in descending order .sort((a: {numericBalance: number}, b: {numericBalance: number}) => b.numericBalance - a.numericBalance); setEthChainBalances(chainBalances); } } else { setEthBalance('0.000000'); setEthChainBalances([]); } // After getting balances, fetch an initial quote estimate using the default amount if (address && embeddedWallet) { fetchEstimatedQuote(swapAmount); } } catch (err) { console.error('Error fetching balances:', err); setUsdcBalance('0.00'); setEthBalance('0.000000'); setUsdcChainBalances([]); setEthChainBalances([]); } }; // Poll for transaction status const startStatusPolling = (quoteId: string) => { if (statusPollingRef.current) { clearInterval(statusPollingRef.current); } setIsPolling(true); statusPollingRef.current = setInterval(async () => { try { const statusData = await checkTransactionStatus(quoteId); setStatus(statusData); // If the transaction is completed or failed, stop polling if (statusData.status === 'COMPLETED' || statusData.status === 'FAILED') { if (statusPollingRef.current) { clearInterval(statusPollingRef.current); setIsPolling(false); } // Refresh balances after transaction is completed if (accountAddress && statusData.status === 'COMPLETED') { fetchBalances(accountAddress); } } } catch (err) { console.error('Error polling transaction status:', err); if (statusPollingRef.current) { clearInterval(statusPollingRef.current); setIsPolling(false); } } }, 1000); // Poll every 1 second }; // Request and execute a chain-abstracted swap const handleSwap = async () => { if (!accountAddress || !embeddedWallet) { setError('Wallet not connected or OneBalance account not set up'); return; } setLoading(true); setSwapping(true); setError(null); setSuccess(false); try { // Use already fetched quote if available let quoteResponse = quote; // If no quote available or it's stale, fetch a new one if (!quoteResponse) { // Convert to smallest unit based on direction const amount = swapDirection === 'USDC_TO_ETH' ? (parseFloat(swapAmount) * 1_000_000).toString() // USDC -> ETH (6 decimals) : (parseFloat(swapAmount) * 1e18).toString(); // ETH -> USDC (18 decimals) const quoteRequest = { from: { account: { sessionAddress: embeddedWallet.address, adminAddress: embeddedWallet.address, accountAddress: accountAddress, }, asset: { assetId: swapDirection === 'USDC_TO_ETH' ? 'ds:usdc' : 'ds:eth', }, amount, }, to: { asset: { assetId: swapDirection === 'USDC_TO_ETH' ? 'ds:eth' : 'ds:usdc', }, }, }; quoteResponse = await getQuote(quoteRequest); setQuote(quoteResponse); } if (!quoteResponse) { throw new Error('Failed to get a quote for the swap'); } // Step 2: Sign the quote const signedQuote = await signQuote(quoteResponse, embeddedWallet); // Step 3: Execute the quote setLoading(true); const executionResponse = await executeQuote(signedQuote); // Step 4: Start polling for transaction status startStatusPolling(quoteResponse.id); setSuccess(true); setLoading(false); } catch (err: any) { console.error('Error in swap process:', err); setError(err.message || 'Failed to complete swap'); setLoading(false); } finally { setSwapping(false); } }; if (!ready || !authenticated) { return (
); } return (

OneBalance Dashboard

Connected as
{user?.email?.address || embeddedWallet?.address || 'Anonymous'}
{/* Account Info Section */}

Account Information

OneBalance Smart Account
{accountAddress ||
}
USDC Balance
{usdcBalance ? `${usdcBalance} USDC` :
}
setShowUsdcChainDetails(!showUsdcChainDetails)}> {showUsdcChainDetails ? "Hide chain details" : "Show chain details"}
{showUsdcChainDetails && (
{usdcChainBalances.length > 0 ? ( usdcChainBalances.map((chainBalance, idx) => (
{getChainName(chainBalance.chainId)} {chainBalance.balance} USDC
)) ) : (
No chain data available
)}
)}
ETH Balance
{ethBalance ? `${ethBalance} ETH` :
}
setShowEthChainDetails(!showEthChainDetails)}> {showEthChainDetails ? "Hide chain details" : "Show chain details"}
{showEthChainDetails && (
{ethChainBalances.length > 0 ? ( ethChainBalances.map((chainBalance, idx) => (
{getChainName(chainBalance.chainId)} {chainBalance.balance} ETH
)) ) : (
No chain data available
)}
)}

Chain-Abstracted Swap

From
{swapDirection === 'USDC_TO_ETH' ? 'USDC' : 'ETH'}
on any chain
↔
To
{fetchingQuote ? (
) : estimatedAmount ? ( `${estimatedAmount} ${swapDirection === 'USDC_TO_ETH' ? 'ETH' : 'USDC'}` ) : ( swapDirection === 'USDC_TO_ETH' ? 'ETH' : 'USDC' )}
on any chain
No gas tokens needed - pay fees with your {swapDirection === 'USDC_TO_ETH' ? 'USDC' : 'ETH'} balance!
{error && (

Error

{error}

)} {success && (

Success!

Your chain-abstracted swap has been initiated. {isPolling && ' Monitoring transaction status...'}

)} {status && (

Transaction Status

Status: {status.status || 'Pending'}
{status.originChainOperations?.length > 0 && (
Origin Chain: View Transaction
)} {status.destinationChainOperations?.length > 0 && (
Destination Chain: View Transaction
)}
)}
); } ``` When the swap is successfully completed, users can see the status of the transaction: ![Successful Swap](https://storage.googleapis.com/onebalance-public-assets/docs/getting-started/successful-swap.png) ## Understanding the Chain-Abstracted Swap Flow This improved implementation offers several key enhancements over the basic version: 1. **Bidirectional Swapping** * Users can swap from USDC β†’ ETH or ETH β†’ USDC * The UI dynamically updates based on the selected direction 2. **Real-Time Quote Estimation** * When users input an amount, a quote is fetched to show the estimated output * This provides transparency about the expected exchange rate 3. **Balance Display** * Shows users their current USDC and ETH balances from across all chains * Updates balances automatically after a successful swap 4. **Robust Transaction Polling** * Implements a proper polling mechanism to track transaction status * Handles different transaction states (pending, completed, failed) * Automatically stops polling when the transaction completes or fails 5. **Error Handling & Recovery** * Provides clear error messages when issues occur * Implements clean-up logic to prevent resource leaks (clearing intervals) 6. **Enhanced User Experience** * Shows loading indicators during swap operations * Provides visual feedback for transaction states * Includes links to block explorers to view transactions ## Multichain Asset Visibility One of the most powerful aspects of this implementation is how it handles multichain asset visibility. When users interact with the application: * They see their USDC and ETH balances as a **single aggregated total** across all supported chains * They can select "from" and "to" assets without needing to specify which chain they're on * The underlying complexity of potentially interacting with multiple blockchains is completely hidden This creates an abstraction layer where users don't need to think about chains at all - they simply work with tokens as if they existed in a unified space, which dramatically simplifies the user experience. ## Key Technical Implementations ### 1. Transaction Status Polling ```typescript status-polling.ts // Poll for transaction status const startStatusPolling = (quoteId: string) => { if (statusPollingRef.current) { clearInterval(statusPollingRef.current); } setIsPolling(true); statusPollingRef.current = setInterval(async () => { try { const statusData = await checkTransactionStatus(quoteId); setStatus(statusData); // If the transaction is completed or failed, stop polling if (statusData.status.status === 'COMPLETED' || statusData.status.status === 'FAILED') { if (statusPollingRef.current) { clearInterval(statusPollingRef.current); setIsPolling(false); } // Refresh balances after transaction is completed if (accountAddress && statusData.status.status === 'COMPLETED') { fetchBalances(accountAddress); } } } catch (err) { console.error('Error polling transaction status:', err); if (statusPollingRef.current) { clearInterval(statusPollingRef.current); setIsPolling(false); } } }, 1000); // Poll every 1 second }; ``` This polling mechanism uses the [Get Execution Status](/api-reference/status/get-quote-status) endpoint to track transaction progress in real-time. ### 2. Signing with Privy A critical part of the implementation is securely signing operations using the Privy wallet. The signing process handles: * Converting the typed data format required by EIP-712 * Managing the signature process using the embedded wallet * Ensuring all operations in a quote are properly signed * Handling both origin and destination chain operations ```typescript swap-flow.ts // Example of the signing flow const handleSwap = async () => { // ... other code // Get the quote from OneBalance const quoteResponse = await getQuote(quoteRequest); // Sign the quote using the Privy wallet const signedQuote = await signQuote(quoteResponse, embeddedWallet); // Execute the signed quote await executeQuote(signedQuote); // ... other code }; ``` ### 3. Balance Checking Across Chains ```typescript balance-checker.ts // Fetch USDC and ETH balances const fetchBalances = async (address: string) => { try { const balanceData = await getAggregatedBalance(address); // Find USDC in the balance data const usdcAsset = balanceData.balanceByAggregatedAsset.find( (asset: any) => asset.aggregatedAssetId === 'ds:usdc' ); // Find ETH in the balance data const ethAsset = balanceData.balanceByAggregatedAsset.find( (asset: any) => asset.aggregatedAssetId === 'ds:eth' ); if (usdcAsset) { // Format the balance (USDC has 6 decimals) const formattedBalance = parseFloat(formatUnits(BigInt(usdcAsset.balance), 6)).toFixed(2); setUsdcBalance(formattedBalance); } if (ethAsset) { // Format the balance (ETH has 18 decimals) const formattedBalance = parseFloat(formatUnits(BigInt(ethAsset.balance), 18)).toFixed(6); setEthBalance(formattedBalance); } } catch (err) { console.error('Error fetching balances:', err); } }; ``` This function leverages the [Aggregated Balance](/api-reference/balances/aggregated-balance) endpoint to retrieve all token balances across multiple chains with a single API call. ## What Makes This Implementation Powerful? 1. **Comprehensive Chain Abstraction** * Users can swap tokens across chains without even knowing which chains are involved * The UI treats USDC and ETH as unified assets regardless of their underlying chains 2. **Robust Error Handling** * Handles API errors gracefully * Provides clear feedback to users * Implements proper cleanup of resources 3. **Real-Time Transaction Tracking** * Users can see the status of their transactions as they progress * Provides links to block explorers for deeper inspection * Automatically refreshes balances when transactions complete 4. **Bidirectional Swapping** * Users can swap in either direction (USDC β†’ ETH or ETH β†’ USDC) * The UI adapts dynamically to the selected direction 5. **Gas Abstraction** * Users can pay transaction fees with the token they're swapping * No need to hold native gas tokens on each chain ## Next Steps Now that you've seen how to create a chain-abstracted swap interface with OneBalance, you can explore the complete working implementation: Get the full working code example to start building immediately Discover all available endpoints in our API documentation # Your First API Call Source: https://docs.onebalance.io/getting-started/first-api-call Set up a smart contract account for chain-abstracted transactions Before you can execute chain-abstracted operations with OneBalance, you need to set up a smart contract account (SCA). This smart contract account will hold your assets and enable gas abstraction across chains. **Key Concept**: OneBalance uses smart contract accounts (SCAs) to enable chain and gas abstraction. Your SCA is your actual deposit address where funds are stored, and your Privy wallet acts as a **signer** for this account. ## Predicting Your Account Address Now that we've set up our project with proper CORS handling, let's use the OneBalance API to predict a smart contract account (SCA) address based on Privy's embedded wallet addresses: First, create a component that will handle the account setup: ```tsx AccountSetup.tsx // src/components/AccountSetup.tsx 'use client'; // Example usage in a component import { useEffect, useState } from 'react'; import { useWallets } from '@privy-io/react-auth'; import { predictAccountAddress } from '@/lib/onebalance'; export function AccountSetup() { const { wallets } = useWallets(); const [accountAddress, setAccountAddress] = useState(null); // Find the Privy-managed embedded wallet const embeddedWallet = wallets.find(wallet => wallet.walletClientType === 'privy'); useEffect(() => { async function setupAccount() { if (embeddedWallet && embeddedWallet.address) { try { // Using the same address as both session and admin for simplicity const predictedAddress = await predictAccountAddress( embeddedWallet.address, embeddedWallet.address ); setAccountAddress(predictedAddress); console.log(`Smart Contract Account Address: ${predictedAddress}`); } catch (err) { console.error('Error setting up account:', err); } } } setupAccount(); }, [embeddedWallet]); return (
{accountAddress ? (

Your OneBalance Smart Account: {accountAddress}

) : (

Loading account address...

)}
); } ``` This component uses the [Predict Address](/api-reference/account/predict-address) endpoint to determine your smart contract account address before it's deployed. Next, create a dashboard page that will use this component: ```tsx page.tsx // src/app/dashboard/page.tsx import { AccountSetup } from '@/components/AccountSetup'; export default function Dashboard() { return (

OneBalance Dashboard

); } ``` When a user logs in with Privy on the homepage, they'll be redirected to this dashboard page where their predicted smart contract account address will be displayed. This is your first successful API call to OneBalance! The `predictAccountAddress` function makes an API call to OneBalance's `/account/predict-address` endpoint with the Privy wallet address as both the session and admin address. This deterministically calculates the smart contract account address that will be used for all your OneBalance operations. When a user logs in with Privy via email (as shown in the previous section), they receive a verification code to complete the authentication process. Once verified, Privy creates an embedded wallet for them automatically, which becomes available through the `useWallets()` hook shown above. ## Understanding Smart Contract Accounts (SCAs) OneBalance uses smart contract accounts to enable key features like: 1. **Gas Abstraction**: Pay for transactions using any token in your balance, not just the chain's native gas token 2. **Resource Locks**: Secure validation of transaction intents without requiring on-chain confirmations for every step 3. **Fast Path**: Near-instant transactions without waiting for block confirmations **Important Account Relationship**: - The **SCA address** (predicted by `/account/predict-address`) is your deposit address where funds are stored - The **Privy-managed EOA wallet** acts as a **signer** for your SCA, authorizing transactions - Always send funds to the SCA address, not to the Privy wallet address Smart contract accounts are **counterfactual**, meaning they don't need to be deployed until they're actually used in a transaction. This saves gas costs and simplifies the user experience. The account setup in this quickstart uses a common configuration for ease of use. OneBalance offers more advanced modular account options for specialized use cases, which you can explore in our [Core Concepts: Account Models](/concepts/accounts) section. ## Checking Account Balances Before performing operations, you'll want to check if your account has sufficient funds. The OneBalance API allows you to fetch aggregated balances across all chains: ```typescript balanceChecker.ts // Example of checking for USDC and ETH balances import { getAggregatedBalance } from '@/lib/onebalance'; import { formatUnits } from 'viem'; async function checkBalances(accountAddress: string) { try { const balanceData = await getAggregatedBalance(accountAddress); // Find USDC in the balance data const usdcAsset = balanceData.balanceByAggregatedAsset.find( (asset: any) => asset.aggregatedAssetId === 'ds:usdc' ); // Find ETH in the balance data const ethAsset = balanceData.balanceByAggregatedAsset.find( (asset: any) => asset.aggregatedAssetId === 'ds:eth' ); if (usdcAsset) { // Format the balance (USDC has 6 decimals) const formattedBalance = parseFloat(formatUnits(BigInt(usdcAsset.balance), 6)).toFixed(2); console.log(`USDC Balance: ${formattedBalance} USDC`); } if (ethAsset) { // Format the balance (ETH has 18 decimals) const formattedBalance = parseFloat(formatUnits(BigInt(ethAsset.balance), 18)).toFixed(6); console.log(`ETH Balance: ${formattedBalance} ETH`); } } catch (err) { console.error('Error fetching balances:', err); } } ``` **Asset ID Format**: OneBalance uses the "ds:" prefix for aggregated asset identifiers (e.g., `ds:usdc`, `ds:eth`). This prefix indicates that these are aggregated assets that represent the same token across multiple chains. For example, `ds:usdc` represents USDC from Ethereum, Polygon, Arbitrum, and other supported networks as a single unified asset. Learn more about how aggregated assets work in [Aggregated Assets](/concepts/aggregated-assets). This example uses the [Get Aggregated Balance](/api-reference/balances/aggregated-balance) endpoint to fetch your token balances across all supported chains with a single API call. This function fetches the aggregated balance for an account and formats the values correctly: * USDC has 6 decimals, so we divide by 10^6 * ETH has 18 decimals, so we divide by 10^18 The balances shown represent the total amount available across all supported chains, providing users with a unified view of their assets. ![Dashboard Overview](https://storage.googleapis.com/onebalance-public-assets/docs/getting-started/dashboard-overview.png) Your dashboard will show aggregated balances across multiple chains as a single, unified total. For example, if you have 10 USDC on Ethereum, 5 USDC on Polygon, and 5 USDC on Arbitrum, your dashboard will display a total of 20 USDC. This multichain aggregation is a key benefit of using OneBalance, as it eliminates the need to track assets separately across different networks. Learn how OneBalance unifies tokens across multiple chains into single aggregated assets ![Dashboard Account Info](https://storage.googleapis.com/onebalance-public-assets/docs/getting-started/account-info.png) In the full implementation (next section), we'll enhance the dashboard to display both the aggregated balance and a breakdown of assets per chain. Users can toggle to see exactly how their funds are distributed across networks like Ethereum, Arbitrum, Optimism, and others. **Cross-Chain Visibility**: OneBalance automatically tracks your assets across all supported chains, so you don't need to switch networks or use separate wallets to view your complete holdings. All tokens of the same type (e.g., USDC) are presented as a single aggregated balance, even if they're distributed across multiple networks. ## Funding Your Account Before you can perform any operations with your smart contract account, you'll need to fund it. Send tokens to the `predictedAddress` returned from the API. This is your SCA's deposit address. Remember to send funds to the SCA address (predictedAddress), not to your Privy wallet addresses. The predictedAddress is where your funds will be stored and managed across all chains. ## Next Steps Now that you've learned how to set up a smart contract account and check balances, you're ready to execute chain-abstracted operations. In the next section, we'll use the predicted account address to create a quote for swapping tokens across chains, with no bridging required. Learn how to execute seamless chain-abstracted swaps using your new smart contract account # Introduction Source: https://docs.onebalance.io/getting-started/introduction Build chain-abstracted experiences in minutes, not months export const LoomVideo = ({loomUrl}) => { return