@goplausible/openclaw-algorand-plugin 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/lib/x402-fetch.ts +3 -10
  2. package/openclaw.plugin.json +4 -2
  3. package/package.json +1 -1
  4. package/skills/algorand-interaction/SKILL.md +12 -6
  5. package/skills/algorand-interaction/references/algorand-mcp.md +81 -7
  6. package/skills/algorand-interaction/references/examples-algorand-mcp.md +77 -2
  7. package/skills/haystack-router-development/SKILL.md +85 -0
  8. package/skills/haystack-router-development/references/api-reference.md +381 -0
  9. package/skills/haystack-router-development/references/configuration.md +184 -0
  10. package/skills/haystack-router-development/references/fees-and-referrals.md +91 -0
  11. package/skills/haystack-router-development/references/getting-started.md +93 -0
  12. package/skills/haystack-router-development/references/migration.md +53 -0
  13. package/skills/haystack-router-development/references/node-automation.md +113 -0
  14. package/skills/haystack-router-development/references/quotes.md +155 -0
  15. package/skills/haystack-router-development/references/react-integration.md +260 -0
  16. package/skills/haystack-router-development/references/swaps.md +161 -0
  17. package/skills/haystack-router-interaction/SKILL.md +146 -0
  18. package/skills/haystack-router-interaction/references/configuration.md +53 -0
  19. package/skills/haystack-router-interaction/references/getting-started.md +48 -0
  20. package/skills/haystack-router-interaction/references/node-automation.md +51 -0
  21. package/skills/haystack-router-interaction/references/quotes.md +80 -0
  22. package/skills/haystack-router-interaction/references/swaps.md +84 -0
