Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/connector-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
{
label: 'Automate yield strategies',
display:
'Deposit USDC into the highest-TVL lending vault on Polygon or Katana with TVL above $100M and report the APY and pool address. Then set up a daily cron job to automatically re-evaluate and deposit into the best vault each morning.',
'Deposit USDC into the highest-yield active lending vault on Polygon and report the APY and pool address. Then set up a daily cron job to automatically re-evaluate and deposit into the best vault each morning.',
icon: TrendingUp
}
];
Expand All @@ -144,7 +144,7 @@
const [sessionCode, setSessionCode] = useState<string>('');
const [showFunding, setShowFunding] = useState(false);
const [showDashboard, setShowDashboard] = useState(false);
const [feeTokens, setFeeTokens] = useState<any | null>(null);

Check warning on line 147 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const [selectedUseCase, setSelectedUseCase] = useState(0);
const [selectedAgent, setSelectedAgent] = useState<string>('claude');
const [copied, setCopied] = useState(false);
Expand Down Expand Up @@ -177,7 +177,7 @@
}
setCliPkHex(cli_pk_hex);
})
.catch((e: any) => setError(`Failed to load session key: ${e?.message || String(e)}`));

Check warning on line 180 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}, [rid]);

// Fetch USD portfolio balance when wallet address is first known
Expand Down Expand Up @@ -228,7 +228,7 @@
} catch {
setFeeTokens(null);
}
} catch (e: any) {

Check warning on line 231 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
setError(e?.message || String(e));
}
})();
Expand All @@ -253,12 +253,12 @@
const VALUE_FORWARDER = '0xABAAd93EeE2a569cF0632f39B10A9f5D734777ca';
const USDC = (await resolveErc20Symbol(chainId, 'USDC'))?.address;
const USDT = (await resolveErc20Symbol(chainId, 'USDT'))?.address;
const basePermissions: any[] = [{ target: VALUE_FORWARDER, rules: [] }];

Check warning on line 256 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const searchParams = new URLSearchParams(window.location.search);
const erc20 = searchParams.get('erc20');
const erc20To = searchParams.get('erc20To');
const erc20Amount = searchParams.get('erc20Amount');
const oneOffErc20Permissions: any[] =

