@pafi-dev/trading
v0.4.0
Published
Stateless on-chain trading handlers for PAFI — swap, quote, perp deposit
Readme
@pafi-dev/trading
On-chain trading for PAFI: V4 swap quote + UserOp build, Orderly perp deposit. Direction-agnostic since 0.2.0 — quote and swap any ERC-20 → ERC-20 routable through PAFI's V4 pools.
Browser + Node-safe. Stateless (no DB, no signer, no auth). Peer-deps:
viem ^2. Plus @pafi-dev/core for primitives.
Why this exists
Issuer backends (@pafi-dev/issuer) cover the issuer-signed flows
(claim, redeem). Trading is the FE-callable surface for actions that
don't need an issuer signature:
- Quote PT → USDT, USDT → PT, PT0 → PT1 — pure on-chain V4 Quoter read, multicall'd.
- Build swap UserOp with operator gas-reimbursement fee in the input token (auto-quoted via Chainlink). Sponsored + fallback variants.
- Build perp deposit UserOp via the PAFI Orderly Relay (Relay
covers LayerZero
msg.value, user pays USDC fee).
Stateless and HTTP-free — issuer backends or FE apps that want a
server-canonical quote/swap can wrap TradingHandlers directly.
Installation
pnpm add @pafi-dev/trading @pafi-dev/core viemQuick start (FE)
import { createPublicClient, http } from "viem";
import {
TradingHandlers,
fetchPafiPools,
} from "@pafi-dev/trading";
import { getContractAddresses } from "@pafi-dev/core";
const provider = createPublicClient({ transport: http("https://mainnet.base.org") });
const trading = new TradingHandlers({ provider, chainId: 8453 });
const { usdt } = getContractAddresses(8453);
// Pool discovery — loop call when both sides are PT (PT0 → PT1 multi-hop).
const pools = await fetchPafiPools(8453, POINT_TOKEN);
// Quote — direction-agnostic.
const q = await trading.handleQuote({
chainId: 8453,
inputTokenAddress: POINT_TOKEN,
outputTokenAddress: usdt,
amount: 100n * 10n ** 18n, // 100 PT
pools,
});
// q.estimatedOutputAmount — bigint USDT raw (6 dec)
// Swap — builds UserOp ready to submit via Bundler + EIP-7702.
const swap = await trading.handleSwap({
chainId: 8453,
userAddress,
inputTokenAddress: POINT_TOKEN,
outputTokenAddress: usdt,
amount: 100n * 10n ** 18n,
aaNonce,
pools,
// gasFeeAmount auto-quotes when input is a PT; pass explicit bigint to override.
// slippageBps auto-picks 50 bps single-hop / 100 bps multi-hop.
});
// swap.userOp + swap.userOpFallback (when fee > 0)
// swap.hops — number of hops in chosen routeDirection matrix
| Direction | Use case | Hop count | Hook fee | Operator fee token |
| --- | --- | --- | --- | --- |
| PT → USDT | Cashout | 1 | 10% on this leg | PT |
| USDT → PT | Buy PT | 1 | 0% | USDT |
| PT0 → PT1 | Same-issuer multi-token swap | 2 (via USDT) | 10% on PT0 leg only | PT0 |
Same-issuer only for PT0 → PT1 — caller is responsible for ensuring both PTs belong to the same issuer. SDK doesn't enforce.
The PAFI V4 hook charges 10% on PT → USDT direction at pool level
(applied inside UniversalRouter.execute). Reflected in
estimatedOutputAmount from findBestQuote.
Operator fee strategy
The operator gas-reimbursement fee is charged in the input token
(0.2.0+). Single approve covers amountIn + gasFeeAmount. User only
needs to hold a single token.
| Input | Auto-quote source | Caller needs to override? |
| --- | --- | --- |
| PT | quoteOperatorFeePt (Chainlink + V4 subgraph) | No |
| USDT | falls back to 0 — pass gasFeeAmount explicitly | Yes (use quoteOperatorFeeUsdt) |
Multi-hop PT → PT1
import { fetchPafiPools, TradingHandlers } from "@pafi-dev/trading";
// Loop call — fetch pools for both PTs, merge.
const [poolsA, poolsB] = await Promise.all([
fetchPafiPools(8453, POINT_TOKEN_0),
fetchPafiPools(8453, POINT_TOKEN_1),
]);
const pools = [...poolsA, ...poolsB];
const swap = await trading.handleSwap({
chainId: 8453,
userAddress,
inputTokenAddress: POINT_TOKEN_0,
outputTokenAddress: POINT_TOKEN_1,
amount: 100n * 10n ** 18n,
aaNonce,
pools,
});
console.log(swap.hops); // 2 — went through USDTV4 router auto-picks the best route across pools + COMMON_POOLS
(default maxHops=3). Slippage auto-bumps to 100 bps for multi-hop.
Perp deposit
const deposit = await trading.handlePerpDeposit({
chainId: 8453,
userAddress,
amount: 1_000_000n, // 1 USDC (6 dec)
aaNonce,
brokerId: "orderly", // "orderly" | "woofi_pro" | "logx"
// viaRelay: true (default) — uses PAFI Orderly Relay (no msg.value needed)
pointTokenAddress: POINT_TOKEN, // optional — for PT operator fee
});
// deposit.userOp — submit via Bundler + Paymaster
// deposit.path — "relay" (preferred) or "vault" (direct, requires native ETH)The PAFI Relay covers LayerZero msg.value from its ETH reserve and
charges a USDC fee (Relay.quoteTokenFee). User wallet doesn't need
native ETH.
Standalone primitives
For callers that want to compose flows beyond what TradingHandlers
provides:
import {
buildSwapUserOp,
findBestQuote,
simulateSwap,
buildUniversalRouterExecuteArgs,
buildPermit2ApprovalCalldata,
fetchPafiPools,
} from "@pafi-dev/trading";Use these to build custom direction-agnostic swap flows or pre-flight simulations.
Direct path — no AA, no bundler, no sponsor-relayer
v0.4.0+ ships swapDirect() + perpDepositDirect() for the FE-only
flow where the user pays gas in ETH and broadcasts a single type-2 tx
calling its own EIP-7702 delegated bytecode. No Pimlico API key, no
sponsor-relayer, no PAFI infra.
Precondition: user EOA must already be EIP-7702 delegated (run
delegateDirect() from @pafi-dev/core first; both helpers throw a
clear error otherwise).
swapDirect
import { swapDirect, fetchPafiPools } from "@pafi-dev/trading";
import { createPublicClient, createWalletClient, custom, http, parseUnits } from "viem";
import { base } from "viem/chains";
const publicClient = createPublicClient({ chain: base, transport: http() });
// `walletClient` from Privy embedded wallet, MetaMask, etc.
const walletClient = createWalletClient({
account: wallet.address,
chain: base,
transport: custom(await wallet.getEthereumProvider()),
});
const pools = await fetchPafiPools(8453, POINT_TOKEN);
const result = await swapDirect({
userAddress: wallet.address,
chainId: 8453,
inputTokenAddress: POINT_TOKEN,
outputTokenAddress: USDT,
amount: parseUnits("100", 18),
pools,
publicClient,
walletClient,
// optional: slippageBps, deadline, gasFeeAmountOutput, waitForReceipt
});
console.log("Swap tx:", result.txHash);
// {
// txHash, receipt?, estimatedOutputAmount, minAmountOut, hops,
// deadline, feeAmountUsed
// }Operator fee: default 0n (skip — PAFI not sponsoring this swap).
Pass an explicit gasFeeAmountOutput: bigint only if you want to mirror
the sponsored flow's fee transfer.
perpDepositDirect
import { perpDepositDirect } from "@pafi-dev/trading";
const result = await perpDepositDirect({
userAddress: wallet.address,
chainId: 8453,
amount: parseUnits("10", 6), // 10 USDC
brokerId: "orderly", // | "woofi_pro" | "logx"
publicClient,
walletClient,
// optional: maxRelayFee, gasFeeUsdc, waitForReceipt
});
// {
// txHash, receipt?, relayTokenFee, maxFee, netDeposit,
// feeAmountUsed, accountId, brokerHash, usdcAddress, relayAddress
// }The Relay still charges its own USDC fee (Relay.quoteTokenFee) to
cover LayerZero msg.value; that's separate from PAFI's operator fee
(which is skipped by default on the direct path).
When to use direct vs. AA path
| Aspect | swapDirect / perpDepositDirect | TradingHandlers.handleSwap / handlePerpDeposit |
| --- | --- | --- |
| User pays gas | ETH (native) | Free (sponsor) or ETH (fallback) |
| Bundler required | No | Yes (Pimlico) |
| Sponsor-relayer required | No | Yes (sponsored path) |
| Operator fee charged | Default skip | Default included |
| Round trips | 1 (broadcast tx) | 2+ (paymaster + bundler) |
| Precondition | EIP-7702 delegated | Same |
Use direct for FE dev/test, integrations without a Pimlico key, or production paths where users explicitly opt to pay gas. Use AA path for the production gas-free UX via sponsor-relayer.
API reference
TradingHandlers
new TradingHandlers({ provider: PublicClient, chainId: number })Methods:
| Method | Purpose |
| --- | --- |
| handleQuote({ inputTokenAddress, outputTokenAddress, amount, pools? }) | V4 quote. Returns { inputAmount, estimatedOutputAmount, gasEstimate, quoteError? } |
| handleSwap({ userAddress, inputTokenAddress, outputTokenAddress, amount, aaNonce, pools?, gasFeeAmount?, slippageBps? }) | Build UserOp. Returns { userOp, userOpFallback?, estimatedOutputAmount, minAmountOut, hops, deadline, feeAmountUsed, feeRecipient } |
| handlePerpDeposit({ userAddress, amount, aaNonce, brokerId, viaRelay?, maxRelayFee?, pointTokenAddress?, gasFeePt? }) | Build UserOp. Returns Orderly deposit response |
Free functions
findBestQuote(client, chainId, tokenIn, tokenOut, amount, pools, quoterAddress?, maxHops?)quoteBestRoute,quoteExactInput,quoteExactInputSinglebuildAllPaths,combineRoutesbuildSwapUserOp(params)— direction-agnostic UserOp builderbuildUniversalRouterExecuteArgs,buildV4SwapInput,buildSwapFromQuotebuildPermit2ApprovalCalldata,buildErc20ApprovalCalldata,checkAllowancesimulateSwap—eth_calldry-runfetchPafiPools(chainId, pointTokenAddress, subgraphUrl?)swapDirect(params)— FE-direct swap (no AA, user pays gas) — see "Direct path" sectionperpDepositDirect(params)— FE-direct perp deposit (no AA, user pays gas)
License
Apache-2.0
