@epoch-protocol/epoch-flows-sdk
v0.1.7
Published
Headless SDK for Epoch Protocol intent flows (Pay, Swap, Earn). Framework-agnostic core extracted from epoch-intent-widget.
Readme
@epoch-protocol/epoch-flows-sdk
Headless SDK for Epoch Protocol intent flows — Pay, Swap, and Earn (deposit + withdraw). Framework-agnostic core extracted from @epoch-protocol/epoch-intent-widget.
Use this package when you want Epoch's business logic — intent building, market/position fetching, and the full quote → submit → poll-until-settled orchestration — without the React UI. The widget itself is just a UI layer over this SDK; everything the widget does, you can do here with your own components (or no components at all).
your UI ─────────────► epoch-flows-sdk ─────────────► Epoch allocator + solver network
(React, Vue, Svelte, (builders, fetchers, (smallocator, 1delta, SIO)
vanilla, a CLI, …) sessions/state machines)Contents
- Install
- Configure
- Two ways to use it
- Pure builders
- Async fetchers (Earn data)
- Sessions: the quote/submit/poll state machine
- Chain helpers & registries
- Using it from React
- Full export map
- Concepts & gotchas
Install
pnpm add @epoch-protocol/epoch-flows-sdk viemviem is a peer dependency (^2). You supply a viem WalletClient to the sessions for signing.
Configure
Everything hangs off one EpochFlowsSDK instance:
import { EpochFlowsSDK } from "@epoch-protocol/epoch-flows-sdk";
const sdk = new EpochFlowsSDK({
apiBaseUrl: "https://allocator.example.com", // Epoch allocator (smallocator). Required.
positionsBaseUrl: "https://positions.example.com", // 1delta pools/positions proxy. Earn only.
rpcUrls: { 8453: "https://base-mainnet.my-node.com" }, // per-chain RPC overrides. Optional.
});interface EpochFlowsSDKConfig {
apiBaseUrl: string; // all quote/solve/status calls go here
positionsBaseUrl?: string; // /pools + positions; fetchers degrade to mocks without it
rpcUrls?: Record<number, string>; // override built-in public RPCs for on-chain reads
}All exports are also available as free functions (see Full export map) if you'd rather not hold an instance.
Two ways to use it
The SDK splits cleanly into pure pieces (no network, no wallet — easy to unit test) and stateful pieces:
| Kind | What | Examples |
| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Pure builders | Turn inputs into an IntentProps / validation result | buildPayIntentFromFlatProps, earn.buildDepositIntent |
| Pure adapters/helpers | Reshape 1delta data | flattenConfigsToMarkets, toEpochEarnMarket, estimatedAnnualYield |
| Async fetchers | Hit your positionsBaseUrl | earn.fetchLendingPools, earn.fetchUserPositions |
| Sessions | Full quote → sign → submit → poll state machine, event-driven | createPaySession, createEarnSession |
Pure builders
No wallet or network needed — pass data in, get an intent (or an error) out.
// Flat pay: validates inputs, looks up token metadata, returns a ready IntentProps
const built = sdk.pay.buildIntent({
toAddress: "0x4235…89a9",
toAmount: "0.15", // human/decimal string
toChainId: 8453,
toToken: "0x8335…2913",
});
if (built.ok) {
built.intent; // IntentProps — feed into a PaySession
} else {
built.error; // human-readable reason
}
// Earn deposit/withdraw: validates the amount against the market/position
const deposit = sdk.earn.buildDepositIntent(market, "100");
const withdraw = sdk.earn.buildWithdrawIntent(position, "50");
// → { ok: true, intent, title, submitButtonText } | { ok: false, error }
// APR-only yield preview (not a guarantee)
const perYear = sdk.earn.estimatedAnnualYield(100, 0.0525); // 5.25% APR → 5.25Async fetchers (Earn data)
These call your positionsBaseUrl. Without one configured, they degrade to bundled mock data so flows still render.
// Lending pools — fans out one /pools request per chain, in parallel.
const { configs } = await sdk.earn.fetchLendingPools({
chainIds: [1, 8453, 42161],
lender: "AAVE_V3,MORPHO", // CSV of 1delta lender keys
sortBy: "totalDepositsUsd", // depositRate | variableBorrowRate | totalDepositsUsd | totalLiquidityUsd | utilization
sortDir: "DESC",
count: 100,
});
// Turn configs into flat markets for a picker
const markets = sdk.earn.flattenConfigsToMarkets(configs);
// A user's open positions (for the withdraw side)
const { positions, summary } = await sdk.earn.fetchUserPositions({
address: "0x…",
network: "mainnet",
configs,
});Each fetcher accepts an AbortSignal (signal) and a per-call positionsBaseUrl override. Bundled fallbacks: MOCK_MAINNET_MARKETS, MOCK_TESTNET_MARKETS, mockPositionsForAddress, and HARDCODED_ONEDELTA_CONFIGS.
Sessions: the quote/submit/poll state machine
Sessions are where the real work happens. Each one is a TypedEventEmitter — you subscribe with .on(event, fn) (which returns an unsubscribe function) and drive it with quote() / submit().
Wallet client is type-erased to
unknown. Pass a viemWalletClient. The alias avoids structural-type mismatches in monorepos that have multiple viem copies installed; the session casts internally.
PaySession (pay/swap)
const session = sdk.createPaySession({
walletClient, // viem WalletClient
address: "0xUser…",
sessionId: crypto.randomUUID(),
mode: "pay", // 'pay' | 'swap'
requiredToken: { address: "0x8335…2913", symbol: "USDC", decimals: 6 },
requiredAmount: 5_000_000n, // atomic units
isTestnet: false,
intentConfig: {
protocol: "transfer", // 'transfer'|'swap'|'bridge' → simple route; else protocol interaction
action: "pay",
fixedOutput: true,
destinationChainId: 8453,
slippageBps: 100, // 1% output slippage; 0 = strict
},
});
// Subscribe
const unsub = session.on("statusChange", (s) => console.log("status", s));
session.on("quote", (q) => console.log("pay-side quote", q)); // human + raw input amount
session.on("intentSent", ({ nonce }) => {});
session.on("success", ({ nonce, status }) => {});
session.on("error", (msg) => {});
session.on("requestClose", () => {}); // fires ~2s after settle
// (optional) preview the input cost for fixedOutput intents
await session.fetchQuote({ sourceChainId: 42161, sourceToken });
// Submit → signs, solves, then polls until settled
await session.submit({ sourceChainId: 42161, sourceToken });
// Cleanup
unsub(); // remove one listener
session.dispose(); // clear timers + all listenersPaySession status: idle → submitting → sent → polling → complete (or error).
EarnSession (deposit/withdraw)
const session = sdk.createEarnSession({
walletClient,
address: "0xUser…",
sessionId: crypto.randomUUID(),
});
session.on("statusChange", (s) => {}); // idle → quoting → submitting → sent → polling → complete | error
session.on("quote", (q) => {}); // EarnQuote: tokenIn/out, executionTransactions, resourceLockRequired
session.on("quoteError", (m) => {});
session.on("progress", (p) => {}); // 0–100
session.on("pollTick", () => {}); // each settle poll with no completion yet
session.on("success", ({ nonce, status }) => {});
const input = {
tab: "deposit", // 'deposit' | 'withdraw'
amount: "100", // human string
market, // EpochEarnMarket (must carry oneDeltaMarketUid)
sourceChainId: 8453,
sourceToken, // EpochToken
network: "mainnet",
};
const quote = await session.quote(input); // optional preview
await session.submit({ ...input, quote }); // reuses the quote if presentWithdraw extras on the input: isAll (protocol max-withdraw), smartWithdraw + smartDestChainId + smartDestTokenAddress (cross-chain delivery).
Event reference
Both sessions share a common spine. Each .on() returns an unsubscribe function.
| Event | Payload | Pay | Earn | Meaning |
| ---------------- | ---------------------------- | :-: | :--: | ------------------------------------------ |
| statusChange | status string | ✅ | ✅ | Lifecycle transition |
| progress | number (0–100) | ✅ | ✅ | Progress bar value |
| activeStep | number | ✅ | ✅ | Stepper index |
| quote | {raw, human} / EarnQuote | ✅ | ✅ | Quote settled |
| quoteError | string | ✅ | ✅ | Quote failed |
| isQuoting | boolean | ✅ | — | Quote in flight (pay) |
| nonce | string \| null | ✅ | ✅ | Intent nonce |
| error | string \| null | ✅ | ✅ | Error message |
| start | OnStartCtx | ✅ | ✅ | Submit began |
| sign | OnSignCtx | ✅ | ✅ | Signature requested |
| intentSent | {nonce} | ✅ | ✅ | Submitted, pre-settle |
| intentComplete | {nonce, status} | ✅ | ✅ | Settle finished |
| success | OnSuccessCtx | ✅ | ✅ | Settled OK |
| errorCtx | OnErrorCtx | ✅ | ✅ | Error with session context |
| pollTick | void | — | ✅ | Settle poll, not complete yet |
| requestClose | void | ✅ | ✅ | Fires shortly after settle (close your UI) |
Lifecycle methods on both: getStatus(), reset() (back to idle), dispose() (clear timers + listeners). Sessions poll the allocator every 3s after submit until a status of completed / finalized / success appears.
Chain helpers & registries
import {
EPOCH_SUPPORTED_CHAINS,
EPOCH_TESTNET_CHAINS,
EPOCH_SUPPORTED_TOKENS,
EPOCH_TESTNET_TOKENS,
getEpochChainById,
getChainName,
getEpochTokensByChainEnv,
getEpochTokensBySymbol,
} from "@epoch-protocol/epoch-flows-sdk";
// On-chain reads via the SDK instance (uses config.rpcUrls, else built-in public RPCs)
const client = sdk.chain.getPublicClient(8453);
const balance = await sdk.chain.fetchTokenBalance(
8453,
tokenAddress,
userAddress,
);
const rpc = sdk.chain.resolveRpcUrl(8453);Using it from React
The SDK is framework-agnostic, but the bridge is small: create the session, mirror events into state, dispose on unmount.
import { useEffect, useRef, useState } from "react";
import {
EpochFlowsSDK,
type PayIntentFlowStatus,
} from "@epoch-protocol/epoch-flows-sdk";
import { useWalletClient, useAccount } from "wagmi";
function usePayFlow(sdk: EpochFlowsSDK, intentArgs) {
const { address } = useAccount();
const { data: walletClient } = useWalletClient();
const [status, setStatus] = useState<PayIntentFlowStatus>("idle");
const sessionRef = useRef<ReturnType<EpochFlowsSDK["createPaySession"]>>();
useEffect(() => {
if (!walletClient || !address) return;
const session = sdk.createPaySession({
walletClient,
address,
...intentArgs,
});
const unsubs = [
session.on("statusChange", setStatus),
// session.on('success', …), session.on('error', …)
];
sessionRef.current = session;
return () => {
unsubs.forEach((u) => u());
session.dispose();
};
}, [walletClient, address]);
return { status, submit: (input) => sessionRef.current?.submit(input) };
}If you're building in React and don't need a bespoke UI, just use
@epoch-protocol/epoch-intent-widget— it ships exactly this wiring plus a polished, themeable component.
Full export map
// Main class + session factories
EpochFlowsSDK;
// Sessions (and their config/event/quote types)
(PaySession, EarnSession, TypedEventEmitter);
// Pure builders
(buildPayIntentFromFlatProps, buildEarnDepositIntent, buildEarnWithdrawIntent);
// Earn adapters / helpers
(toEpochEarnMarket,
flattenConfigsToMarkets,
deriveChainsAndLenders,
oneDeltaPositionsToEpoch,
oneDeltaPositionsSummary,
unwrapPoolsArray,
poolsResponseToConfigs,
byConfigResponseToConfigs,
estimatedAnnualYield,
chainLabelFor,
HARDCODED_ONEDELTA_CONFIGS);
// Earn fetchers
(fetchLendingPools, fetchLendingPoolsByConfig, fetchUserPositions);
// Mock data
(MOCK_MAINNET_MARKETS, MOCK_TESTNET_MARKETS, mockPositionsForAddress);
// Chain helpers + registries
(getChainPublicClient,
getDefaultRpcUrl,
resolveRpcUrl,
fetchTokenBalanceOnChain,
EPOCH_SUPPORTED_CHAINS,
EPOCH_TESTNET_CHAINS,
getEpochChainById,
getEpochChains,
getChainName,
EPOCH_SUPPORTED_TOKENS,
EPOCH_TESTNET_TOKENS,
getEpochTokensByChainEnv,
getEpochTokensBySymbol);
// Formatting + ids
(formatAmount,
formatRawAmount,
formatTokenIn,
formatBalancePortionForInput,
trimAmountInput,
truncateAddress,
makeId);All domain types (IntentConfig, IntentProps, EpochToken, EpochChain, EpochEarnMarket, EpochEarnPosition, OneDeltaConfig, the session config/event/quote types, etc.) are re-exported from the package root via export type * from './types' plus the per-module type exports above. See src/index.ts for the authoritative surface.
Concepts & gotchas
- Amount units. Builder
*Human/amountparams and flat-paytoAmountare decimal strings.requiredAmounton an intent and session config is abigintin atomic units. Don't mix them. fixedOutput.true= "deliver exactlyrequiredAmount; the user pays the quoted input."slippageBps(default100= 1%) lets the solver land marginally under the display amount on cross-chain/cross-token routes; set0for strict pinning.protocolrouting.transfer/swap/bridgemap to the solver's simple token-in→token-out task. Any other protocol string is treated as a custom protocol interaction (raffles, vault deposits, etc.).- Earn is mainnet-only. The 1delta upstream doesn't index testnet pools.
- Markets need a
oneDeltaMarketUid.EarnSessionderives the destination chain + underlying from it and validates them againstmarket.chainId/market.token.address. - Listener errors are swallowed.
TypedEventEmitterlogs (but doesn't rethrow) if one listener throws, so a bad subscriber can't break the others or the flow. - Always
dispose(). Sessions hold polling/progress intervals; dispose on unmount/teardown to stop them.
License
MIT
