diff --git a/docs.json b/docs.json index fb28eb9f..62dc52bc 100644 --- a/docs.json +++ b/docs.json @@ -1007,7 +1007,8 @@ "guides/building-relaying-server", "guides/analytics-guide", "guides/build-embedding-wallet", - "guides/use-with-privy" + "guides/use-with-privy", + "guides/embedded-wallet-yield-with-trails" ] }, { diff --git a/guides/embedded-wallet-yield-with-trails.mdx b/guides/embedded-wallet-yield-with-trails.mdx new file mode 100644 index 00000000..bb14c0be --- /dev/null +++ b/guides/embedded-wallet-yield-with-trails.mdx @@ -0,0 +1,592 @@ +--- +title: "Build a Yield Flow with Embedded Wallet + Trails" +description: "Learn how to build a white-labeled yield deposit flow using Sequence Embedded Wallet for authentication and signing, and Trails intents for routing assets into a target vault." +sidebarTitle: Embedded Wallet Yield with Trails +--- + +Time to complete: 20-30 minutes + +In this guide, you'll learn how to build a white-labeled yield deposit flow where users authenticate with Sequence Embedded Wallet, choose an exact `USDC` deposit amount for a vault on `Polygon`, and let Trails figure out how to route funds from the user's current chain and token into that final deposit. + +This walkthrough assumes: + +- your app owns the full mobile or web UI +- Sequence Embedded Wallet is your authentication and signing layer +- Trails is your routing and settlement layer +- your backend decides which vault or lending market to target +- the user may fund the deposit from a different supported token or chain + + +This guide focuses on a practical v1 implementation: + +- one destination market or vault +- `USDC` as the destination deposit asset +- `Polygon` as the destination chain +- a user-initiated deposit flow + +If you later want recurring deposits or delegated actions, you can layer Smart Sessions on top. They are not required for the first version. + + +This can be accomplished with 8 steps: + +1. [Create your Builder project and collect keys](/guides/embedded-wallet-yield-with-trails#1-create-your-builder-project-and-collect-keys) +2. [Install dependencies](/guides/embedded-wallet-yield-with-trails#2-install-dependencies) +3. [Initialize Sequence Embedded Wallet](/guides/embedded-wallet-yield-with-trails#3-initialize-sequence-embedded-wallet) +4. [Read the user's balances](/guides/embedded-wallet-yield-with-trails#4-read-the-users-balances) +5. [Define and encode the destination deposit call](/guides/embedded-wallet-yield-with-trails#5-define-and-encode-the-destination-deposit-call) +6. [Quote and commit the Trails intent](/guides/embedded-wallet-yield-with-trails#6-quote-and-commit-the-trails-intent) +7. [Fund the intent from the embedded wallet](/guides/embedded-wallet-yield-with-trails#7-fund-the-intent-from-the-embedded-wallet) +8. [Execute the intent and wait for the receipt](/guides/embedded-wallet-yield-with-trails#8-execute-the-intent-and-wait-for-the-receipt) + +## 1. Create your Builder project and collect keys + +First, create or open a Sequence Builder project and configure Embedded Wallet for your app. + +You will need: + +- a `PROJECT_ACCESS_KEY` +- a `WAAS_CONFIG_KEY` +- a `TRAILS_API_KEY` + +Sequence setup references: + +- [Embedded Wallet configuration](/solutions/builder/embedded-wallet/configuration) +- [Initialization and Authentication](/sdk/headless-wallet/authentication) + +Trails references: + +- [Trails overview](/solutions/payments/trails) +- [Trails API Getting Started](https://docs.trails.build/api-reference/introduction) + +## 2. Install dependencies + +Install the Sequence WaaS SDK, Trails API SDK, and `viem` for calldata encoding. + + +```bash pnpm +pnpm add @0xsequence/waas @0xtrails/api viem +``` + +```bash npm +npm install @0xsequence/waas @0xtrails/api viem +``` + +```bash yarn +yarn add @0xsequence/waas @0xtrails/api viem +``` + + +Create environment variables for both your frontend and backend: + +```bash +VITE_PROJECT_ACCESS_KEY=your_sequence_project_access_key +VITE_WAAS_CONFIG_KEY=your_sequence_waas_config_key +TRAILS_API_KEY=your_trails_api_key +``` + + +Keep your `TRAILS_API_KEY` on the backend. Your client should call your backend for quote, commit, execute, and receipt polling. + + +## 3. Initialize Sequence Embedded Wallet + +Initialize the Sequence WaaS SDK and authenticate the user. + +```ts [lib/sequence.ts] +import { SequenceWaaS } from "@0xsequence/waas" + +export const sequence = new SequenceWaaS({ + projectAccessKey: import.meta.env.VITE_PROJECT_ACCESS_KEY, + waasConfigKey: import.meta.env.VITE_WAAS_CONFIG_KEY, + network: "polygon", +}) +``` + +After your user completes sign-in, retrieve the wallet address: + +```ts +await sequence.signIn({ idToken }, "yield-session") + +const ownerAddress = await sequence.getAddress() +console.log(ownerAddress) +``` + +You can use any supported Embedded Wallet authentication method here: + +- social `idToken` +- email +- guest wallet + +See [Initialization and Authentication](/sdk/headless-wallet/authentication) for the full authentication options. + +## 4. Read the user's balances + +Before you can construct a useful deposit quote, you need to know which token and chain the user will fund from. + +Use the Sequence indexer to fetch the user's balances: + +```ts [server/getBalances.ts] +export async function getBalances(ownerAddress: string) { + const response = await fetch( + "https://indexer.sequence.app/rpc/IndexerGateway/GetTokenBalances", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Access-Key": process.env.TRAILS_API_KEY!, + }, + body: JSON.stringify({ + accountAddress: ownerAddress, + includeMetadata: true, + }), + } + ) + + return response.json() +} +``` + +For this guide, keep the usage simple: + +- show the user supported balances +- let them choose a source token and chain +- pass `originChainId`, `originTokenAddress`, and the desired deposit amount into your quote request + +This gives Trails the origin context it needs while keeping the walkthrough compact. + +## 5. Define and encode the destination deposit call + +For a white-labeled flow, your backend should decide which target contract is valid for deposits. + +For example, you might support a single ERC-4626 vault and store: + +- `chainId` +- `vaultAddress` +- `depositTokenAddress` +- protocol display name +- ABI fragment required for deposit + +Then encode the deposit calldata using `viem`: + +```ts [server/encodeVaultDeposit.ts] +import { encodeFunctionData } from "viem" + +export function encodeVaultDeposit( + destinationAmountAtomic: bigint, + ownerAddress: `0x${string}` +) { + return encodeFunctionData({ + abi: [ + { + type: "function", + name: "deposit", + stateMutability: "nonpayable", + inputs: [ + { name: "assets", type: "uint256" }, + { name: "receiver", type: "address" }, + ], + outputs: [{ name: "shares", type: "uint256" }], + }, + ], + functionName: "deposit", + args: [destinationAmountAtomic, ownerAddress], + }) +} +``` + +If you are targeting Aave instead, Trails' `earn` docs use the pool's `supply(asset, amount, onBehalfOf, referralCode)` method. + + +This walkthrough uses a fixed destination amount. The user chooses how much `USDC` should arrive at the destination vault on `Polygon`, and Trails computes how much source liquidity is required. + +Even with that simplified setup, Trails is still doing the important work in this architecture: + +- expressing the deposit as an intent +- routing user funds from the origin chain and token to the final destination +- executing the destination contract call +- returning a receipt model your app can track + +This is the cleanest way to showcase Trails in a yield flow without adding extra UI complexity. + + +## 6. Quote and commit the Trails intent + +Once you know the owner address, source token, amount, and target vault call, your backend can request a Trails quote. + +```ts [server/trails.ts] +import { TradeType, TrailsApi } from "@0xtrails/api" + +const trails = new TrailsApi(process.env.TRAILS_API_KEY!) +const USDC_POLYGON = "0x..." as const +const VAULT_ADDRESS = "0x..." as const + +export async function quoteYieldDeposit({ + ownerAddress, + originChainId, + originTokenAddress, + destinationAmountAtomic, + destinationCallData, +}: { + ownerAddress: `0x${string}` + originChainId: number + originTokenAddress: `0x${string}` + destinationAmountAtomic: bigint + destinationCallData: `0x${string}` +}) { + return trails.quoteIntent({ + ownerAddress, + originChainId, + originTokenAddress, + destinationChainId: 137, + destinationTokenAddress: USDC_POLYGON, + destinationTokenAmount: destinationAmountAtomic, + destinationToAddress: VAULT_ADDRESS, + destinationCallData, + destinationCallValue: 0n, + tradeType: TradeType.EXACT_OUTPUT, + }) +} +``` + +In this version of the flow, your app fixes the destination: + +- a vault on `Polygon` +- `USDC` as the destination token +- an exact amount of `USDC` to deposit + +Trails then computes: + +- how much source liquidity is required +- whether a swap is needed +- whether a bridge is needed +- how to settle the final contract call on the destination chain + +After quoting, commit the exact returned intent: + +```ts +const { intent } = await quoteYieldDeposit({ + ownerAddress, + originChainId, + originTokenAddress, + destinationAmountAtomic, + destinationCallData, +}) + +const { intentId } = await trails.commitIntent({ + intent, +}) +``` + + +Do not mutate the returned `intent` object before calling `commitIntent`. Commit the exact quote returned by Trails. + + +## 7. Fund the intent from the embedded wallet + +Trails returns a `depositTransaction` on the quoted intent. That is the transaction your user must fund from their embedded wallet. + +Read the funding information from the intent: + +```ts +const intentAddress = intent.depositTransaction.toAddress +const depositTokenAddress = intent.depositTransaction.tokenAddress +const depositAmount = intent.depositTransaction.amount +const depositChainId = intent.originChainId +``` + +Then submit the token transfer from the user's Sequence wallet with `sendERC20`: + +```ts [client/fundIntent.ts] +import { isSentTransactionResponse } from "@0xsequence/waas" +import { sequence } from "./sequence" + +export async function fundIntent({ + depositChainId, + depositTokenAddress, + intentAddress, + depositAmount, +}: { + depositChainId: number + depositTokenAddress: string + intentAddress: string + depositAmount: string +}) { + const tx = await sequence.sendERC20({ + chainId: depositChainId, + token: depositTokenAddress, + to: intentAddress, + value: depositAmount, + }) + + if (!isSentTransactionResponse(tx)) { + throw new Error("Intent funding transaction failed") + } + + return tx.data.txHash +} +``` + +The returned transaction hash is your `depositTransactionHash`. + + +This is the point where the Trails-powered flow becomes visible to the user: they can fund a deposit from the origin asset returned by the quote, while your app still guarantees the final deposit lands in the target `Polygon` vault as `USDC`. + + +## 8. Execute the intent and wait for the receipt + +Once the funding transaction has been submitted, execute the Trails intent on your backend: + +```ts [server/executeIntent.ts] +export async function executeYieldDeposit({ + intentId, + depositTransactionHash, +}: { + intentId: string + depositTransactionHash: string +}) { + await trails.executeIntent({ + intentId, + depositTransactionHash, + }) + + return trails.waitIntentReceipt({ + intentId, + }) +} +``` + +Use the receipt to drive your user experience: + +- show pending and completed states +- link to the origin and destination transactions +- reconcile deposits in your backend +- surface support information if execution fails + +## Suggested API shape + +If you want this to feel like a product-level yield API inside your app, create a thin backend layer on top of Sequence and Trails. + +Recommended endpoints: + +- `POST /yield/quote` +- `POST /yield/commit` +- `POST /yield/execute` +- `GET /yield/receipt/:intentId` +- `GET /yield/opportunities` +- `GET /yield/positions` + +This keeps the client simple while letting your backend own: + +- supported vault metadata +- calldata encoding +- Trails API access +- receipt monitoring +- APY and earnings data + +This keeps your client integration stable while Trails handles: + +- route construction +- bridge and swap orchestration +- destination execution +- completion tracking + +## Testnet path + +Before wiring a production vault, it is useful to validate the full intent lifecycle on testnet. + +The safest way to do that is: + +1. query Trails for supported chains +2. filter for testnets +3. fetch supported tokens for the selected testnet +4. swap the production addresses in this guide for testnet addresses + +For example: + +```ts [server/getTestnetChains.ts] +const chains = await trails.getChains() + +const testnetChains = chains.chains.filter((chain) => chain.isTestnet) +``` + +Then fetch supported tokens for the chain you want to use: + +```ts [server/getTestnetTokens.ts] +const tokenList = await trails.getTokenList({ + chainIds: [selectedTestnetChainId], +}) +``` + +At that point, replace the production constants in the guide with: + +- a testnet `originChainId` +- a supported testnet source token +- a testnet destination token +- a testnet vault or lending market address + +If you want a public protocol target for testing, Aave V3 is usually the easiest place to start because its pool interface is public and it exposes both `supply(...)` and `withdraw(...)` methods. + +For Morpho, do not assume there is a public official testnet vault you can target. Morpho vaults are permissionless and you can deploy your own ERC-4626-style test vault, but the public Morpho vault documentation and addresses pages do not provide a general official testnet vault catalog in the same way you might expect for mainnet integrations. + + +Keep the integration model exactly the same on testnet: + +- quote the Trails intent +- fund the returned deposit transaction +- execute the intent +- wait for the receipt + +Use testnet to validate the integration flow, not the economic behavior. + +In practice, the safest test targets are: + +- an Aave V3 testnet market with published addresses +- your own test ERC-4626 vault + +That validates the product flow before you introduce production assets. + + +## Withdrawals + +For a first release, keep withdrawals as a two-stage flow: + +1. call the vault's `withdraw(...)` or `redeem(...)` method directly from the user's Sequence wallet +2. if needed, create a second Trails intent to move the withdrawn funds into another token or chain + +For an ERC-4626-style vault: + +```ts +const tx = await sequence.callContract({ + to: VAULT_ADDRESS, + abi: "withdraw(uint256 assets, address receiver, address owner)", + func: "withdraw", + args: { + assets: amountAtomic, + receiver: ownerAddress, + owner: ownerAddress, + }, + value: 0, +}) +``` + +For Aave V3, the public pool interface exposes: + +- `supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)` +- `withdraw(address asset, uint256 amount, address to)` + +You can review the user-facing Aave flow here: + +- [Supply Tokens | Aave](https://aave.com/help/supplying/supply-tokens) + +So the same model applies there: + +- deposit with a Trails destination call to `supply(...)` +- withdraw directly with the user's wallet by calling `withdraw(...)` + +If the withdrawn asset is already on the correct chain and in the correct token, stop there. + +If not, follow the withdrawal with a second Trails intent: + +```ts +const quoteResponse = await trails.quoteIntent({ + ownerAddress, + originChainId: withdrawalChainId, + originTokenAddress: withdrawnTokenAddress, + originTokenAmount: withdrawnAmount, + destinationChainId: preferredChainId, + destinationTokenAddress: preferredTokenAddress, + destinationToAddress: ownerAddress, + tradeType: TradeType.EXACT_INPUT, +}) +``` + +This keeps position exits and post-withdrawal routing separate, which is easier to reason about and easier to support. + +## Positions and earnings backend design + +Once deposits are live, your backend needs to answer product questions such as: + +- how much has the user deposited +- what is their current position value +- what is the current APY +- how much has the user earned + +The cleanest architecture is to split this into two layers. + +### 1. Execution history + +Use Trails for transaction lifecycle data: + +- `GetIntent` +- `GetIntentReceipt` +- `WaitIntentReceipt` +- `GetIntentHistory` + +This tells you: + +- which deposits were initiated +- whether they completed +- which chains and tokens were involved +- the final execution receipts + +### 2. Live position state + +Use protocol reads or protocol data services for current position state: + +- ERC-4626 share balances and conversion helpers +- Aave aToken balances and reserve data +- protocol APIs or subgraphs where available + +This tells you: + +- current underlying balance +- current share balance +- realized and unrealized earnings +- current APY or rate inputs + +If you want a minimal production model, store the following per user position: + +- `protocol` +- `vaultAddress` +- `depositAsset` +- `shareToken` +- `destinationChainId` +- `latestPrincipal` +- `latestPositionValue` +- `latestApy` +- `lastSyncedAt` + +Then build a sync job that: + +1. reads completed Trails intents for the wallet +2. groups them by supported vault +3. fetches live protocol state +4. computes current value and earnings +5. caches the result for your app + +You can also use Trails token pricing endpoints when you need USD-normalized portfolio summaries. + +## APY and earnings + +Trails gives you the routing and execution layer. For a white-labeled app, you should plan to own the APY and earnings layer yourself. + +Typical sources are: + +- protocol contract reads +- protocol APIs or subgraphs +- your own indexer or cache + +That gives you full control over: + +- current APY display +- earnings summaries +- in-app badges like "earning" +- which opportunities are available in your product + +## Next steps + +- [Configure Embedded Wallet in Builder](/solutions/builder/embedded-wallet/configuration) +- [Learn Embedded Wallet authentication](/sdk/headless-wallet/authentication) +- [Send transactions with Embedded Wallet](/sdk/headless-wallet/use-wallets#send-transactions) +- [Learn more about Trails](/solutions/payments/trails) +- [Read the full Trails API reference](https://docs.trails.build/api-reference/introduction) diff --git a/guides/guide-cards.json b/guides/guide-cards.json index 37112e7a..3e781e7c 100644 --- a/guides/guide-cards.json +++ b/guides/guide-cards.json @@ -1,5 +1,5 @@ { - "lastUpdated": "2026-01-05T15:02:56.281Z", + "lastUpdated": "2026-04-01T11:39:13.963Z", "totalCards": 7, "sections": [ { diff --git a/guides/guide-overview.mdx b/guides/guide-overview.mdx index 92412695..af3d201e 100644 --- a/guides/guide-overview.mdx +++ b/guides/guide-overview.mdx @@ -14,18 +14,26 @@ sidebarTitle: Overview Leveraging Sequence's Transaction API and a serverless environment, you will build a scalable minting service for NFT mints or any other transactions that automatically handles blockchain complexities like reorgs, nonce management, and transaction parallelization. - - Learn how to connect Privy with Sequence, enabling your users to sign in and interact with your app through a Sequence Smart Wallet. This involves creating a Sequence wallet controlled by a user's Privy-managed EOA, and then using that Sequence wallet to send gasless transactions. - - - + Learn how to connect Privy with Sequence, enabling your users to sign in and interact with your app through a Sequence Smart Wallet. This involves creating a Sequence wallet controlled by a user's Privy-managed EOA, and then using that Sequence wallet to send gasless transactions. + + + + Build a white-labeled yield flow that uses Sequence Embedded Wallet for authentication and signing, and Trails intents to route user funds into a target vault. + + + Build an API-driven marketplace where players can mint, then sell or buy items using a custom web-based interface leveraging Sequence Orderbook APIs.