glend

Installation
SKILL.md

Glend Agent Skill

What is Glend?

Glend is a decentralized lending and borrowing protocol deployed on multiple EVM chains by GemachDAO. It allows agents to supply assets to earn interest, borrow against collateral, repay debt, and withdraw supplied funds. Glend runs as:

  • Aave V3 fork on Pharos Testnet (Chain ID 688688) — default
  • Compound V2 fork (gTokens/tTokens) on Ethereum (Chain ID 1) and Base (Chain ID 8453)

Glend App: https://glendv2.gemach.io

Agent capabilities:

  • Supply / Lend — deposit assets to earn interest
  • Borrow — take loans against supplied collateral
  • Repay — pay back outstanding debt
  • Withdraw — reclaim supplied assets (subject to utilization and health factor)
  • Monitor health — track collateral ratio and liquidation risk
  • Get market data — supply/borrow APY, reserve data
  • Get test tokens — faucet on Pharos Testnet

All on-chain interactions use viem. Contract addresses and chain configuration are pre-loaded below.


Environment Variables

Variable Required Default Description
AGENT_PRIVATE_KEY Yes Wallet private key (never log or commit)
GLEND_CHAIN_ID No 688688 Chain to operate on: 688688 (Pharos), 1 (Ethereum), 8453 (Base)
GLEND_RPC_URL No chain default Override the default RPC URL
GLEND_POOL_ADDRESS No chain default Override the default pool/comptroller address

Supported Deployments

1. Pharos Testnet — Aave V3 (default)

Parameter Value
Chain ID 688688
Protocol Aave V3 fork
RPC https://testnet.dplabs-internal.com
Explorer https://testnet.pharosscan.xyz
Native Token PHRS
Pool Contract 0xe838eb8011297024bca9c09d4e83e2d3cd74b7d0
WETHGateway 0xa8e550710bf113db6a1b38472118b8d6d5176d12
Faucet 0x2e9d89d372837f71cb529e5ba85bfbc1785c69cd

Token Addresses (Pharos Testnet):

Token Address Decimals
WETH 0x8d3e82e914271dfc98727c8f4db18ba5c3a7d3a3 18
USDT 0x4b2d8b441f7e7a6e9c5c3a3b2e1f0d9c8b7a6f5e 6
USDC 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b 6
BTC 0x9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e 8

2. Ethereum Mainnet — Compound V2 fork

Parameter Value
Chain ID 1
Protocol Compound V2 fork
Comptroller 0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58

gToken Markets (Ethereum):

Market gToken Address
gDAI 0x0A0c4d9e8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d
gUSDC 0x1B1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
gUSDT 0x2C2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e
gWBTC 0x3D3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f
gWETH 0x4E4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f
gstETH 0x5F5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a
gETH 0x6A6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b
gcbBTC 0x7B7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c
tstETH 0x8C8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
tcbBTC 0x9D9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e
tETH 0xAEaf1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a

3. Base — Compound V2 fork

Parameter Value
Chain ID 8453
Protocol Compound V2 fork
Comptroller 0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58
PriceOracle 0x97f602E17ed4e765a6968f295Bdc3F6b4c1Ef93b
CompoundLens 0x41d9071C885da8dCa042E05AA66D7D5034383C53

Getting Test Tokens (Pharos Testnet)

Use the faucet contract to mint test tokens on Pharos Testnet. The faucet is at 0x2e9d89d372837f71cb529e5ba85bfbc1785c69cd.