@@ -0,0 +1,93 @@
1
+ # Getting Started
2
+
3
+ ## Prerequisites
4
+
5
+ - **Node.js** >= 20
6
+ - **algosdk** 3.x (peer dependency)
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @txnlab/haystack-router algosdk
12
+ ```
13
+
14
+ ## RouterClient Initialization
15
+
16
+ ```typescript
17
+ import { RouterClient } from '@txnlab/haystack-router'
18
+
19
+ const router = new RouterClient({
20
+ apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
21
+ })
22
+ ```
23
+
24
+ ### With Options
25
+
26
+ ```typescript
27
+ const router = new RouterClient({
28
+ apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
29
+ autoOptIn: true, // Auto-detect asset opt-in needs
30
+ referrerAddress: 'ABC...', // Earn 25% of swap fees
31
+ feeBps: 15, // Fee in basis points (default: 10)
32
+ debugLevel: 'info', // 'none' | 'info' | 'debug' | 'trace'
33
+ })
34
+ ```
35
+
36
+ ### TestNet
37
+
38
+ Override the algod connection and API base URL for TestNet:
39
+
40
+ ```typescript
41
+ const router = new RouterClient({
42
+ apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
43
+ algodUri: 'https://testnet-api.4160.nodely.dev/',
44
+ // Set apiBaseUrl if using a TestNet-specific API endpoint
45
+ })
46
+ ```
47
+
48
+ ## Quick Example
49
+
50
+ ```typescript
51
+ import { RouterClient } from '@txnlab/haystack-router'
52
+
53
+ const router = new RouterClient({
54
+ apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
55
+ })
56
+
57
+ // Get a quote: swap 1 ALGO → USDC
58
+ const quote = await router.newQuote({
59
+ fromASAID: 0, // ALGO
60
+ toASAID: 31566704, // USDC
61
+ amount: 1_000_000, // 1 ALGO in microAlgos
62
+ address: activeAddress,
63
+ })
64
+
65
+ console.log(`Expected output: ${quote.quote} microUSDC`)
66
+ console.log(`USD value: $${quote.usdOut}`)
67
+
68
+ // Execute the swap (use-wallet signer for browser, custom signer for Node.js)
69
+ const swap = await router.newSwap({
70
+ quote,
71
+ address: activeAddress,
72
+ signer: transactionSigner,
73
+ slippage: 1, // 1% slippage tolerance
74
+ })
75
+
76
+ const result = await swap.execute()
77
+ console.log(`Confirmed in round ${result.confirmedRound}`)
78
+ ```
79
+
80
+ ## Amounts and Units
81
+
82
+ All amounts are in **base units** (smallest denomination):
83
+
84
+ | Asset | Decimals | 1 unit in base | Example |
85
+ | ------------------- | -------- | -------------- | -------------------- |
86
+ | ALGO (ASA 0) | 6 | 1,000,000 | `1_000_000` = 1 ALGO |
87
+ | USDC (ASA 31566704) | 6 | 1,000,000 | `5_000_000` = 5 USDC |
88
+
89
+ Convert human-readable amounts:
90
+
91
+ ```typescript
92
+ const amount = BigInt(Math.floor(parseFloat(userInput) * 10 ** decimals))
93
+ ```
@@ -0,0 +1,53 @@
1
+ # Migration from @txnlab/deflex
2
+
3
+ ## Package Rename
4
+
5
+ ```bash
6
+ npm uninstall @txnlab/deflex
7
+ npm install @txnlab/haystack-router
8
+ ```
9
+
10
+ ## Import Changes
11
+
12
+ ```typescript
13
+ // Before
14
+ import { DeflexClient } from '@txnlab/deflex'
15
+
16
+ // After
17
+ import { RouterClient } from '@txnlab/haystack-router'
18
+ ```
19
+
20
+ ## Class and Type Renames
21
+
22
+ | Before (@txnlab/deflex) | After (@txnlab/haystack-router) |
23
+ | ----------------------- | ------------------------------- |
24
+ | `DeflexClient` | `RouterClient` |
25
+ | `DeflexQuote` | `SwapQuote` |
26
+ | `DeflexTransaction` | `SwapTransaction` |
27
+ | `DeflexConfig` | `Config` |
28
+ | `DeflexConfigParams` | `ConfigParams` |
29
+ | `DeflexSignature` | `Signature` |
30
+
31
+ ## API Endpoint
32
+
33
+ The SDK automatically uses the updated API endpoint. No manual URL changes needed.
34
+
35
+ ## Functionality
36
+
37
+ All functionality is identical — only naming changed. A find-and-replace of the class and type names is sufficient.
38
+
39
+ ### Quick Migration
40
+
41
+ ```bash
42
+ # Find all files that import from @txnlab/deflex
43
+ grep -r "@txnlab/deflex" --include="*.ts" --include="*.tsx" -l
44
+
45
+ # Replace imports
46
+ # @txnlab/deflex → @txnlab/haystack-router
47
+ # DeflexClient → RouterClient
48
+ # DeflexQuote → SwapQuote
49
+ # DeflexTransaction → SwapTransaction
50
+ # DeflexConfig → Config
51
+ # DeflexConfigParams → ConfigParams
52
+ # DeflexSignature → Signature
53
+ ```
@@ -0,0 +1,113 @@
1
+ # Node.js Automation (SDK)
2
+
3
+ When building Node.js automation scripts that integrate Haystack Router directly in application code:
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install @txnlab/haystack-router algosdk
9
+ ```
10
+
11
+ ## Environment Variables
12
+
13
+ ```bash
14
+ # .env
15
+ HAYSTACK_API_KEY=1b72df7e-1131-4449-8ce1-29b79dd3f51e # Free tier (60 requests/min)
16
+ ```
17
+
18
+ ## Complete Example
19
+
20
+ ```typescript
21
+ import algosdk from 'algosdk'
22
+ import { RouterClient } from '@txnlab/haystack-router'
23
+
24
+ async function main() {
25
+ const apiKey = process.env.HAYSTACK_API_KEY!
26
+
27
+ // For application code, use use-wallet or AlgorandClient for signing
28
+ // For Node.js scripts, create a custom signer:
29
+ const account = algosdk.mnemonicToSecretKey(process.env.MNEMONIC!)
30
+ const address = account.addr.toString()
31
+
32
+ const signer = async (
33
+ txnGroup: algosdk.Transaction[],
34
+ indexesToSign: number[],
35
+ ): Promise<Uint8Array[]> => {
36
+ return indexesToSign.map(
37
+ (index) => algosdk.signTransaction(txnGroup[index], account.sk).blob,
38
+ )
39
+ }
40
+
41
+ // Initialize router
42
+ const router = new RouterClient({
43
+ apiKey,
44
+ autoOptIn: true,
45
+ })
46
+
47
+ // Get quote: 1 ALGO → USDC
48
+ const quote = await router.newQuote({
49
+ fromASAID: 0,
50
+ toASAID: 31566704,
51
+ amount: 1_000_000,
52
+ address,
53
+ })
54
+
55
+ console.log(`Expected output: ${Number(quote.quote) / 1e6} USDC`)
56
+ console.log(`USD value: $${quote.usdOut.toFixed(2)}`)
57
+
58
+ // Execute swap
59
+ const swap = await router.newSwap({
60
+ quote,
61
+ address,
62
+ signer,
63
+ slippage: 1,
64
+ })
65
+
66
+ const result = await swap.execute()
67
+ console.log(`Confirmed in round ${result.confirmedRound}`)
68
+ console.log(`Transaction IDs: ${result.txIds.join(', ')}`)
69
+
70
+ const summary = swap.getSummary()
71
+ if (summary) {
72
+ console.log(`Input: ${summary.inputAmount} microunits`)
73
+ console.log(`Output: ${summary.outputAmount} microunits`)
74
+ console.log(`Fees: ${summary.totalFees} microAlgos`)
75
+ }
76
+ }
77
+
78
+ main().catch(console.error)
79
+ ```
80
+
81
+ ## Tracking with Notes
82
+
83
+ Attach identifiers to transactions for backend tracking:
84
+
85
+ ```typescript
86
+ const swap = await router.newSwap({
87
+ quote,
88
+ address,
89
+ signer,
90
+ slippage: 1,
91
+ note: new TextEncoder().encode(
92
+ JSON.stringify({
93
+ orderId: 'order-123',
94
+ timestamp: Date.now(),
95
+ }),
96
+ ),
97
+ })
98
+
99
+ await swap.execute()
100
+ const txId = swap.getInputTransactionId()
101
+ console.log(`Tracked: order-123 → ${txId}`)
102
+ ```
103
+
104
+ ## Debug Logging
105
+
106
+ Enable verbose logging for troubleshooting:
107
+
108
+ ```typescript
109
+ const router = new RouterClient({
110
+ apiKey,
111
+ debugLevel: 'debug', // 'none' | 'info' | 'debug' | 'trace'
112
+ })
113
+ ```
@@ -0,0 +1,155 @@
1
+ # Quotes
2
+
3
+ ## Getting a Quote
4
+
5
+ ```typescript
6
+ import { RouterClient } from '@txnlab/haystack-router'
7
+
8
+ const router = new RouterClient({
9
+ apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
10
+ })
11
+
12
+ const quote = await router.newQuote({
13
+ fromASAID: 0, // ALGO
14
+ toASAID: 31566704, // USDC
15
+ amount: 1_000_000, // 1 ALGO in base units
16
+ address: activeAddress,
17
+ })
18
+ ```
19
+
20
+ ## Parameters
21
+
22
+ | Parameter | Type | Required | Description |
23
+ | ------------------- | ------------------ | -------- | ----------------------------------------------- |
24
+ | `fromASAID` | `number \| bigint` | Yes | Input asset ID (0 = ALGO) |
25
+ | `toASAID` | `number \| bigint` | Yes | Output asset ID |
26
+ | `amount` | `number \| bigint` | Yes | Amount in base units |
27
+ | `type` | `string` | No | `'fixed-input'` (default) or `'fixed-output'` |
28
+ | `address` | `string` | No | User address (needed for auto opt-in detection) |
29
+ | `maxGroupSize` | `number` | No | Max transactions in group (default: 16) |
30
+ | `maxDepth` | `number` | No | Max routing hops (default: 4) |
31
+ | `optIn` | `boolean` | No | Include opt-in transaction for output asset |
32
+ | `disabledProtocols` | `Protocol[]` | No | Protocols to exclude from routing |
33
+
34
+ ## Quote Response (SwapQuote)
35
+
36
+ `newQuote()` returns a `SwapQuote` (extends `FetchQuoteResponse`):
37
+
38
+ ```typescript
39
+ quote.quote // bigint — expected output amount in base units
40
+ quote.amount // bigint — original input amount
41
+ quote.usdIn // number — USD value of input
42
+ quote.usdOut // number — USD value of output
43
+ quote.userPriceImpact // number | undefined — price impact %
44
+ quote.marketPriceImpact // number | undefined — market price impact %
45
+ quote.route // Route[] — routing path details
46
+ quote.flattenedRoute // Record<string, number> — protocol split percentages
47
+ quote.quotes // DexQuote[] — individual DEX quotes
48
+ quote.requiredAppOptIns // number[] — app IDs needing opt-in
49
+ quote.createdAt // number — timestamp (ms)
50
+ quote.address // string | undefined — user address if provided
51
+ ```
52
+
53
+ ## Quote Types
54
+
55
+ **Fixed-input** (default): Specify exact input amount, receive variable output.
56
+
57
+ ```typescript
58
+ const quote = await router.newQuote({
59
+ fromASAID: 0,
60
+ toASAID: 31566704,
61
+ amount: 1_000_000, // Exact: send 1 ALGO
62
+ type: 'fixed-input',
63
+ })
64
+ // quote.quote = expected USDC received
65
+ ```
66
+
67
+ **Fixed-output**: Specify desired output, send variable input.
68
+
69
+ ```typescript
70
+ const quote = await router.newQuote({
71
+ fromASAID: 0,
72
+ toASAID: 31566704,
73
+ amount: 1_000_000, // Exact: receive 1 USDC
74
+ type: 'fixed-output',
75
+ })
76
+ // quote.quote = ALGO required to send
77
+ ```
78
+
79
+ ## Displaying Quote Data
80
+
81
+ ```typescript
82
+ const fromDecimals = 6 // ALGO
83
+ const toDecimals = 6 // USDC
84
+
85
+ const outputHuman = Number(quote.quote) / 10 ** toDecimals
86
+ const inputHuman = Number(quote.amount) / 10 ** fromDecimals
87
+ const rate = outputHuman / inputHuman
88
+
89
+ console.log(`${inputHuman} ALGO → ${outputHuman} USDC`)
90
+ console.log(`Rate: 1 ALGO = ${rate.toFixed(4)} USDC`)
91
+ console.log(`USD in: $${quote.usdIn.toFixed(2)}`)
92
+ console.log(`USD out: $${quote.usdOut.toFixed(2)}`)
93
+
94
+ if (quote.userPriceImpact !== undefined) {
95
+ console.log(`Price impact: ${quote.userPriceImpact.toFixed(2)}%`)
96
+ }
97
+ ```
98
+
99
+ ## Route Details
100
+
101
+ Each quote includes routing information showing how the swap is split:
102
+
103
+ ```typescript
104
+ // Flattened view: protocol → percentage
105
+ for (const [protocol, pct] of Object.entries(quote.flattenedRoute)) {
106
+ console.log(`${protocol}: ${pct}%`)
107
+ }
108
+ // e.g., "TinymanV2: 60%", "Pact: 40%"
109
+
110
+ // Detailed route with hops
111
+ for (const route of quote.route) {
112
+ console.log(`${route.percentage}% of swap:`)
113
+ for (const hop of route.path) {
114
+ console.log(` ${hop.in.unit_name} → ${hop.out.unit_name} via ${hop.name}`)
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Asset Opt-In Detection
120
+
121
+ ```typescript
122
+ // Option 1: Set autoOptIn on the client
123
+ const router = new RouterClient({
124
+ apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
125
+ autoOptIn: true,
126
+ })
127
+ const quote = await router.newQuote({
128
+ fromASAID: 0,
129
+ toASAID: 31566704,
130
+ amount: 1_000_000,
131
+ address: activeAddress, // Required for auto opt-in
132
+ })
133
+
134
+ // Option 2: Check manually and pass optIn flag
135
+ const needsOptIn = await router.needsAssetOptIn(activeAddress, 31566704)
136
+ const quote = await router.newQuote({
137
+ fromASAID: 0,
138
+ toASAID: 31566704,
139
+ amount: 1_000_000,
140
+ optIn: needsOptIn,
141
+ })
142
+ ```
143
+
144
+ ## Lower-Level: fetchQuote()
145
+
146
+ `fetchQuote()` returns the raw `FetchQuoteResponse` without `SwapQuote` enhancements (no bigint coercion, no `createdAt`). Use `newQuote()` unless you need the raw response.
147
+
148
+ ```typescript
149
+ const raw = await router.fetchQuote({
150
+ fromASAID: 0,
151
+ toASAID: 31566704,
152
+ amount: 1_000_000,
153
+ })
154
+ // raw.quote is string | number (not bigint)
155
+ ```
@@ -0,0 +1,260 @@
1
+ # React Integration
2
+
3
+ ## Setup
4
+
5
+ Install dependencies:
6
+
7
+ ```bash
8
+ npm install @txnlab/haystack-router @txnlab/use-wallet-react algosdk
9
+ ```
10
+
11
+ Wrap your app with `WalletProvider`:
12
+
13
+ ```tsx
14
+ import { WalletProvider, WalletId } from '@txnlab/use-wallet-react'
15
+
16
+ const walletProviders = [
17
+ { id: WalletId.PERA },
18
+ { id: WalletId.DEFLY },
19
+ { id: WalletId.LUTE },
20
+ ]
21
+
22
+ function App() {
23
+ return (
24
+ <WalletProvider wallets={walletProviders}>
25
+ <SwapInterface />
26
+ </WalletProvider>
27
+ )
28
+ }
29
+ ```
30
+
31
+ ## Basic Swap Component
32
+
33
+ ```tsx
34
+ import { useState } from 'react'
35
+ import { useWallet } from '@txnlab/use-wallet-react'
36
+ import { RouterClient, type SwapQuote } from '@txnlab/haystack-router'
37
+
38
+ function SwapInterface() {
39
+ const { activeAddress, transactionSigner } = useWallet()
40
+ const [amount, setAmount] = useState('')
41
+ const [fromAsset, setFromAsset] = useState(0) // ALGO
42
+ const [toAsset, setToAsset] = useState(31566704) // USDC
43
+ const [slippage, setSlippage] = useState('1')
44
+ const [quote, setQuote] = useState<SwapQuote | null>(null)
45
+
46
+ const getQuote = async () => {
47
+ const router = new RouterClient({
48
+ apiKey: import.meta.env.VITE_HAYSTACK_API_KEY,
49
+ autoOptIn: true,
50
+ })
51
+
52
+ const amountInBaseUnits = BigInt(Math.floor(parseFloat(amount) * 1_000_000))
53
+
54
+ const result = await router.newQuote({
55
+ fromASAID: fromAsset,
56
+ toASAID: toAsset,
57
+ amount: amountInBaseUnits,
58
+ address: activeAddress!,
59
+ })
60
+
61
+ setQuote(result)
62
+ }
63
+
64
+ const executeSwap = async () => {
65
+ if (!quote || !activeAddress) return
66
+
67
+ const router = new RouterClient({
68
+ apiKey: import.meta.env.VITE_HAYSTACK_API_KEY,
69
+ autoOptIn: true,
70
+ })
71
+
72
+ const swap = await router.newSwap({
73
+ quote,
74
+ address: activeAddress,
75
+ signer: transactionSigner,
76
+ slippage: parseFloat(slippage),
77
+ })
78
+
79
+ const result = await swap.execute()
80
+ console.log(`Confirmed in round ${result.confirmedRound}`)
81
+
82
+ setQuote(null) // Clear stale quote
83
+ }
84
+
85
+ return (
86
+ <div>
87
+ <input
88
+ type="number"
89
+ value={amount}
90
+ onChange={(e) => setAmount(e.target.value)}
91
+ placeholder="Amount"
92
+ />
93
+ <button onClick={getQuote} disabled={!activeAddress || !amount}>
94
+ Get Quote
95
+ </button>
96
+
97
+ {quote && (
98
+ <div>
99
+ <p>Output: {(Number(quote.quote) / 1e6).toFixed(4)} USDC</p>
100
+ <p>USD value: ${quote.usdOut.toFixed(2)}</p>
101
+ <p>
102
+ Route:{' '}
103
+ {Object.entries(quote.flattenedRoute)
104
+ .map(([protocol, pct]) => `${protocol}: ${pct}%`)
105
+ .join(', ')}
106
+ </p>
107
+ <button onClick={executeSwap}>Execute Swap</button>
108
+ </div>
109
+ )}
110
+ </div>
111
+ )
112
+ }
113
+ ```
114
+
115
+ ## TanStack Query Integration
116
+
117
+ For production UIs, use TanStack Query for auto-refreshing quotes and better state management.
118
+
119
+ ```bash
120
+ npm install @tanstack/react-query
121
+ ```
122
+
123
+ ```tsx
124
+ import { useState, useEffect, useMemo } from 'react'
125
+ import {
126
+ useQuery,
127
+ useMutation,
128
+ QueryClient,
129
+ QueryClientProvider,
130
+ } from '@tanstack/react-query'
131
+ import { useWallet } from '@txnlab/use-wallet-react'
132
+ import { RouterClient, type SwapQuote } from '@txnlab/haystack-router'
133
+
134
+ const queryClient = new QueryClient()
135
+
136
+ function SwapWithAutoRefresh() {
137
+ const { activeAddress, transactionSigner } = useWallet()
138
+ const [amount, setAmount] = useState('')
139
+ const [debouncedAmount, setDebouncedAmount] = useState('')
140
+ const [fromAsset] = useState(0)
141
+ const [toAsset] = useState(31566704)
142
+ const [slippage] = useState(1)
143
+
144
+ // Debounce amount input (500ms)
145
+ useEffect(() => {
146
+ const timer = setTimeout(() => setDebouncedAmount(amount), 500)
147
+ return () => clearTimeout(timer)
148
+ }, [amount])
149
+
150
+ const router = useMemo(
151
+ () =>
152
+ new RouterClient({
153
+ apiKey: import.meta.env.VITE_HAYSTACK_API_KEY,
154
+ autoOptIn: true,
155
+ }),
156
+ [],
157
+ )
158
+
159
+ const isValidRequest =
160
+ activeAddress && debouncedAmount && parseFloat(debouncedAmount) > 0
161
+
162
+ // Auto-fetch and refresh quotes
163
+ const {
164
+ data: quote,
165
+ error,
166
+ isLoading,
167
+ } = useQuery({
168
+ queryKey: ['quote', fromAsset, toAsset, debouncedAmount, activeAddress],
169
+ queryFn: () =>
170
+ router.newQuote({
171
+ fromASAID: fromAsset,
172
+ toASAID: toAsset,
173
+ amount: BigInt(Math.floor(parseFloat(debouncedAmount) * 1_000_000)),
174
+ address: activeAddress!,
175
+ }),
176
+ enabled: !!isValidRequest,
177
+ refetchInterval: 15_000, // Refresh every 15 seconds
178
+ retry: 1,
179
+ })
180
+
181
+ // Swap mutation
182
+ const swapMutation = useMutation({
183
+ mutationFn: async () => {
184
+ if (!quote || !activeAddress) throw new Error('Missing quote or address')
185
+
186
+ const swap = await router.newSwap({
187
+ quote,
188
+ address: activeAddress,
189
+ signer: transactionSigner,
190
+ slippage,
191
+ })
192
+ return swap.execute()
193
+ },
194
+ onSuccess: (result) => {
195
+ console.log(`Confirmed in round ${result.confirmedRound}`)
196
+ queryClient.invalidateQueries({ queryKey: ['quote'] })
197
+ },
198
+ })
199
+
200
+ return (
201
+ <div>
202
+ <input
203
+ type="number"
204
+ value={amount}
205
+ onChange={(e) => setAmount(e.target.value)}
206
+ placeholder="Amount"
207
+ />
208
+
209
+ {isLoading && <p>Fetching quote...</p>}
210
+ {error && <p>Error: {(error as Error).message}</p>}
211
+
212
+ {quote && (
213
+ <div>
214
+ <p>Output: {(Number(quote.quote) / 1e6).toFixed(4)} USDC</p>
215
+ <p>USD: ${quote.usdOut.toFixed(2)}</p>
216
+ <button
217
+ onClick={() => swapMutation.mutate()}
218
+ disabled={swapMutation.isPending}
219
+ >
220
+ {swapMutation.isPending ? 'Swapping...' : 'Swap'}
221
+ </button>
222
+ </div>
223
+ )}
224
+ </div>
225
+ )
226
+ }
227
+ ```
228
+
229
+ Key patterns:
230
+
231
+ - **Debounced input**: Prevent quote requests on every keystroke
232
+ - **Auto-refresh**: `refetchInterval: 15_000` keeps quotes current
233
+ - **Invalidation**: Clear stale quotes after a successful swap
234
+ - **Loading/error states**: TanStack Query manages all async state
235
+
236
+ ## Displaying Route Details
237
+
238
+ ```tsx
239
+ function RouteDisplay({ quote }: { quote: SwapQuote }) {
240
+ return (
241
+ <div>
242
+ <h4>Route</h4>
243
+ {quote.route.map((route, i) => (
244
+ <div key={i}>
245
+ <strong>{route.percentage}%</strong>
246
+ {route.path.map((hop, j) => (
247
+ <span key={j}>
248
+ {j > 0 && ' → '}
249
+ {hop.in.unit_name} → {hop.out.unit_name} ({hop.name})
250
+ </span>
251
+ ))}
252
+ </div>
253
+ ))}
254
+ {quote.userPriceImpact !== undefined && (
255
+ <p>Price impact: {quote.userPriceImpact.toFixed(2)}%</p>
256
+ )}
257
+ </div>
258
+ )
259
+ }
260
+ ```