Aave V4 TypeScript Quickstart
AaveKit TypeScript for Aave V4 provides a type-safe, low-level API client for interacting with Aave V4. It offers a lightweight abstraction over the GraphQL API and is ideal for server-to-server communication.
Built with a modular, functional approach inspired by the viem client-actions architecture, the SDK structures functionality into distinct, reusable actions focused on specific protocol features like lending, borrowing, and data retrieval.
Getting Started
To get started, follow the steps below.
1. Install SDK
First, install the @aave/ package using your package manager of choice.
npm install @aave/client@next
yarn add @aave/client@next
pnpm add @aave/client@next
2. Set Up Client
Then, create an Aave instance that will be used to interact with the protocol.
import { AaveClient } from "@aave/client";
export const client = AaveClient.create();
3. Start Building
You can now use the @aave/ to interact with the Aave Protocol. Here’s a basic example to query supported chains:
import { ChainsFilter } from "@aave/client";
import { chains } from "@aave/client/actions";
import { client } from "./client";
// Fetch all chains supported by Aave
const result = await chains(client, {
query: { filter: ChainsFilter.ALL },
});
if (result.isOk()) {
console.log("Chains:", result.value);
} else {
console.error("Error:", result.error);
}
Server-Side Rendering
Aave supports SSR hand-off through the ssr option. Create a server-side client, run your queries, serialize client., then hydrate a browser client with that snapshot.
import { AaveClient, type SSRData, ChainsFilter } from "@aave/client";
import { chains } from "@aave/client/actions";
const client = AaveClient.create({
ssr: { isServer: true },
});
await chains(client, {
query: { filter: ChainsFilter.ALL },
});
const initialState: SSRData = client.extractData();
// Serialize into the HTML: <script>window.__AAVE_SSR_DATA__ = ${JSON.stringify(initialState)}</script>
import { AaveClient, type SSRData } from "@aave/client";
declare global {
interface Window {
__AAVE_SSR_DATA__?: SSRData;
}
}
export const client = AaveClient.create({
ssr: {
isServer: false,
initialState: window.__AAVE_SSR_DATA__,
},
});
If the snapshot arrives after the client is created, call client. to hydrate the cache later.
Display Configuration
The optional display config transforms how assets are presented across queries. Transforms are applied after the cache, so the cache always stores untransformed data.
import { AaveClient } from "@aave/client";
export const client = AaveClient.create({
display: {
// Show wrapped native tokens (e.g. WETH) as the native asset (e.g. ETH)
showWrappedNativeReserveAsNative: true,
// Per-asset overrides, keyed by chain ID and address
assetOverrides: [
{
chainId: 1,
address: "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0",
display: { name: "PT USDe (May 2026)", symbol: "PT USDe", icon: "https://…" },
},
],
},
});
show: whenWrapped Native Reserve As Native true, wrapped native tokens are shown using the native asset’sname,symbol, andiconwithin reserve contexts (Reserve,Hub,Asset Asset). Wallet balance, reward payout, and swap queries are unaffected. The underlyingisflag and tokenWrapped Native Token addressare preserved.asset: per-asset display overrides applied globally across all queries, keyed byOverrides chainandId address. Each entry takes adisplayobject with optionalname,symbol, andicon.
If both settings target the same token, asset takes precedence.
Result Objects
AaveKit handles errors with Result<T, E>, which represents one of two states:
Ok<T>: A successful result containing a value of typeTErr<E>: A failure containing an error of typeE
import { ok, err, Result } from "@aave/client";
function parseId(s: string): Result<number, Error> {
if (/^\d+$/.test(s)) {
return ok(Number(s));
}
return err(new Error("ID must be a number"));
}
With a Result<T, E>, use the is and is methods to check the outcome and narrow the type.
const result: Result<number, Error> = parseId("123");
if (result.isOk()) {
console.log(result.value); // 123
} else {
console.error(result.error); // Error
}
This approach avoids reliance on try/ blocks and promotes predictable, type-safe code by ensuring errors are handled explicitly.
AaveKit builds its Result object on
NeverThrow.
Result is the async, thenable variant of Result<T, E>. Awaiting it resolves to a Result<T, E>.
import { ResultAsync } from "@aave/client";
function fetchUser(id: number): ResultAsync<{ name: string }, Error> {
return ResultAsync.fromPromise(
fetch(`/api/users/${id}`).then((r) => r.json()),
() => new Error("Not found"),
);
}
const result = await fetchUser(1);
if (result.isOk()) {
console.log(result.value); // { name: "John" }
} else {
console.error(result.error); // Error
}
Result objects can be chained:
function divide(a: number, b: number): Result<number, string> {
return b === 0 ? err("Division by zero") : ok(a / b);
}
const result = parseId("42").andThen((num) => divide(num, 2));
if (result.isOk()) {
console.log("Result:", result.value);
} else {
console.error("Error:", result.error);
}
You can also provide a default value if the result is an error:
const value = parseId("invalid").unwrapOr(0); // 0
See the NeverThrow documentation for more information.
Integrations
AaveKit TypeScript includes first-class support for viem, ethers v6, Privy, thirdweb and Turnkey wallets.
Ensure you have viem package installed in your project.
npm install viem@2
yarn add viem@2
pnpm add viem@2
Ensure you have ethers package installed in your project.
npm install ethers
yarn add ethers
pnpm add ethers
Ensure you have viem and @privy-io/ packages installed in your project.
npm install viem@2 @privy-io/server-auth
yarn add viem@2 @privy-io/server-auth
pnpm add viem@2 @privy-io/server-auth
The Aave Privy adapter uses Viem to monitor the transaction status afterwards.
Ensure you have thirdweb installed in your project.
npm install thirdweb@5
yarn add thirdweb@5
pnpm add thirdweb@5
Ensure you have viem, @turnkey/ and @turnkey/ packages installed in your project.
npm install viem@2 @turnkey/viem @turnkey/sdk-server
yarn add viem@2 @turnkey/viem @turnkey/sdk-server
pnpm add viem@2 @turnkey/viem @turnkey/sdk-server
Send Aave Transactions
Among the actions provided by AaveKit TypeScript, some are specifically designed to handle protocol interactions such as supplying, borrowing, withdrawing, repaying, and more.
There are two main types of transaction actions:
- Simple transactions: single-step transactions that can be sent directly to the wallet.
- Complex transactions: transactions that may require prior approvals before they can be executed.
To send transactions, follow the steps below.
1. Import the Helper
First, import the send helper function for the wallet library of your choice.
Import the send helper from the @aave/ entry point.
import { sendWith } from "@aave/client/viem";
Import the send helper from the @aave/ entry point.
import { sendWith } from "@aave/client/ethers";
Import the send helper from the @aave/ entry point.
import { sendWith } from "@aave/client/privy";
Import the send helper from the @aave/ entry point.
import { sendWith } from "@aave/client/thirdweb";
Import the send helper from the @aave/ entry point.
import { sendWith } from "@aave/client/viem";
2. Send the Transaction
Then, use the send helper to send the transaction.
To demonstrate how this helper works, assume we have a transaction action named foobar.
import { foobar } from "@aave/client/actions";
import { client } from "./client";
const result = await foobar(client);
import { AaveClient } from "@aave/client";
export const client = AaveClient.create();
To send the transaction, chain the action with send for your wallet library, then use client. to wait until it’s confirmed and indexed by AaveKit API.
import { foobar } from "@aave/client/actions";
import { sendWith } from "@aave/client/viem";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { client } from "./client";
const wallet = createWalletClient({
account: privateKeyToAccount("<PRIVATE_KEY>"),
transport: http(),
});
const result = await foobar(client)
.andThen(sendWith(wallet))
.andThen(client.waitForTransaction);
import { foobar } from "@aave/client/actions";
import { sendWith } from "@aave/client/ethers";
import { Wallet, getDefaultProvider } from "ethers";
import { client } from "./client";
const wallet = new Wallet("<PRIVATE_KEY>", getDefaultProvider("mainnet"));
const result = await foobar(client)
.andThen(sendWith(wallet))
.andThen(client.waitForTransaction);
import { foobar } from "@aave/client/actions";
import { sendWith } from "@aave/client/privy";
import { PrivyClient } from "@privy-io/server-auth";
import { client } from "./client";
const privy = new PrivyClient("<PRIVY_APP_ID>", "<PRIVY_APP_SECRET>");
const result = await foobar(client)
.andThen(sendWith(privy, "<PRIVY_WALLET_ID>"))
.andThen(client.waitForTransaction);
import { foobar } from "@aave/client/actions";
import { sendWith } from "@aave/client/thirdweb";
import { createThirdwebClient } from "thirdweb";
import { client } from "./client";
const thirdwebClient = createThirdwebClient({
secretKey: "<THIRDWEB_SECRET_KEY>",
});
const result = await foobar(client)
.andThen(sendWith(client, thirdwebClient))
.andThen(client.waitForTransaction);
import { foobar } from "@aave/client/actions";
import { sendWith } from "@aave/client/viem";
import { createAccount } from "@turnkey/viem";
import { Turnkey } from "@turnkey/sdk-server";
import { createWalletClient, http } from "viem";
import { mainnet } from "viem/chains";
import { client } from "./client";
const turnkeyClient = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiPrivateKey: "<TURNKEY_API_PRIVATE_KEY>",
apiPublicKey: "<TURNKEY_API_PUBLIC_KEY>",
defaultOrganizationId: "<TURNKEY_ORGANIZATION_ID>",
});
const turnkeyAccount = await createAccount({
client: turnkeyClient.apiClient(),
organizationId: "<TURNKEY_ORGANIZATION_ID>",
signWith: "<TURNKEY_WALLET_ADDRESS>",
});
const wallet = createWalletClient({
account: turnkeyAccount,
chain: mainnet,
transport: http(),
});
const result = await foobar(client)
.andThen(sendWith(wallet))
.andThen(client.waitForTransaction);
3. Handle the Result
Finally, handle the result of the operation.
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
case "SigningError":
// Most likely the user rejected the transaction
console.error(`Failed to sign the transaction: ${result.error.message}`);
break;
case "TimeoutError":
console.error(`Transaction timed out: ${result.error.message}`);
break;
case "TransactionError":
console.error(`Transaction failed: ${result.error.message}`);
break;
case "ValidationError":
console.error(
"Insufficient balance:",
`required: ${result.error.cause.required.value.toDisplayString(2)}`,
`available: ${result.error.cause.available.value.toDisplayString(2)}`,
);
break;
case "UnexpectedError":
console.error(result.error.message);
break;
}
return;
} else {
console.log(result.value.txHash); // TxHash
}
You’ve sent the transaction.
Sign Typed Data
Some Aave operations require EIP-712 typed data signatures for the following:
- ERC-20 Permits: Authorize exact amount transfers without separate approval transactions
- Swap Intents: Sign swap orders for intent-based swaps
Permits are available for ERC-20 tokens that implement EIP-2612.
Import the signing helpers for your wallet library.
Import the sign or permit helpers from the @aave/ entry point.
import { signTypedDataWith, permitWith } from "@aave/client/viem";
Import the sign or permit helpers from the @aave/ entry point.
import { signTypedDataWith, permitWith } from "@aave/client/ethers";
Import the sign or permit helpers from the @aave/ entry point.
import { signTypedDataWith, permitWith } from "@aave/client/privy";
Import the sign or permit helpers from the @aave/ entry point.
import { signTypedDataWith, permitWith } from "@aave/client/thirdweb";
Import the sign or permit helpers from the @aave/ entry point.
import { signTypedDataWith, permitWith } from "@aave/client/viem";
Then, use them as described in the specific operation guide.