const FAUCET_ABI = [
  {
    name: 'mint',
    type: 'function',
    inputs: [
      { name: 'token', type: 'address' },
      { name: 'to', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [],
    stateMutability: 'nonpayable',
  },
] as const;

async function mintTestTokens(
  walletClient: WalletClient,
  publicClient: PublicClient,
  tokenAddress: `0x${string}`,
  amount: bigint
) {
  const FAUCET_ADDRESS = '0x2e9d89d372837f71cb529e5ba85bfbc1785c69cd';
  const { request } = await publicClient.simulateContract({
    address: FAUCET_ADDRESS,
    abi: FAUCET_ABI,
    functionName: 'mint',
    args: [tokenAddress, walletClient.account!.address, amount],
    account: walletClient.account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

Aave V3 Protocol — Pharos Testnet

ABI

const GLEND_POOL_ABI = [
  // Supply
  {
    name: 'supply',
    type: 'function',
    inputs: [
      { name: 'asset', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'onBehalfOf', type: 'address' },
      { name: 'referralCode', type: 'uint16' },
    ],
    outputs: [],
    stateMutability: 'nonpayable',
  },
  // Borrow
  {
    name: 'borrow',
    type: 'function',
    inputs: [
      { name: 'asset', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'interestRateMode', type: 'uint256' },
      { name: 'referralCode', type: 'uint16' },
      { name: 'onBehalfOf', type: 'address' },
    ],
    outputs: [],
    stateMutability: 'nonpayable',
  },
  // Repay
  {
    name: 'repay',
    type: 'function',
    inputs: [
      { name: 'asset', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'interestRateMode', type: 'uint256' },
      { name: 'onBehalfOf', type: 'address' },
    ],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // Withdraw
  {
    name: 'withdraw',
    type: 'function',
    inputs: [
      { name: 'asset', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'to', type: 'address' },
    ],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // getUserAccountData
  {
    name: 'getUserAccountData',
    type: 'function',
    inputs: [{ name: 'user', type: 'address' }],
    outputs: [
      { name: 'totalCollateralBase', type: 'uint256' },
      { name: 'totalDebtBase', type: 'uint256' },
      { name: 'availableBorrowsBase', type: 'uint256' },
      { name: 'currentLiquidationThreshold', type: 'uint256' },
      { name: 'ltv', type: 'uint256' },
      { name: 'healthFactor', type: 'uint256' },
    ],
    stateMutability: 'view',
  },
  // getReserveData
  {
    name: 'getReserveData',
    type: 'function',
    inputs: [{ name: 'asset', type: 'address' }],
    outputs: [
      {
        name: '',
        type: 'tuple',
        components: [
          { name: 'configuration', type: 'tuple', components: [{ name: 'data', type: 'uint256' }] },
          { name: 'liquidityIndex', type: 'uint128' },
          { name: 'currentLiquidityRate', type: 'uint128' },
          { name: 'variableBorrowIndex', type: 'uint128' },
          { name: 'currentVariableBorrowRate', type: 'uint128' },
          { name: 'currentStableBorrowRate', type: 'uint128' },
          { name: 'lastUpdateTimestamp', type: 'uint40' },
          { name: 'id', type: 'uint16' },
          { name: 'aTokenAddress', type: 'address' },
          { name: 'stableDebtTokenAddress', type: 'address' },
          { name: 'variableDebtTokenAddress', type: 'address' },
          { name: 'interestRateStrategyAddress', type: 'address' },
          { name: 'accruedToTreasury', type: 'uint128' },
          { name: 'unbacked', type: 'uint128' },
          { name: 'isolationModeTotalDebt', type: 'uint128' },
        ],
      },
    ],
    stateMutability: 'view',
  },
  // setUserUseReserveAsCollateral
  {
    name: 'setUserUseReserveAsCollateral',
    type: 'function',
    inputs: [
      { name: 'asset', type: 'address' },
      { name: 'useAsCollateral', type: 'bool' },
    ],
    outputs: [],
    stateMutability: 'nonpayable',
  },
] as const;

ERC-20 Approve ABI (used before supply/repay)

const ERC20_ABI = [
  {
    name: 'approve',
    type: 'function',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'nonpayable',
  },
  {
    name: 'allowance',
    type: 'function',
    inputs: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
    ],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  {
    name: 'balanceOf',
    type: 'function',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
] as const;

Agent Operations

1. Set Up viem Clients

import { createPublicClient, createWalletClient, http, type Chain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet, base } from 'viem/chains';

const pharosTestnet: Chain = {
  id: 688688,
  name: 'Pharos Testnet',
  nativeCurrency: { name: 'PHRS', symbol: 'PHRS', decimals: 18 },
  rpcUrls: {
    default: { http: ['https://testnet.dplabs-internal.com'] },
  },
  blockExplorers: {
    default: { name: 'Pharos Explorer', url: 'https://testnet.pharosscan.xyz' },
  },
};

const CHAIN_CONFIG: Record<number, { chain: Chain; rpc: string; pool: `0x${string}`; protocol: string }> = {
  688688: {
    chain: pharosTestnet,
    rpc: 'https://testnet.dplabs-internal.com',
    pool: '0xe838eb8011297024bca9c09d4e83e2d3cd74b7d0',
    protocol: 'aave-v3',
  },
  1: {
    chain: mainnet,
    rpc: 'https://eth.llamarpc.com',
    pool: '0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58',
    protocol: 'compound-v2',
  },
  8453: {
    chain: base,
    rpc: 'https://mainnet.base.org',
    pool: '0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58',
    protocol: 'compound-v2',
  },
};

const PHAROS_TOKENS = {
  WETH: { address: '0x8d3e82e914271dfc98727c8f4db18ba5c3a7d3a3' as `0x${string}`, decimals: 18 },
  USDT: { address: '0x4b2d8b441f7e7a6e9c5c3a3b2e1f0d9c8b7a6f5e' as `0x${string}`, decimals: 6 },
  USDC: { address: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b' as `0x${string}`, decimals: 6 },
  BTC:  { address: '0x9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e' as `0x${string}`, decimals: 8 },
};

const chainId = Number(process.env.GLEND_CHAIN_ID ?? '688688');
const config = CHAIN_CONFIG[chainId];
if (!config) throw new Error(`Unsupported GLEND_CHAIN_ID: ${chainId}. Supported: 688688, 1, 8453`);

const rpcUrl = process.env.GLEND_RPC_URL ?? config.rpc;

const publicClient = createPublicClient({
  chain: config.chain,
  transport: http(rpcUrl),
});

const account = privateKeyToAccount(
  (process.env.AGENT_PRIVATE_KEY as `0x${string}`)
);

const walletClient = createWalletClient({
  account,
  chain: config.chain,
  transport: http(rpcUrl),
});

const POOL_ADDRESS = (process.env.GLEND_POOL_ADDRESS ?? config.pool) as `0x${string}`;

2. Approve ERC-20

Always approve the exact amount before calling supply or repay. Never use an open-ended allowance.

async function approveToken(
  tokenAddress: `0x${string}`,
  spender: `0x${string}`,
  amount: bigint
) {
  const { request } = await publicClient.simulateContract({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [spender, amount],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

3. Supply (Lend) Assets

async function supplyAsset(
  tokenAddress: `0x${string}`,
  amount: bigint
) {
  // Step 1: approve pool to spend tokens
  await approveToken(tokenAddress, POOL_ADDRESS, amount);

  // Step 2: supply
  const { request } = await publicClient.simulateContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'supply',
    args: [tokenAddress, amount, account.address, 0],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

4. Borrow Assets

Check health factor >= 1.5 (in WAD = 1e18 units) before borrowing. Interest rate mode 2 = variable rate.

async function borrowAsset(
  tokenAddress: `0x${string}`,
  amount: bigint
) {
  // Check health before borrowing
  const health = await getAccountHealth();
  const WAD = BigInt('1000000000000000000');
  if (health.healthFactor < WAD * 3n / 2n) {
    throw new Error(`Health factor too low: ${health.healthFactor}. Must be >= 1.5 WAD before borrowing.`);
  }

  const { request } = await publicClient.simulateContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'borrow',
    args: [tokenAddress, amount, 2, 0, account.address],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

5. Repay Debt

Pass maxUint256 as amount to repay all outstanding debt. Always approve exact or maxUint256 first.

async function repayDebt(
  tokenAddress: `0x${string}`,
  amount: bigint // use maxUint256 to repay all
) {
  // Approve the repay amount (use the exact amount or maxUint256)
  await approveToken(tokenAddress, POOL_ADDRESS, amount);

  const { request } = await publicClient.simulateContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'repay',
    args: [tokenAddress, amount, 2, account.address],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

// Repay all debt:
// import { maxUint256 } from 'viem';
// await repayDebt(tokenAddress, maxUint256);

6. Withdraw Supplied Assets

async function withdrawAsset(
  tokenAddress: `0x${string}`,
  amount: bigint // use maxUint256 to withdraw all
) {
  const { request } = await publicClient.simulateContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'withdraw',
    args: [tokenAddress, amount, account.address],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

7. Check Account Health

healthFactor is returned in WAD (1e18). A value > 1e18 is safe; < 1e18 means the position can be liquidated.

async function getAccountHealth() {
  const data = await publicClient.readContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'getUserAccountData',
    args: [account.address],
  });
  return {
    totalCollateralBase:         data[0],
    totalDebtBase:               data[1],
    availableBorrowsBase:        data[2],
    currentLiquidationThreshold: data[3],
    ltv:                         data[4],
    healthFactor:                data[5],
  };
}

8. Get Reserve (Market) Data

async function getReserveData(tokenAddress: `0x${string}`) {
  const data = await publicClient.readContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'getReserveData',
    args: [tokenAddress],
  });
  // currentLiquidityRate and currentVariableBorrowRate are in RAY (1e27 units)
  // APY ≈ rate / 1e27 * 100
  return {
    supplyRateRAY:  data.currentLiquidityRate,
    borrowRateRAY:  data.currentVariableBorrowRate,
    aTokenAddress:  data.aTokenAddress,
  };
}

Compound V2 Protocol — Ethereum & Base

gToken (CToken) ABI

const GTOKEN_ABI = [
  // mint — supply underlying, receive gTokens
  {
    name: 'mint',
    type: 'function',
    inputs: [{ name: 'mintAmount', type: 'uint256' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // borrow
  {
    name: 'borrow',
    type: 'function',
    inputs: [{ name: 'borrowAmount', type: 'uint256' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // repayBorrow
  {
    name: 'repayBorrow',
    type: 'function',
    inputs: [{ name: 'repayAmount', type: 'uint256' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // redeemUnderlying — withdraw
  {
    name: 'redeemUnderlying',
    type: 'function',
    inputs: [{ name: 'redeemAmount', type: 'uint256' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // underlying — get underlying token address
  {
    name: 'underlying',
    type: 'function',
    inputs: [],
    outputs: [{ name: '', type: 'address' }],
    stateMutability: 'view',
  },
  // supplyRatePerBlock
  {
    name: 'supplyRatePerBlock',
    type: 'function',
    inputs: [],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  // borrowRatePerBlock
  {
    name: 'borrowRatePerBlock',
    type: 'function',
    inputs: [],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  // balanceOf
  {
    name: 'balanceOf',
    type: 'function',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  // borrowBalanceCurrent
  {
    name: 'borrowBalanceCurrent',
    type: 'function',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
] as const;

Comptroller ABI

const COMPTROLLER_ABI = [
  // enterMarkets — enable assets as collateral
  {
    name: 'enterMarkets',
    type: 'function',
    inputs: [{ name: 'cTokens', type: 'address[]' }],
    outputs: [{ name: '', type: 'uint256[]' }],
    stateMutability: 'nonpayable',
  },
  // getAccountLiquidity
  {
    name: 'getAccountLiquidity',
    type: 'function',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [
      { name: 'error', type: 'uint256' },
      { name: 'liquidity', type: 'uint256' },
      { name: 'shortfall', type: 'uint256' },
    ],
    stateMutability: 'view',
  },
  // markets — get market info
  {
    name: 'markets',
    type: 'function',
    inputs: [{ name: 'cToken', type: 'address' }],
    outputs: [
      { name: 'isListed', type: 'bool' },
      { name: 'collateralFactorMantissa', type: 'uint256' },
      { name: 'isComped', type: 'bool' },
    ],
    stateMutability: 'view',
  },
] as const;

Compound Agent Operations

1. Get Underlying Token Address

async function getUnderlyingToken(
  gTokenAddress: `0x${string}`
): Promise<`0x${string}`> {
  return publicClient.readContract({
    address: gTokenAddress,
    abi: GTOKEN_ABI,
    functionName: 'underlying',
  });
}

2. Enable Market as Collateral (enterMarkets)

Must be called before borrowing to enable a gToken as collateral.

async function enableCollateral(
  comptrollerAddress: `0x${string}`,
  gTokenAddress: `0x${string}`
) {
  const { request } = await publicClient.simulateContract({
    address: comptrollerAddress,
    abi: COMPTROLLER_ABI,
    functionName: 'enterMarkets',
    args: [[gTokenAddress]],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

3. Supply (Mint gTokens)

async function compoundSupply(
  gTokenAddress: `0x${string}`,
  amount: bigint
) {
  const underlyingAddress = await getUnderlyingToken(gTokenAddress);

  // Approve the gToken contract to spend underlying tokens
  await approveToken(underlyingAddress, gTokenAddress, amount);

  const { request, result } = await publicClient.simulateContract({
    address: gTokenAddress,
    abi: GTOKEN_ABI,
    functionName: 'mint',
    args: [amount],
    account,
  });

  // Compound V2 returns an error code — 0 means success
  if (result !== 0n) {
    throw new Error(`Compound mint failed with error code: ${result}`);
  }

  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

4. Borrow

Check account liquidity before borrowing. liquidity > 0 and shortfall === 0 required.

async function compoundBorrow(
  comptrollerAddress: `0x${string}`,
  gTokenAddress: `0x${string}`,
  amount: bigint
) {
  // Check liquidity before borrowing
  const [error, liquidity, shortfall] = await publicClient.readContract({
    address: comptrollerAddress,
    abi: COMPTROLLER_ABI,
    functionName: 'getAccountLiquidity',
    args: [account.address],
  });

  if (error !== 0n) throw new Error(`Comptroller error: ${error}`);
  if (shortfall > 0n) throw new Error('Account has shortfall — cannot borrow');
  if (liquidity === 0n) throw new Error('No available liquidity to borrow');

  const { request } = await publicClient.simulateContract({
    address: gTokenAddress,
    abi: GTOKEN_ABI,
    functionName: 'borrow',
    args: [amount],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

5. Repay

async function compoundRepay(
  gTokenAddress: `0x${string}`,
  amount: bigint
) {
  const underlyingAddress = await getUnderlyingToken(gTokenAddress);

  // Approve exact repay amount
  await approveToken(underlyingAddress, gTokenAddress, amount);

  const { request } = await publicClient.simulateContract({
    address: gTokenAddress,
    abi: GTOKEN_ABI,
    functionName: 'repayBorrow',
    args: [amount],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

6. Withdraw (Redeem)

async function compoundWithdraw(
  gTokenAddress: `0x${string}`,
  amount: bigint
) {
  const { request } = await publicClient.simulateContract({
    address: gTokenAddress,
    abi: GTOKEN_ABI,
    functionName: 'redeemUnderlying',
    args: [amount],
    account,
  });
  const hash = await walletClient.writeContract(request);
  return publicClient.waitForTransactionReceipt({ hash });
}

7. Check Account Liquidity

async function getCompoundAccountHealth(
  comptrollerAddress: `0x${string}`
) {
  const [error, liquidity, shortfall] = await publicClient.readContract({
    address: comptrollerAddress,
    abi: COMPTROLLER_ABI,
    functionName: 'getAccountLiquidity',
    args: [account.address],
  });
  return { error, liquidity, shortfall };
}

8. Get Market Rates

async function getCompoundMarketRates(gTokenAddress: `0x${string}`) {
  const [supplyRatePerBlock, borrowRatePerBlock] = await Promise.all([
    publicClient.readContract({
      address: gTokenAddress,
      abi: GTOKEN_ABI,
      functionName: 'supplyRatePerBlock',
    }),
    publicClient.readContract({
      address: gTokenAddress,
      abi: GTOKEN_ABI,
      functionName: 'borrowRatePerBlock',
    }),
  ]);

  // Approximate APY (assuming ~2,102,400 blocks/year on Ethereum)
  const BLOCKS_PER_YEAR = 2102400n;
  const MANTISSA = 10n ** 18n;
  const supplyAPY = (supplyRatePerBlock * BLOCKS_PER_YEAR * 100n) / MANTISSA;
  const borrowAPY = (borrowRatePerBlock * BLOCKS_PER_YEAR * 100n) / MANTISSA;

  return { supplyRatePerBlock, borrowRatePerBlock, supplyAPY, borrowAPY };
}

Key Safety Rules for Agents

  1. Always check health before borrowing — Aave V3: health factor > 1.5 (1.5 × 10^18 WAD); Compound: liquidity > 0, shortfall == 0.
  2. Approve exact amount before supply/mint and repay/repayBorrow; never use open-ended allowances in production.
  3. On Aave V3: maxUint256 in repay() repays all debt; ERC-20 approval must use the precise repay amount (or maxUint256 when clearing all debt).
  4. On Compound: call enterMarkets() before borrowing; check return values — 0 = success, non-zero = error code.
  5. Simulate before sending — always use publicClient.simulateContract to catch revert reasons before submitting transactions.
  6. Wait for receipt — call publicClient.waitForTransactionReceipt({ hash }) before assuming success.
  7. Never log or commit private keysAGENT_PRIVATE_KEY is sensitive; load it from environment only.
  8. Validate amounts — ensure amounts are > 0 and <= available balance/collateral before transacting.

Simulate Before Send Pattern

// Always simulate first to get revert messages before spending gas
try {
  const { request } = await publicClient.simulateContract({
    address: POOL_ADDRESS,
    abi: GLEND_POOL_ABI,
    functionName: 'supply',
    args: [tokenAddress, amount, account.address, 0],
    account,
  });
  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  return receipt;
} catch (err) {
  // simulateContract throws with a descriptive reason if the tx would revert
  throw new Error(`Supply simulation failed: ${(err as Error).message}`);
}

Typical Agent Workflows

Pharos Testnet (Aave V3)

1. mintTestTokens       — get test tokens from faucet
2. getAccountHealth     — confirm starting state
3. supplyAsset          — deposit collateral
4. getAccountHealth     — verify health factor improved
5. borrowAsset          — borrow against collateral (check HF >= 1.5 first)
6. repayDebt            — repay borrowed amount
7. withdrawAsset        — withdraw supplied collateral

Ethereum & Base (Compound V2)

1. compoundSupply       — supply underlying tokens, receive gTokens
2. enableCollateral     — call enterMarkets to enable supplied asset as collateral
3. getCompoundAccountHealth — verify liquidity > 0 and shortfall == 0
4. compoundBorrow       — borrow against collateral
5. compoundRepay        — repay borrowed amount
6. compoundWithdraw     — redeem underlying tokens

Troubleshooting

Problem Likely Cause Fix
"insufficient collateral" Health factor too low or no collateral supplied Supply more collateral first; check getAccountHealth()
"health factor too low" Attempted borrow would breach threshold Reduce borrow amount or add more collateral
Compound borrow fails silently enterMarkets not called Call enableCollateral() before compoundBorrow()
Gas estimation fails Contract would revert Use simulateContract to get the revert reason
Transaction pending forever Gas price too low or network congestion Increase maxFeePerGas / maxPriorityFeePerGas in client
Approval rejected Previous allowance non-zero (some tokens) Approve 0 first, then approve the desired amount
"No available liquidity" Market utilization 100% Try a different token or reduce borrow amount
Wrong chain GLEND_CHAIN_ID mismatch Set GLEND_CHAIN_ID to 688688, 1, or 8453 accordingly

Resources

Related skills
Installs
1
GitHub Stars
5
First Seen
Mar 21, 2026