Aave V3 TypeScript Quickstart
AaveKit TypeScript provides a type-safe, low-level API client for interacting with Aave Protocol V3. 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 vault management.
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@latest
yarn add @aave/client@latest
pnpm add @aave/client@latest
2. Set Up the 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 start using the @aave/ to interact with the Aave Protocol.
import { chains } from "@aave/client/actions";
import { client } from "./client";
// Fetch all chains supported by Aave
const result = await chains(client);
if (result.isOk()) {
console.log("Chains:", result.value);
} else {
console.error("Error:", result.error);
}
Result Objects
AaveKit actions use a functional approach to error handling. They return a Result: a thenable object that represents one of two states:
Ok<T>: A successful result containing a value of typeTErr<E>: A failure containing an error of typeE
When you await a Result, it resolves to a Result<T, E>.
With a Result<T, E>, you can use the convenient is and is methods to check the outcome and narrow the type.
If you have a Result, you must first await it to access those methods.
const result = await actionName(/* args */);
if (result.isOk()) {
console.log("Result:", result.value);
} else {
console.error("Error:", result.error);
}
This approach avoids reliance on try/ blocks and promotes predictable, type-safe code by ensuring errors are handled explicitly.
AaveKit re-exports the NeverThrow library. See its docs for details.
For example:
import { ok, err, type Result } from "@aave/client";
function parseNumber(input: string): Result<number, string> {
return isNaN(Number(input)) ? err("Invalid number") : ok(Number(input));
}
const result = parseNumber("42");
You can chain multiple operations:
function divide(a: number, b: number): Result<number, string> {
return b === 0 ? err("Division by zero") : ok(a / b);
}
const result = parseNumber("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:
const value = parseNumber("invalid").unwrapOr(0); // 0
NeverThrow also provides a Result type for handling asynchronous operations. This is a thenable object that can be awaited, and/or chained with other operations:
const result = await ResultAsync.fromPromise(
fetch("https://api.example.com/data")
)
.map((response) => response.json())
.mapErr((error) => `Failed to fetch data: ${error}`);
if (result.isOk()) {
console.log("Data:", result.value);
} else {
console.error("Error:", result.error);
}
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 = 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(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 "SigningError":
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 "UnexpectedError":
console.error(result.error.message);
break;
}
} else {
console.log("Transaction sent with hash:", result.value);
}
You’ve sent the transaction.
Sign ERC-20 Permits
Some operations support permit functionality using EIP-2612 signatures to authorize ERC-20 token transfers within the same transaction.
To use permits, you’ll need both the permit action to generate the permit data and a sign helper specific to your wallet library.
Import the sign helper from the @aave/ entry point.
import { signERC20PermitWith } from "@aave/client/viem";
Import the sign helper from the @aave/ entry point.
import { signERC20PermitWith } from "@aave/client/ethers";
Import the sign helper from the @aave/ entry point.
import { signERC20PermitWith } from "@aave/client/privy";
Import the sign helper from the @aave/ entry point.
import { signERC20PermitWith } from "@aave/client/thirdweb";
Import the sign helper from the @aave/ entry point.
import { signERC20PermitWith } from "@aave/client/viem";
Use permit to generate the permit data, then chain it with your wallet-specific sign helper:
import { permitTypedData } from "@aave/client/actions";
import { signERC20PermitWith } 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 permitTypedData(client, {
// permit parameters
}).andThen(signERC20PermitWith(wallet));
import { permitTypedData } from "@aave/client/actions";
import { signERC20PermitWith } from "@aave/client/ethers";
import { Wallet, getDefaultProvider } from "ethers";
import { client } from "./client";
const wallet = new Wallet("<PRIVATE_KEY>", getDefaultProvider("mainnet"));
const result = await permitTypedData(client, {
// permit parameters
}).andThen(signERC20PermitWith(wallet));
import { permitTypedData } from "@aave/client/actions";
import { signERC20PermitWith } 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 permitTypedData(client, {
// permit parameters
}).andThen(signERC20PermitWith(privy, "<PRIVY_WALLET_ID>"));
import { permitTypedData } from "@aave/client/actions";
import { signERC20PermitWith } from "@aave/client/thirdweb";
import { createThirdwebClient } from "thirdweb";
import { client } from "./client";
const thirdwebClient = createThirdwebClient({
secretKey: "<THIRDWEB_SECRET_KEY>",
});
const result = await permitTypedData(client, {
// permit parameters
}).andThen(signERC20PermitWith(thirdwebClient));
import { permitTypedData } from "@aave/client/actions";
import { signERC20PermitWith } 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 permitTypedData(client, {
// permit parameters
}).andThen(signERC20PermitWith(wallet));
For complete permit parameter examples, refer to the specific operation documentation.