Check warning on line 261 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
erc20 && erc20To && erc20Amount
? (() => {
const tokenAddr = erc20.toLowerCase() === 'usdc' ? USDC : erc20;
Expand All @@ -267,7 +267,7 @@
const f = (fRaw + '0'.repeat(decimals)).slice(0, decimals);
const valueLimit = BigInt(i || '0') * 10n ** BigInt(decimals) + BigInt(f || '0');
return [
Utils.PermissionBuilder.for(tokenAddr as any)

Check warning on line 270 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
.forFunction('function transfer(address to, uint256 value)')
.withUintNParam(
'value',
Expand All @@ -278,7 +278,7 @@
)
.withAddressParam(
'to',
erc20To as any,

Check warning on line 281 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
Permission.ParameterOperation.EQUAL,
false
)
Expand All @@ -292,8 +292,8 @@
const nativeLimit = searchParams.get('nativeLimit') || searchParams.get('polLimit');
const tokenLimitsRaw = searchParams.get('tokenLimits');
const USDC_E_POLYGON = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
const openTokenPermissions: any[] = [];

Check warning on line 295 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const dynamicTokenPermissions: any[] = [];

Check warning on line 296 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (tokenLimitsRaw) {
const parts = tokenLimitsRaw
.split(',')
Expand All @@ -305,7 +305,7 @@
const td = await resolveErc20Symbol(chainId, sym);
if (!td) throw new Error(`${sym} not found for this chain in token-directory`);
dynamicTokenPermissions.push(
Utils.PermissionBuilder.for(td.address as any)

Check warning on line 308 in packages/connector-ui/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
.forFunction('function transfer(address to, uint256 value)')
.withUintNParam(
'value',
Expand Down
42 changes: 26 additions & 16 deletions packages/polygon-agent-cli/skills/polygon-defi/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ interface PoolTokenInfo {

## Deposit to Earn Yield

Pool discovery uses `TrailsApi.getEarnPools` — picks the most liquid pool (highest TVL) for the asset on the current chain. No hardcoded addresses — the pool is resolved at runtime.
Pool discovery uses `TrailsApi.getEarnPools` — picks the most liquid pool (highest TVL) for the asset. Only Polygon mainnet (chainId 137) is supported. No hardcoded addresses — the pool is resolved at runtime.

**Gas requirement:** The wallet needs POL for gas, or a session created with `--usdc-limit` to enable USDC paymaster. If the wallet has no POL, create the session with `--usdc-limit 5`. When USDC paymaster is active and the deposit amount would consume the full balance, the CLI auto-reserves 0.05 USDC for gas and prints a note.

```bash
# Dry-run — shows pool name, APY, TVL, and deposit address before committing
Expand Down Expand Up @@ -181,19 +183,6 @@ Common token contracts on Polygon mainnet (already auto-whitelisted in sessions
| Morpho Compound WETH | WETH | `0xf5c81d25ee174d83f1fd202ca94ae6070d073ccf` |
| Morpho Compound POL | POL | `0x3f33f9f7e2d7cfbcbdf8ea8b870a6e3d449664c2` |

#### Katana (chainId 747474) — Morpho Vaults

| Vault | Asset | TVL | Address |
|-------|-------|-----|---------|
| Gauntlet USDT | USDT | ~$97M | `0x1ecdc3f2b5e90bfb55ff45a7476ff98a8957388e` |
| Steakhouse Prime USDC | USDC | ~$54M | `0x61d4f9d3797ba4da152238c53a6f93fb665c3c1d` |
| Yearn OG ETH | WETH | ~$16M | `0xfade0c546f44e33c134c4036207b314ac643dc2e` |
| Yearn OG USDC | USDC | ~$16M | `0xce2b8e464fc7b5e58710c24b7e5ebfb6027f29d7` |
| Gauntlet USDC | USDC | ~$8M | `0xe4248e2105508fcbad3fe95691551d1af14015f7` |
| Yearn OG USDT | USDT | ~$8M | `0x8ed68f91afbe5871dce31ae007a936ebe8511d47` |
| Gauntlet WETH | WETH | ~$6M | `0xc5e7ab07030305fc925175b25b93b285d40dcdff` |
| Hyperithm vbUSDC Apex | USDC | ~$3M | `0xef77f8c53af95f3348cee0fb2a02ee02ab9cdca5` |

---

## Full DeFi Flow Example
Expand All @@ -216,11 +205,32 @@ polygon-agent swap --from USDC --to USDC --amount 0.5 --to-chain arbitrum --broa

---

## wallet create — Key Options

| Flag | Purpose |
|------|---------|
| `--usdc-limit <amt>` | Enable USDC gas paymaster. Required when the wallet has no POL. Recommended: `--usdc-limit 5`. |
| `--force` | Replace an existing session without prompting. By default, re-creating a session is blocked if one already exists — the old wallet balance is not accessible from a new session. |
| `--contract <addr>` | Whitelist an additional contract (repeatable). Use this if a deposit is rejected due to a missing contract permission. |

```bash
# New session with USDC gas and deposit contracts pre-whitelisted
polygon-agent wallet create --usdc-limit 5

# Replace an existing session
polygon-agent wallet create --force --usdc-limit 5
```

---

## Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| `Deposit session rejected` | Pool or token contract not whitelisted | Re-create wallet with `--contract <tokenAddress> --contract <depositAddress>` (both required) |
| `Insufficient <token>: wallet has X` | Balance too low for the requested deposit amount | Run `polygon-agent balances` and adjust `--amount` |
| `Wallet has no POL for gas` | No native gas and no USDC paymaster | Fund with POL (`polygon-agent fund`) or re-create session with `--usdc-limit 5` |
| `Transaction rejected by relay` | Session permissions missing for pool or token contract | Re-create with `--contract <tokenAddress> --contract <depositAddress>` |
| `Unable to pay gas` | No usable fee token found | Fund with POL or add `--usdc-limit 5` to session |
| `Wallet already exists` | Re-creating would orphan the old session | Use `--force` only after confirming old wallet funds are swept or unneeded |
| `Protocol X not yet supported` | Trails returned a protocol other than aave/morpho | Use `polygon-agent swap` to obtain the yield-bearing token manually |
| `Fee option errors` | Wallet has insufficient balance | Run `polygon-agent balances` and fund the wallet |
| `swap`: no route found | Insufficient liquidity for the pair | Try a different amount or token pair |
120 changes: 99 additions & 21 deletions packages/polygon-agent-cli/src/commands/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,8 @@ export const balancesCommand: CommandModule = {
// --- fund ---
export const fundCommand: CommandModule = {
command: 'fund',
describe: 'Open Trails widget to fund wallet',
builder: (yargs) =>
withWalletAndChain(yargs).option('token', {
type: 'string',
describe: 'Fund token address'
}),
describe: 'Get funding URL for wallet',
builder: (yargs) => withWalletAndChain(yargs),
handler: async (argv) => {
const walletName = argv.wallet as string;

Expand All @@ -374,10 +370,7 @@ export const fundCommand: CommandModule = {

const walletAddress = session.walletAddress;
const chainId = session.chainId || 137;
const toToken = (argv.token as string) || '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359';
const apiKey = process.env.SEQUENCE_PROJECT_ACCESS_KEY || '';

const fundingUrl = `https://demo.trails.build/?mode=swap&toAddress=${walletAddress}&toChainId=${chainId}&toToken=${toToken}&apiKey=${apiKey}&theme=light`;
const fundingUrl = `https://wallet.polygon.technology`;

if (isTTY()) {
await inkRender(
Expand All @@ -392,7 +385,7 @@ export const fundCommand: CommandModule = {
walletAddress,
chainId,
fundingUrl,
message: 'Open the funding URL in your browser to fund your wallet via Trails.'
message: `Visit ${fundingUrl} to fund your wallet (${walletAddress}).`
},
null,
2
Expand Down Expand Up @@ -1083,7 +1076,7 @@ export const depositCommand: CommandModule = {
handler: async (argv) => {
const walletName = (argv.wallet as string) || 'main';
const assetSymbol = ((argv.asset as string) || 'USDC').toUpperCase();
const amountArg = argv.amount as string;
let amountArg = argv.amount as string;
const protocolFilter = argv.protocol as string | undefined;
const broadcast = argv.broadcast as boolean;

Expand Down Expand Up @@ -1128,9 +1121,10 @@ export const depositCommand: CommandModule = {

if (pools.length === 0) {
throw new Error(
`No active earn pools found for ${assetSymbol} on ${network.name}` +
(protocolFilter ? ` (protocol filter: ${protocolFilter})` : '') +
`. Try 'polygon-agent swap --from ${assetSymbol} --to <yield-token>' as an alternative.`
`No active ${assetSymbol} earn pools found on ${network.name}` +
(protocolFilter ? ` (protocol: ${protocolFilter})` : '') +
`. Confirm wallet state: polygon-agent balances. ` +
`Alternative: polygon-agent swap --from ${assetSymbol} --to <yield-token>.`
);
}

Expand All @@ -1139,7 +1133,57 @@ export const depositCommand: CommandModule = {
const pool = pools[0];
const proto = (pool.protocol || '').toLowerCase();

const { encodeFunctionData, parseUnits: viemParseUnits } = await import('viem');
const {
encodeFunctionData,
parseUnits: viemParseUnits,
formatUnits: viemFormatUnits,
createPublicClient,
http
} = await import('viem');
// Pre-flight: verify balance and auto-reserve gas buffer when wallet has no native token
try {
const viemChain = await viemChainForDeposit(chainId);
const publicClient = createPublicClient({ chain: viemChain, transport: http() });
const [usdcBal, nativeBal] = await Promise.all([
publicClient.readContract({
address: asset.address as `0x${string}`,
abi: ERC20_BALANCE_OF_ABI,
functionName: 'balanceOf',
args: [walletAddress as `0x${string}`]
}),
publicClient.getBalance({ address: walletAddress as `0x${string}` })
]);
const requestedUnits = viemParseUnits(amountArg, asset.decimals);
const GAS_RESERVE = viemParseUnits('0.05', asset.decimals);
if (usdcBal < requestedUnits) {
const available = viemFormatUnits(usdcBal, asset.decimals);
throw new Error(
`Insufficient ${assetSymbol}: wallet has ${available} ${assetSymbol}, deposit requires ${amountArg}. ` +
`Run: polygon-agent balances`
);
}
if (nativeBal === 0n && requestedUnits + GAS_RESERVE > usdcBal) {
// Wallet has no POL — USDC paymaster will pay gas; auto-reduce to leave 0.05 buffer
const adjusted = usdcBal - GAS_RESERVE;
if (adjusted <= 0n) {
throw new Error(
`Insufficient ${assetSymbol} for deposit plus 0.05 gas reserve. ` +
`Fund with POL for native gas: polygon-agent fund`
);
}
amountArg = viemFormatUnits(adjusted, asset.decimals);
process.stderr.write(
`Note: reduced deposit to ${amountArg} ${assetSymbol} (0.05 reserved for USDC gas)\n`
);
}
} catch (e) {
if ((e as Error).message?.match(/^Insufficient/)) throw e;
// RPC unreachable — warn and continue
process.stderr.write(
`Warning: balance pre-flight check skipped (${(e as Error).message})\n`
);
}

const amountUnits = viemParseUnits(amountArg, asset.decimals);

const ERC20_APPROVE_ABI = [
Expand Down Expand Up @@ -1267,11 +1311,31 @@ export const depositCommand: CommandModule = {
preferNativeFee: false
});
} catch (txErr) {
if ((txErr as Error).message?.includes('No signer supported')) {
const txMsg = (txErr as Error).message || '';
if (txMsg.includes('No signer supported')) {
throw new Error(
`Session does not permit calls to ${pool.depositAddress} (${pool.protocol} pool) or ${asset.address} (${assetSymbol} approve). ` +
`Re-create the wallet session with: polygon-agent wallet create --contract ${asset.address} --contract ${pool.depositAddress}\n` +
`Original error: ${(txErr as Error).message}`
`Original error: ${txMsg}`
);
}
if (txMsg.includes('Identity signers not found') || txMsg.includes('signers not found')) {
throw new Error(
`Wallet has no POL for gas and no USDC paymaster is configured. ` +
`Fund with POL: polygon-agent fund\n` +
`Or enable USDC gas: polygon-agent wallet create --usdc-limit 5\n` +
`Original error: ${txMsg}`
);
}
if (
txMsg.includes('Request aborted') ||
txMsg.includes('AbortedError') ||
txMsg.includes('code 1005')
) {
throw new Error(
`Transaction rejected by relay — likely a session permission issue. ` +
`Re-create the wallet session: polygon-agent wallet create --contract ${asset.address} --contract ${pool.depositAddress}\n` +
`Original error: ${txMsg}`
);
}
throw txErr;
Expand Down Expand Up @@ -1400,10 +1464,10 @@ const ERC4626_REDEEM_ABI = [
}
] as const;

async function viemChainForWithdraw(chainId: number) {
async function viemChainMap() {
const { mainnet, polygon, arbitrum, optimism, base, avalanche, bsc, gnosis, polygonAmoy } =
await import('viem/chains');
const map = {
return {
1: mainnet,
137: polygon,
42161: arbitrum,
Expand All @@ -1413,7 +1477,21 @@ async function viemChainForWithdraw(chainId: number) {
56: bsc,
100: gnosis,
80002: polygonAmoy
} as const;
} as const satisfies Record<number, unknown>;
}

async function viemChainForDeposit(chainId: number) {
const map = await viemChainMap();
const c = map[chainId as keyof typeof map];
if (!c)
throw new Error(
`deposit: chainId ${chainId} has no bundled viem chain config for pre-flight check.`
);
return c;
}

async function viemChainForWithdraw(chainId: number) {
const map = await viemChainMap();
const c = map[chainId as keyof typeof map];
if (!c) {
throw new Error(
Expand Down
9 changes: 7 additions & 2 deletions packages/polygon-agent-cli/src/lib/dapp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ export async function runDappClientTx({

if (paymentAddress && erc20Token) {
const decimals = typeof erc20Token.decimals === 'number' ? erc20Token.decimals : 6;
const feeValue = decimals >= 2 ? 10 ** (decimals - 2) : 1;
// 0.1 token units as fee cap (e.g. 0.1 USDC = 100000 for 6 decimals)
const feeValue = decimals >= 1 ? 10 ** (decimals - 1) : 1;
feeOpt = {
token: erc20Token,
to: paymentAddress,
Expand All @@ -399,7 +400,11 @@ export async function runDappClientTx({
const feeOptions = await client.getFeeOptions(chainId, transactions as any);
feeOpt = feeOptions?.[0];
} catch (e) {
throw new Error(`Unable to determine fee option: ${(e as Error)?.message}`);
throw new Error(
`Unable to pay gas: wallet has no POL (native gas) and no usable fee token. ` +
`Fund with POL: polygon-agent fund, or enable USDC gas: polygon-agent wallet create --usdc-limit 5. ` +
`Technical: ${(e as Error)?.message}`
);
}
}

Expand Down
37 changes: 21 additions & 16 deletions skills/polygon-defi/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ interface PoolTokenInfo {

## Deposit to Earn Yield

Pool discovery uses `TrailsApi.getEarnPools` — picks the most liquid pool (highest TVL) for the asset on the current chain. No hardcoded addresses — the pool is resolved at runtime.
Pool discovery uses `TrailsApi.getEarnPools` — picks the most liquid pool (highest TVL) for the asset on the requested chain. No hardcoded addresses — the pool is resolved at runtime. Supported chains: Polygon, Base, Arbitrum, Optimism, Ethereum mainnet (any chain Trails indexes).

**Gas requirement:** The wallet needs POL for gas, or a session created with `--usdc-limit` to enable USDC paymaster. If the wallet has no POL, create the session with `--usdc-limit 5`. When USDC paymaster is active and the deposit amount would consume the full balance, the CLI auto-reserves 0.05 USDC for gas and prints a note.

```bash
# Dry-run — shows pool name, APY, TVL, and deposit address before committing
Expand Down Expand Up @@ -181,19 +183,6 @@ Common token contracts on Polygon mainnet (already auto-whitelisted in sessions
| Morpho Compound WETH | WETH | `0xf5c81d25ee174d83f1fd202ca94ae6070d073ccf` |
| Morpho Compound POL | POL | `0x3f33f9f7e2d7cfbcbdf8ea8b870a6e3d449664c2` |

#### Katana (chainId 747474) — Morpho Vaults

| Vault | Asset | TVL | Address |
|-------|-------|-----|---------|
| Gauntlet USDT | USDT | ~$97M | `0x1ecdc3f2b5e90bfb55ff45a7476ff98a8957388e` |
| Steakhouse Prime USDC | USDC | ~$54M | `0x61d4f9d3797ba4da152238c53a6f93fb665c3c1d` |
| Yearn OG ETH | WETH | ~$16M | `0xfade0c546f44e33c134c4036207b314ac643dc2e` |
| Yearn OG USDC | USDC | ~$16M | `0xce2b8e464fc7b5e58710c24b7e5ebfb6027f29d7` |
| Gauntlet USDC | USDC | ~$8M | `0xe4248e2105508fcbad3fe95691551d1af14015f7` |
| Yearn OG USDT | USDT | ~$8M | `0x8ed68f91afbe5871dce31ae007a936ebe8511d47` |
| Gauntlet WETH | WETH | ~$6M | `0xc5e7ab07030305fc925175b25b93b285d40dcdff` |
| Hyperithm vbUSDC Apex | USDC | ~$3M | `0xef77f8c53af95f3348cee0fb2a02ee02ab9cdca5` |

---

## Full DeFi Flow Example
Expand All @@ -216,11 +205,27 @@ polygon-agent swap --from USDC --to USDC --amount 0.5 --to-chain arbitrum --broa

---

## wallet create — Key Options

| Flag | Purpose |
|------|---------|
| `--usdc-limit <amt>` | Enable USDC gas paymaster. Required when the wallet has no POL. Recommended: `--usdc-limit 5`. |
| `--contract <addr>` | Whitelist an additional contract (repeatable). Use this if a deposit is rejected due to a missing contract permission. |

```bash
# New session with USDC gas enabled
polygon-agent wallet create --usdc-limit 5
```

---

## Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| `Deposit session rejected` | Pool or token contract not whitelisted | Re-create wallet with `--contract <tokenAddress> --contract <depositAddress>` (both required) |
| `Insufficient <token>: wallet has X` | Balance too low for the requested deposit amount | Run `polygon-agent balances` and adjust `--amount` |
| `Wallet has no POL for gas` | No native gas and no USDC paymaster | Fund with POL (`polygon-agent fund`) or re-create session with `--usdc-limit 5` |
| `Transaction rejected by relay` | Session permissions missing for pool or token contract | Re-create with `--contract <tokenAddress> --contract <depositAddress>` |
| `Unable to pay gas` | No usable fee token found | Fund with POL or add `--usdc-limit 5` to session |
| `Protocol X not yet supported` | Trails returned a protocol other than aave/morpho | Use `polygon-agent swap` to obtain the yield-bearing token manually |
| `Fee option errors` | Wallet has insufficient balance | Run `polygon-agent balances` and fund the wallet |
| `swap`: no route found | Insufficient liquidity for the pair | Try a different amount or token pair |
Loading