Glend Agent Skill
This file provides instructions for AI agents interacting with the Glend DeFi Lend & Borrow platform by GemachDAO.
What is Glend?
Glend is a decentralized lending and borrowing protocol deployed on multiple EVM chains. It runs as an Aave V3 fork on Pharos Testnet and as a Compound V2 fork (gTokens/tTokens) on Ethereum and Base. Agents can:
- 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
All on-chain interactions are performed with viem. Contract addresses and chain configuration are pre-loaded — agents only need a wallet private key to start.
- Glend App:
https://glendv2.gemach.io
Environment Variables
AGENT_PRIVATE_KEY=<private_key> # Agent wallet private key (never commit!)
The only required variable is AGENT_PRIVATE_KEY. All contract addresses, RPC endpoints, and chain configuration are embedded below.
Optional overrides (advanced — use only if connecting to a custom deployment):
GLEND_RPC_URL=<rpc_url> # Override the default RPC endpoint
GLEND_POOL_ADDRESS=<address> # Override the default pool contract
GLEND_CHAIN_ID=<chain_id> # Override the default chain ID
Supported Deployments
Pharos Testnet (default)
| Property | Value |
|---|---|
| Chain ID | 688688 |
| RPC URL | https://testnet.dplabs-internal.com |
| Block Explorer | https://testnet.pharosscan.xyz |
| Native Token | PHRS |
| Pool (Lending Pool) | 0xe838eb8011297024bca9c09d4e83e2d3cd74b7d0 |
| WETHGateway | 0xa8e550710bf113db6a1b38472118b8d6d5176d12 |
| Faucet | 0x2e9d89d372837f71cb529e5ba85bfbc1785c69cd |
Token Registry — Pharos Testnet
| Token | Address | Decimals |
|---|---|---|
| USDT | 0x0b00fb1f513e02399667fba50772b21f34c1b5d9 | 6 |
| USDC | 0x48249feeb47a8453023f702f15cf00206eebdf08 | 6 |
| BTC | 0xa4a967fc7cf0e9815bf5c2700a055813628b65be | 8 |
Ethereum Mainnet (Compound fork)
| Property | Value |
|---|---|
| Chain ID | 1 |
| Protocol | Compound V2 fork (tTokens) |
| RPC URL | https://eth.llamarpc.com |
| Block Explorer | https://etherscan.io |
| Native Token | ETH |
| Comptroller (Unitroller) | 0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58 |
| PriceOracle | 0x97f602E17ed4e765a6968f295Bdc3F6b4c1Ef93b |
| CompoundLens | 0x47bdd2Ebfd5081c72adb4238E04559576F2c9ba3 |
Market Tokens — Ethereum Mainnet
| Market | Address | Collateral Factor |
|---|---|---|
| tUSDT | 0xfd7E506495fd921a17802Cf523279f01550BE8b6 | 75% |
| tUSDC | 0x1C5215F2fb5417BdF9D93339b0caf20222f210f3 | 75% |
| tETH | 0x6baeCC06B2faFD651B095ab3b7882AEe6EC4369D | 80% |
| tcbBTC | 0xF4faD7E54bF68344906C2b60fBCDB031cdeaDB52 | 70% |
| tstETH | 0x6e9acC9D6ea3edE1acAE7Eeb2Be2dE1F8572Bc82 | 78% |
Base (Compound fork)
| Property | Value |
|---|---|
| Chain ID | 8453 |
| Protocol | Compound V2 fork (gTokens) |
| RPC URL | https://mainnet.base.org |
| Block Explorer | https://basescan.org |
| Native Token | ETH |
| Comptroller (Unitroller) | 0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58 |
| PriceOracle | 0x97f602E17ed4e765a6968f295Bdc3F6b4c1Ef93b |
| CompoundLens | 0x41d9071C885da8dCa042E05AA66D7D5034383C53 |
Market Tokens — Base
| Market | Address |
|---|---|
| gUSDT | 0x6eAB9a4f7fDE8C1Ef0F62DA16549C80Bb7b7f853 |
| gUSDC | 0xfd7E506495fd921a17802Cf523279f01550BE8b6 |
| gETH | 0x1C5215F2fb5417BdF9D93339b0caf20222f210f3 |
| gcbBTC | 0x920D3D27b17DCC16A7263d2ab176ac68C8385cd3 |
Getting Test Tokens (Faucet)
Before interacting with the lending pool, agents need test tokens. Use the on-chain faucet to mint tokens:
const FAUCET_ABI = [
{
name: "mint",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "_asset", type: "address" },
{ name: "_account", type: "address" },
{ name: "_amount", type: "uint256" },
],
outputs: [],
},
] as const;
const FAUCET_ADDRESS = "0x2e9d89d372837f71cb529e5ba85bfbc1785c69cd" as const;
async function mintTestTokens(
tokenAddress: `0x${string}`,
amount: bigint,
) {
const hash = await walletClient.writeContract({
address: FAUCET_ADDRESS,
abi: FAUCET_ABI,
functionName: "mint",
args: [tokenAddress, account.address, amount],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Minted test tokens — tx: ${hash}`);
}
// Example: mint 1000 USDC (6 decimals)
await mintTestTokens("0x48249feeb47a8453023f702f15cf00206eebdf08", parseUnits("1000", 6));
Aave V3 Protocol — Pharos Testnet
All agent operations on Pharos Testnet target the Glend Pool contract (Aave V3 fork). Interact with it via viem's writeContract / readContract.
Minimal ABI (key functions)
export const GLEND_POOL_ABI = [
// ── Read ──────────────────────────────────────────────────────────────────
{
name: "getUserAccountData",
type: "function",
stateMutability: "view",
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" },
],
},
{
name: "getReserveData",
type: "function",
stateMutability: "view",
inputs: [{ name: "asset", type: "address" }],
outputs: [
{ name: "configuration", type: "uint256" },
{ name: "liquidityIndex", type: "uint128" },
{ name: "variableBorrowIndex", type: "uint128" },
{ name: "currentLiquidityRate", type: "uint128" },
{ name: "currentVariableBorrowRate", type: "uint128" },
{ name: "lastUpdateTimestamp", type: "uint40" },
{ name: "aTokenAddress", type: "address" },
{ name: "variableDebtTokenAddress", type: "address" },
{ name: "interestRateStrategyAddress", type: "address" },
{ name: "id", type: "uint8" },
],
},
// ── Write ─────────────────────────────────────────────────────────────────
{
name: "supply",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "onBehalfOf", type: "address" },
{ name: "referralCode", type: "uint16" },
],
outputs: [],
},
{
name: "withdraw",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" }, // use MaxUint256 to withdraw all
{ name: "to", type: "address" },
],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "borrow",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "interestRateMode", type: "uint256" }, // 2 = variable
{ name: "referralCode", type: "uint16" },
{ name: "onBehalfOf", type: "address" },
],
outputs: [],
},
{
name: "repay",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" }, // use MaxUint256 to repay all
{ name: "interestRateMode", type: "uint256" }, // 2 = variable
{ name: "onBehalfOf", type: "address" },
],
outputs: [{ name: "", type: "uint256" }],
},
] as const;
Agent Operations
1 · Set up viem clients
import {
createPublicClient,
createWalletClient,
http,
parseUnits,
formatUnits,
maxUint256,
defineChain,
} from "viem";
import { mainnet, base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import type { Chain } from "viem";
// ── Pharos Testnet chain definition ────────────────────────────────────────
const pharosTestnet = defineChain({
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" },
},
testnet: true,
});
// ── All supported chains ───────────────────────────────────────────────────
const GLEND_CHAINS: Record<number, Chain> = {
688688: pharosTestnet,
1: mainnet,
8453: base,
};
// ── Aave V3 deployment (Pharos Testnet) ────────────────────────────────────
const PHAROS_DEPLOYMENT = {
pool: "0xe838eb8011297024bca9c09d4e83e2d3cd74b7d0" as `0x${string}`,
wethGateway: "0xa8e550710bf113db6a1b38472118b8d6d5176d12" as `0x${string}`,
faucet: "0x2e9d89d372837f71cb529e5ba85bfbc1785c69cd" as `0x${string}`,
tokens: {
USDT: { address: "0x0b00fb1f513e02399667fba50772b21f34c1b5d9" as `0x${string}`, decimals: 6 },
USDC: { address: "0x48249feeb47a8453023f702f15cf00206eebdf08" as `0x${string}`, decimals: 6 },
BTC: { address: "0xa4a967fc7cf0e9815bf5c2700a055813628b65be" as `0x${string}`, decimals: 8 },
},
};
// ── Compound fork deployment (Ethereum Mainnet) ────────────────────────────
const ETH_DEPLOYMENT = {
comptroller: "0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58" as `0x${string}`,
priceOracle: "0x97f602E17ed4e765a6968f295Bdc3F6b4c1Ef93b" as `0x${string}`,
compoundLens: "0x47bdd2Ebfd5081c72adb4238E04559576F2c9ba3" as `0x${string}`,
markets: {
tUSDT: { address: "0xfd7E506495fd921a17802Cf523279f01550BE8b6" as `0x${string}` },
tUSDC: { address: "0x1C5215F2fb5417BdF9D93339b0caf20222f210f3" as `0x${string}` },
tETH: { address: "0x6baeCC06B2faFD651B095ab3b7882AEe6EC4369D" as `0x${string}` },
tcbBTC: { address: "0xF4faD7E54bF68344906C2b60fBCDB031cdeaDB52" as `0x${string}` },
tstETH: { address: "0x6e9acC9D6ea3edE1acAE7Eeb2Be2dE1F8572Bc82" as `0x${string}` },
},
};
// ── Compound fork deployment (Base) ────────────────────────────────────────
const BASE_DEPLOYMENT = {
comptroller: "0x4a4c2A16b58bD63d37e999fDE50C2eBfE3182D58" as `0x${string}`,
priceOracle: "0x97f602E17ed4e765a6968f295Bdc3F6b4c1Ef93b" as `0x${string}`,
compoundLens: "0x41d9071C885da8dCa042E05AA66D7D5034383C53" as `0x${string}`,
markets: {
gUSDT: { address: "0x6eAB9a4f7fDE8C1Ef0F62DA16549C80Bb7b7f853" as `0x${string}` },
gUSDC: { address: "0xfd7E506495fd921a17802Cf523279f01550BE8b6" as `0x${string}` },
gETH: { address: "0x1C5215F2fb5417BdF9D93339b0caf20222f210f3" as `0x${string}` },
gcbBTC: { address: "0x920D3D27b17DCC16A7263d2ab176ac68C8385cd3" as `0x${string}` },
},
};
// ── Resolve configuration ──────────────────────────────────────────────────
const CHAIN_ID = Number(process.env.GLEND_CHAIN_ID ?? 688688);
const chain = GLEND_CHAINS[CHAIN_ID];
if (!chain) {
throw new Error(`Unsupported chain ID: ${CHAIN_ID}. Supported: ${Object.keys(GLEND_CHAINS).join(", ")}`);
}
const RPC_URL = process.env.GLEND_RPC_URL ?? chain.rpcUrls.default.http[0];
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY as `0x${string}`;
const account = privateKeyToAccount(PRIVATE_KEY);
const publicClient = createPublicClient({ chain, transport: http(RPC_URL) });
const walletClient = createWalletClient({ account, chain, transport: http(RPC_URL) });
// ── Pharos helpers ─────────────────────────────────────────────────────────
const POOL_ADDRESS = (process.env.GLEND_POOL_ADDRESS ?? PHAROS_DEPLOYMENT.pool) as `0x${string}`;
function getToken(symbol: string) {
const token = PHAROS_DEPLOYMENT.tokens[symbol as keyof typeof PHAROS_DEPLOYMENT.tokens];
if (!token) throw new Error(`Unknown token: ${symbol}. Available: ${Object.keys(PHAROS_DEPLOYMENT.tokens).join(", ")}`);
return token;
}
// ── Compound helpers ───────────────────────────────────────────────────────
function getDeployment() {
if (CHAIN_ID === 1) return ETH_DEPLOYMENT;
if (CHAIN_ID === 8453) return BASE_DEPLOYMENT;
throw new Error(`Chain ${CHAIN_ID} does not have a Compound V2 deployment. Supported: Ethereum (1), Base (8453). For Aave V3, use Pharos (688688).`);
}
function getComptroller(): `0x${string}` {
const deployment = getDeployment();
if (!deployment.comptroller) {
throw new Error(`Comptroller address not configured for chain ${CHAIN_ID}. Market operations (mint, borrow, etc.) still work directly.`);
}
return deployment.comptroller;
}
function getMarket(symbol: string) {
const deployment = getDeployment();
const market = deployment.markets[symbol as keyof typeof deployment.markets];
if (!market) throw new Error(`Unknown market: ${symbol}. Available: ${Object.keys(deployment.markets).join(", ")}`);
return market;
}
2 · Approve ERC-20 before supply or repay
import { erc20Abi } from "viem";
async function approveToken(tokenAddress: `0x${string}`, amount: bigint) {
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "approve",
args: [POOL_ADDRESS, amount],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log("Approved:", hash);
}
3 · Supply (lend) assets
async function supplyAsset(
tokenAddress: `0x${string}`,
humanAmount: string,
decimals: number,
) {
const amount = parseUnits(humanAmount, decimals);
await approveToken(tokenAddress, amount);
const hash = await walletClient.writeContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "supply",
args: [tokenAddress, amount, account.address, 0],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Supplied ${humanAmount} — tx: ${hash}`);
}
4 · Borrow assets
async function borrowAsset(
tokenAddress: `0x${string}`,
humanAmount: string,
decimals: number,
) {
const amount = parseUnits(humanAmount, decimals);
const hash = await walletClient.writeContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "borrow",
args: [tokenAddress, amount, 2n, 0, account.address], // 2 = variable rate
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Borrowed ${humanAmount} — tx: ${hash}`);
}
5 · Repay debt
import { erc20Abi } from "viem";
/** Fetch the current variable debt balance for a token. */
async function getDebtBalance(
variableDebtTokenAddress: `0x${string}`,
userAddress: `0x${string}`,
): Promise<bigint> {
return publicClient.readContract({
address: variableDebtTokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [userAddress],
});
}
async function repayDebt(
tokenAddress: `0x${string}`,
variableDebtTokenAddress: `0x${string}`,
humanAmount: string | "all",
decimals: number,
) {
// Determine the repay amount for the Pool call (maxUint256 = repay all debt)
const repayAmount = humanAmount === "all" ? maxUint256 : parseUnits(humanAmount, decimals);
// For the ERC-20 approval, always approve the exact token amount being transferred.
// When repaying "all", query the live debt balance so the approval is not excessive.
const approvalAmount =
humanAmount === "all"
? await getDebtBalance(variableDebtTokenAddress, account.address)
: parseUnits(humanAmount, decimals);
await approveToken(tokenAddress, approvalAmount);
const hash = await walletClient.writeContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "repay",
args: [tokenAddress, repayAmount, 2n, account.address],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Repaid — tx: ${hash}`);
}
6 · Withdraw supplied assets
async function withdrawAsset(
tokenAddress: `0x${string}`,
humanAmount: string | "all",
decimals: number,
) {
const amount = humanAmount === "all" ? maxUint256 : parseUnits(humanAmount, decimals);
const hash = await walletClient.writeContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "withdraw",
args: [tokenAddress, amount, account.address],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Withdrew — tx: ${hash}`);
}
7 · Check account health
async function getAccountHealth(userAddress: `0x${string}`) {
const data = await publicClient.readContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "getUserAccountData",
args: [userAddress],
});
const [
totalCollateralBase,
totalDebtBase,
availableBorrowsBase,
currentLiquidationThreshold,
ltv,
healthFactor,
] = data;
// healthFactor is scaled by 1e18 — values below 1e18 risk liquidation
const hf = formatUnits(healthFactor, 18);
console.log({
totalCollateral: formatUnits(totalCollateralBase, 8),
totalDebt: formatUnits(totalDebtBase, 8),
availableToBorrow: formatUnits(availableBorrowsBase, 8),
healthFactor: hf,
atRisk: Number(hf) < 1.1,
});
return { totalCollateralBase, totalDebtBase, availableBorrowsBase, healthFactor };
}
8 · Get reserve (market) data
async function getMarketData(tokenAddress: `0x${string}`) {
const data = await publicClient.readContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "getReserveData",
args: [tokenAddress],
});
// Rates are per-second values in ray units (1e27).
// Annualise using compound-interest: APY = (1 + ratePerSecond)^31_536_000 - 1
const SECONDS_PER_YEAR = 31_536_000n;
const RAY = 10n ** 27n;
function rayToAPY(rayRate: bigint): string {
// Use floating-point for the exponentiation
const ratePerSecond = Number(rayRate) / Number(RAY);
const apy = (Math.pow(1 + ratePerSecond, Number(SECONDS_PER_YEAR)) - 1) * 100;
return `${apy.toFixed(2)}%`;
}
const supplyAPY = rayToAPY(data.currentLiquidityRate);
const borrowAPY = rayToAPY(data.currentVariableBorrowRate);
console.log({ supplyAPY, borrowAPY });
return data;
}
Compound V2 Protocol — Ethereum & Base
On Ethereum Mainnet and Base, Glend uses a Compound V2 fork architecture. Instead of a single Pool contract, agents interact with individual gToken/tToken market contracts and a Comptroller for collateral management.
These ABIs match the standard Compound V2 interface — the same functions used by the Glend front-end at glendv2.gemach.io.
gToken / tToken ABI (Compound V2 CToken interface)
export const GTOKEN_ABI = [
// ── Supply: approve underlying ERC-20 to gToken, then call mint ──────────
{
name: "mint",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "mintAmount", type: "uint256" }],
outputs: [{ name: "", type: "uint256" }], // 0 = success
},
// ── Borrow ───────────────────────────────────────────────────────────────
{
name: "borrow",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "borrowAmount", type: "uint256" }],
outputs: [{ name: "", type: "uint256" }], // 0 = success
},
// ── Repay ────────────────────────────────────────────────────────────────
{
name: "repayBorrow",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "repayAmount", type: "uint256" }],
outputs: [{ name: "", type: "uint256" }], // 0 = success
},
// ── Withdraw (by underlying amount) ──────────────────────────────────────
{
name: "redeemUnderlying",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "redeemAmount", type: "uint256" }],
outputs: [{ name: "", type: "uint256" }], // 0 = success
},
// ── Withdraw (by gToken amount) ──────────────────────────────────────────
{
name: "redeem",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "redeemTokens", type: "uint256" }],
outputs: [{ name: "", type: "uint256" }], // 0 = success
},
// ── Read ──────────────────────────────────────────────────────────────────
{
name: "underlying",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "address" }],
},
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "balanceOfUnderlying",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "borrowBalanceCurrent",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "getAccountSnapshot",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [
{ name: "error", type: "uint256" },
{ name: "tokenBalance", type: "uint256" },
{ name: "borrowBalance", type: "uint256" },
{ name: "exchangeRateMantissa", type: "uint256" },
],
},
{
name: "exchangeRateCurrent",
type: "function",
stateMutability: "nonpayable",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "totalBorrows",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "totalSupply",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "getCash",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "supplyRatePerBlock",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "borrowRatePerBlock",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "decimals",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint8" }],
},
{
name: "symbol",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "string" }],
},
] as const;
Comptroller ABI (Compound V2 Comptroller interface)
export const COMPTROLLER_ABI = [
{
name: "enterMarkets",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "gTokens", type: "address[]" }],
outputs: [{ name: "", type: "uint256[]" }], // 0 = success for each
},
{
name: "exitMarket",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "gTokenAddress", type: "address" }],
outputs: [{ name: "", type: "uint256" }], // 0 = success
},
{
name: "getAccountLiquidity",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [
{ name: "error", type: "uint256" }, // 0 = no error
{ name: "liquidity", type: "uint256" }, // excess liquidity (USD, 18 decimals)
{ name: "shortfall", type: "uint256" }, // shortfall (USD, 18 decimals)
],
},
{
name: "markets",
type: "function",
stateMutability: "view",
inputs: [{ name: "gToken", type: "address" }],
outputs: [
{ name: "isListed", type: "bool" },
{ name: "collateralFactorMantissa", type: "uint256" },
],
},
{
name: "getAllMarkets",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "address[]" }],
},
{
name: "getAssetsIn",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "address[]" }],
},
{
name: "oracle",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "address" }],
},
] as const;
Compound Agent Operations
1 · Get the underlying token address
async function getUnderlying(gTokenAddress: `0x${string}`): Promise<`0x${string}`> {
return publicClient.readContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "underlying",
}) as Promise<`0x${string}`>;
}
2 · Enable a market as collateral (enterMarkets)
Before borrowing, the agent must enable supplied markets as collateral via the Comptroller:
// Use the Comptroller address from the deployment config
const COMPTROLLER = getComptroller();
async function enableCollateral(gTokenAddresses: `0x${string}`[]) {
const hash = await walletClient.writeContract({
address: COMPTROLLER,
abi: COMPTROLLER_ABI,
functionName: "enterMarkets",
args: [gTokenAddresses],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Enabled collateral — tx: ${hash}`);
}
3 · Supply (mint gTokens)
import { erc20Abi } from "viem";
async function compoundSupply(
gTokenAddress: `0x${string}`,
humanAmount: string,
decimals: number,
) {
const amount = parseUnits(humanAmount, decimals);
const underlying = await getUnderlying(gTokenAddress);
// Approve the underlying token to the gToken contract
const approveHash = await walletClient.writeContract({
address: underlying,
abi: erc20Abi,
functionName: "approve",
args: [gTokenAddress, amount],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
// Mint gTokens (supply)
const hash = await walletClient.writeContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "mint",
args: [amount],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Supplied ${humanAmount} — tx: ${hash}`);
}
4 · Borrow
async function compoundBorrow(
gTokenAddress: `0x${string}`,
humanAmount: string,
decimals: number,
) {
const amount = parseUnits(humanAmount, decimals);
const hash = await walletClient.writeContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "borrow",
args: [amount],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Borrowed ${humanAmount} — tx: ${hash}`);
}
5 · Repay
async function compoundRepay(
gTokenAddress: `0x${string}`,
humanAmount: string,
decimals: number,
) {
const amount = parseUnits(humanAmount, decimals);
const underlying = await getUnderlying(gTokenAddress);
// Approve the underlying token
const approveHash = await walletClient.writeContract({
address: underlying,
abi: erc20Abi,
functionName: "approve",
args: [gTokenAddress, amount],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
const hash = await walletClient.writeContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "repayBorrow",
args: [amount],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Repaid ${humanAmount} — tx: ${hash}`);
}
6 · Withdraw (redeem)
async function compoundWithdraw(
gTokenAddress: `0x${string}`,
humanAmount: string,
decimals: number,
) {
const amount = parseUnits(humanAmount, decimals);
const hash = await walletClient.writeContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "redeemUnderlying",
args: [amount],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(`Withdrew ${humanAmount} — tx: ${hash}`);
}
7 · Check account liquidity
async function getCompoundAccountHealth(userAddress: `0x${string}`) {
const [error, liquidity, shortfall] = await publicClient.readContract({
address: COMPTROLLER,
abi: COMPTROLLER_ABI,
functionName: "getAccountLiquidity",
args: [userAddress],
});
console.log({
error: Number(error),
excessLiquidity: formatUnits(liquidity, 18),
shortfall: formatUnits(shortfall, 18),
atRisk: shortfall > 0n,
});
return { error, liquidity, shortfall };
}
8 · Get market rates
async function getCompoundMarketRates(gTokenAddress: `0x${string}`) {
const supplyRate = await publicClient.readContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "supplyRatePerBlock",
});
const borrowRate = await publicClient.readContract({
address: gTokenAddress,
abi: GTOKEN_ABI,
functionName: "borrowRatePerBlock",
});
// Convert per-block rate to APY (assuming ~12s blocks, ~2628000 blocks/year)
const BLOCKS_PER_YEAR = 2_628_000;
const supplyAPY = (Math.pow(1 + Number(supplyRate) / 1e18, BLOCKS_PER_YEAR) - 1) * 100;
const borrowAPY = (Math.pow(1 + Number(borrowRate) / 1e18, BLOCKS_PER_YEAR) - 1) * 100;
console.log({ supplyAPY: `${supplyAPY.toFixed(2)}%`, borrowAPY: `${borrowAPY.toFixed(2)}%` });
return { supplyRate, borrowRate };
}
Key Safety Rules for Agents
- Always check health/liquidity before borrowing — on Aave V3, keep health factor above 1.5; on Compound, ensure
liquidity > 0andshortfall == 0. - Approve the exact amount before
supply/mintandrepay/repayBorrow; never set an open-ended allowance. - On Aave V3:
maxUint256inrepay()means repay all debt — the ERC-20 approval must still use the precise token amount. - On Compound: call
enterMarkets()on the Comptroller before borrowing, otherwise supplied assets won't count as collateral. - Simulate transactions with
publicClient.simulateContractbefore sending:
// Aave V3 example
const { request } = await publicClient.simulateContract({
address: POOL_ADDRESS,
abi: GLEND_POOL_ABI,
functionName: "borrow",
args: [tokenAddress, amount, 2n, 0, account.address],
account,
});
const hash = await walletClient.writeContract(request);
- Wait for receipt before assuming a transaction succeeded.
- Never log or commit private keys; read them from environment variables only.
- Check return values on Compound —
mint,borrow,repayBorrow, andredeemreturn0on success; non-zero means error.
Typical Agent Workflows
Pharos Testnet (Aave V3)
1. mintTestTokens(getToken("USDC").address, parseUnits("1000", 6))
→ get test USDC from faucet
2. getAccountHealth(agentAddress)
→ confirm healthFactor > 1.5 before borrowing
3. supplyAsset(getToken("USDC").address, "1000", 6)
→ deposit 1000 USDC as collateral
4. getAccountHealth(agentAddress)
→ verify collateral registered, check availableBorrowsBase
5. borrowAsset(getToken("USDT").address, "500", 6)
→ borrow 500 USDT against USDC collateral
6. [... use borrowed funds ...]
7. repayDebt(getToken("USDT").address, variableDebtTokenAddress, "all", 6)
→ repay full USDT debt
8. withdrawAsset(getToken("USDC").address, "all", 6)
→ recover USDC collateral
Ethereum & Base (Compound V2)
1. compoundSupply(getMarket("gUSDC").address, "1000", 6)
→ supply 1000 USDC, receive gUSDC tokens
2. enableCollateral([getMarket("gUSDC").address])
→ enable gUSDC as collateral on the Comptroller
3. getCompoundAccountHealth(agentAddress)
→ confirm liquidity > 0 and shortfall == 0
4. compoundBorrow(getMarket("gUSDT").address, "500", 6)
→ borrow 500 USDT against gUSDC collateral
5. [... use borrowed funds ...]
6. compoundRepay(getMarket("gUSDT").address, "500", 6)
→ repay USDT debt
7. compoundWithdraw(getMarket("gUSDC").address, "1000", 6)
→ redeem gUSDC for underlying USDC
Troubleshooting
Aave V3 (Pharos Testnet)
| Error | Likely cause | Fix |
|---|---|---|
CALLER_NOT_IN_WHITELIST | Protocol access control | Confirm the agent wallet is allowed |
HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD | Borrow would make account unsafe | Reduce borrow amount |
NO_ACTIVE_RESERVE | Wrong token address or chain | Use addresses from the Token Registry above |
TRANSFER_AMOUNT_EXCEEDS_BALANCE | Insufficient token balance | Use the faucet to mint test tokens |
| ERC-20 allowance error | approve() not called first | Call approveToken() before supply/repay |
Compound V2 (Ethereum & Base)
| Error / Return Code | Likely cause | Fix |
|---|---|---|
mint returns non-zero | Insufficient balance or allowance | Approve underlying token first |
borrow returns non-zero | Insufficient collateral | Call enterMarkets() or supply more |
COMPTROLLER_REJECTION | Collateral not enabled | Call enableCollateral() before borrowing |
shortfall > 0 | Account is undercollateralized | Repay debt or supply more collateral |
redeemUnderlying returns non-zero | Withdrawal would cause shortfall | Repay debt first |
Resources
- Glend App:
https://glendv2.gemach.io - Pharos Testnet Explorer:
https://testnet.pharosscan.xyz - Ethereum Explorer:
https://etherscan.io - Base Explorer:
https://basescan.org - GemachDAO:
https://github.com/GemachDAO - viem docs:
https://viem.sh