From cfcd318cf817022437593e97b7576476ebf4ef0c Mon Sep 17 00:00:00 2001
From: h1-hunt
Date: Fri, 13 Feb 2026 15:49:24 +0000
Subject: [PATCH 1/2] feat: add Mint Club V2 action provider
Add action provider for Mint Club V2 bonding curve protocol on Base.
Actions:
- get_token_info: Token details and bonding curve information
- get_token_price: Current price in reserve tokens and USD
- buy_token: Mint tokens via bonding curve
- sell_token: Burn tokens via bonding curve
- create_token: Create new token with bonding curve
Uses viem for direct contract interaction with MCV2_Bond.
Includes auto ERC20 approval, slippage protection, and
USD pricing via 1inch Spot Price Aggregator.
Docs: https://docs.mint.club
Protocol: https://mint.club
---
.../agentkit/src/action-providers/index.ts | 1 +
.../src/action-providers/mintclub/README.md | 42 ++
.../action-providers/mintclub/constants.ts | 252 ++++++++++
.../src/action-providers/mintclub/index.ts | 2 +
.../mintclub/mintclubActionProvider.test.ts | 370 ++++++++++++++
.../mintclub/mintclubActionProvider.ts | 461 ++++++++++++++++++
.../src/action-providers/mintclub/schemas.ts | 147 ++++++
.../src/action-providers/mintclub/utils.ts | 244 +++++++++
8 files changed, 1519 insertions(+)
create mode 100644 typescript/agentkit/src/action-providers/mintclub/README.md
create mode 100644 typescript/agentkit/src/action-providers/mintclub/constants.ts
create mode 100644 typescript/agentkit/src/action-providers/mintclub/index.ts
create mode 100644 typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts
create mode 100644 typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts
create mode 100644 typescript/agentkit/src/action-providers/mintclub/schemas.ts
create mode 100644 typescript/agentkit/src/action-providers/mintclub/utils.ts
diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts
index 7f2b0233a..efc6bef4c 100644
--- a/typescript/agentkit/src/action-providers/index.ts
+++ b/typescript/agentkit/src/action-providers/index.ts
@@ -18,6 +18,7 @@ export * from "./erc721";
export * from "./farcaster";
export * from "./jupiter";
export * from "./messari";
+export * from "./mintclub";
export * from "./pyth";
export * from "./moonwell";
export * from "./morpho";
diff --git a/typescript/agentkit/src/action-providers/mintclub/README.md b/typescript/agentkit/src/action-providers/mintclub/README.md
new file mode 100644
index 000000000..458df5623
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/README.md
@@ -0,0 +1,42 @@
+# Mint Club V2 Action Provider
+
+This directory contains the **MintclubActionProvider** implementation, which provides actions to interact with the **Mint Club V2 protocol** on Base mainnet.
+
+## Directory Structure
+
+```
+mintclub/
+├── mintclubActionProvider.ts # Main provider with Mint Club V2 functionality
+├── mintclubActionProvider.test.ts # Test file for Mint Club provider
+├── constants.ts # Mint Club contract constants and ABIs
+├── schemas.ts # Mint Club action schemas
+├── utils.ts # Mint Club utility functions
+├── index.ts # Main exports
+└── README.md # This file
+```
+
+## Actions
+
+- `get_token_info`: Get detailed information about a Mint Club token including bonding curve details
+- `get_token_price`: Get the current price of a Mint Club token in reserve tokens and USD
+- `buy_token`: Buy Mint Club tokens via the bonding curve mechanism
+- `sell_token`: Sell Mint Club tokens via the bonding curve mechanism
+- `create_token`: Create a new Mint Club token with bonding curve
+
+## Adding New Actions
+
+To add new Mint Club actions:
+
+1. Define your action schema in `schemas.ts`
+2. Implement the action in `mintclubActionProvider.ts`
+3. Add tests in `mintclubActionProvider.test.ts`
+
+## Network Support
+
+The Mint Club provider supports Base mainnet only.
+
+## Notes
+
+Mint Club V2 is a bonding curve token protocol that allows anyone to create tokens backed by reserve assets. The protocol uses mathematical curves to determine token prices based on supply and demand.
+
+For more information on the **Mint Club V2 protocol**, visit [Mint Club Documentation](https://mint.club/).
\ No newline at end of file
diff --git a/typescript/agentkit/src/action-providers/mintclub/constants.ts b/typescript/agentkit/src/action-providers/mintclub/constants.ts
new file mode 100644
index 000000000..9a8d61018
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/constants.ts
@@ -0,0 +1,252 @@
+import type { Abi } from "abitype";
+
+export const SUPPORTED_NETWORKS = ["base-mainnet"];
+
+/**
+ * MCV2 Bond contract ABI - minimal functions needed for Mint Club V2 interactions.
+ */
+export const MCV2_BOND_ABI: Abi = [
+ {
+ type: "function",
+ name: "tokenBond",
+ inputs: [{ name: "token", type: "address", internalType: "address" }],
+ outputs: [
+ { name: "creator", type: "address", internalType: "address" },
+ { name: "mintRoyalty", type: "uint16", internalType: "uint16" },
+ { name: "burnRoyalty", type: "uint16", internalType: "uint16" },
+ { name: "createdAt", type: "uint40", internalType: "uint40" },
+ { name: "reserveToken", type: "address", internalType: "address" },
+ { name: "reserveBalance", type: "uint256", internalType: "uint256" },
+ ],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "getReserveForToken",
+ inputs: [
+ { name: "token", type: "address", internalType: "address" },
+ { name: "tokensToMint", type: "uint256", internalType: "uint256" },
+ ],
+ outputs: [
+ { name: "reserveAmount", type: "uint256", internalType: "uint256" },
+ { name: "royalty", type: "uint256", internalType: "uint256" },
+ ],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "getRefundForTokens",
+ inputs: [
+ { name: "token", type: "address", internalType: "address" },
+ { name: "tokensToBurn", type: "uint256", internalType: "uint256" },
+ ],
+ outputs: [
+ { name: "refundAmount", type: "uint256", internalType: "uint256" },
+ { name: "royalty", type: "uint256", internalType: "uint256" },
+ ],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "mint",
+ inputs: [
+ { name: "token", type: "address", internalType: "address" },
+ { name: "tokensToMint", type: "uint256", internalType: "uint256" },
+ { name: "maxReserveAmount", type: "uint256", internalType: "uint256" },
+ { name: "receiver", type: "address", internalType: "address" },
+ ],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "nonpayable",
+ },
+ {
+ type: "function",
+ name: "burn",
+ inputs: [
+ { name: "token", type: "address", internalType: "address" },
+ { name: "tokensToBurn", type: "uint256", internalType: "uint256" },
+ { name: "minRefund", type: "uint256", internalType: "uint256" },
+ { name: "receiver", type: "address", internalType: "address" },
+ ],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "nonpayable",
+ },
+ {
+ type: "function",
+ name: "createToken",
+ inputs: [
+ {
+ name: "tp",
+ type: "tuple",
+ internalType: "struct MCV2_Bond.TokenParams",
+ components: [
+ { name: "name", type: "string", internalType: "string" },
+ { name: "symbol", type: "string", internalType: "string" },
+ ],
+ },
+ {
+ name: "bp",
+ type: "tuple",
+ internalType: "struct MCV2_Bond.BondParams",
+ components: [
+ { name: "mintRoyalty", type: "uint16", internalType: "uint16" },
+ { name: "burnRoyalty", type: "uint16", internalType: "uint16" },
+ { name: "reserveToken", type: "address", internalType: "address" },
+ { name: "maxSupply", type: "uint128", internalType: "uint128" },
+ { name: "stepRanges", type: "uint128[]", internalType: "uint128[]" },
+ { name: "stepPrices", type: "uint128[]", internalType: "uint128[]" },
+ ],
+ },
+ ],
+ outputs: [{ name: "token", type: "address", internalType: "address" }],
+ stateMutability: "payable",
+ },
+ {
+ type: "function",
+ name: "creationFee",
+ inputs: [],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "view",
+ },
+] as const;
+
+/**
+ * ERC20 token ABI - standard functions needed for token interactions.
+ */
+export const ERC20_ABI: Abi = [
+ {
+ type: "function",
+ name: "symbol",
+ inputs: [],
+ outputs: [{ name: "", type: "string", internalType: "string" }],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "decimals",
+ inputs: [],
+ outputs: [{ name: "", type: "uint8", internalType: "uint8" }],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "totalSupply",
+ inputs: [],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "balanceOf",
+ inputs: [{ name: "account", type: "address", internalType: "address" }],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "view",
+ },
+ {
+ type: "function",
+ name: "approve",
+ inputs: [
+ { name: "spender", type: "address", internalType: "address" },
+ { name: "amount", type: "uint256", internalType: "uint256" },
+ ],
+ outputs: [{ name: "", type: "bool", internalType: "bool" }],
+ stateMutability: "nonpayable",
+ },
+ {
+ type: "function",
+ name: "allowance",
+ inputs: [
+ { name: "owner", type: "address", internalType: "address" },
+ { name: "spender", type: "address", internalType: "address" },
+ ],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "view",
+ },
+] as const;
+
+/**
+ * 1inch Spot Price Aggregator ABI for USD pricing.
+ */
+export const SPOT_PRICE_AGGREGATOR_ABI: Abi = [
+ {
+ type: "function",
+ name: "getRate",
+ inputs: [
+ { name: "srcToken", type: "address", internalType: "contract IERC20" },
+ { name: "dstToken", type: "address", internalType: "contract IERC20" },
+ { name: "useWrappers", type: "bool", internalType: "bool" },
+ ],
+ outputs: [{ name: "weightedRate", type: "uint256", internalType: "uint256" }],
+ stateMutability: "view",
+ },
+] as const;
+
+/**
+ * Contract addresses on Base mainnet.
+ */
+export const MINTCLUB_CONTRACT_ADDRESSES = {
+ "base-mainnet": {
+ MCV2_Bond: "0xc5a076cad94176c2996B32d8466Be1cE757FAa27",
+ SpotPriceAggregator: "0x00000000000D6FFc74A8feb35aF5827bf57f6786",
+ USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
+ },
+} as const;
+
+/**
+ * Gets the MCV2 Bond contract address for the specified network.
+ *
+ * @param network - The network ID to get the contract address for.
+ * @returns The contract address for the specified network.
+ * @throws Error if the specified network is not supported.
+ */
+export function getBondAddress(network: string): string {
+ const addresses =
+ MINTCLUB_CONTRACT_ADDRESSES[
+ network.toLowerCase() as keyof typeof MINTCLUB_CONTRACT_ADDRESSES
+ ];
+ if (!addresses) {
+ throw new Error(
+ `Unsupported network: ${network}. Supported: ${Object.keys(MINTCLUB_CONTRACT_ADDRESSES).join(", ")}`,
+ );
+ }
+ return addresses.MCV2_Bond;
+}
+
+/**
+ * Gets the Spot Price Aggregator contract address for the specified network.
+ *
+ * @param network - The network ID to get the contract address for.
+ * @returns The contract address for the specified network.
+ * @throws Error if the specified network is not supported.
+ */
+export function getSpotPriceAggregatorAddress(network: string): string {
+ const addresses =
+ MINTCLUB_CONTRACT_ADDRESSES[
+ network.toLowerCase() as keyof typeof MINTCLUB_CONTRACT_ADDRESSES
+ ];
+ if (!addresses) {
+ throw new Error(
+ `Unsupported network: ${network}. Supported: ${Object.keys(MINTCLUB_CONTRACT_ADDRESSES).join(", ")}`,
+ );
+ }
+ return addresses.SpotPriceAggregator;
+}
+
+/**
+ * Gets the USDC contract address for the specified network.
+ *
+ * @param network - The network ID to get the contract address for.
+ * @returns The contract address for the specified network.
+ * @throws Error if the specified network is not supported.
+ */
+export function getUsdcAddress(network: string): string {
+ const addresses =
+ MINTCLUB_CONTRACT_ADDRESSES[
+ network.toLowerCase() as keyof typeof MINTCLUB_CONTRACT_ADDRESSES
+ ];
+ if (!addresses) {
+ throw new Error(
+ `Unsupported network: ${network}. Supported: ${Object.keys(MINTCLUB_CONTRACT_ADDRESSES).join(", ")}`,
+ );
+ }
+ return addresses.USDC;
+}
diff --git a/typescript/agentkit/src/action-providers/mintclub/index.ts b/typescript/agentkit/src/action-providers/mintclub/index.ts
new file mode 100644
index 000000000..9379c974a
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/index.ts
@@ -0,0 +1,2 @@
+export * from "./schemas";
+export * from "./mintclubActionProvider";
\ No newline at end of file
diff --git a/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts
new file mode 100644
index 000000000..4ea6a1a8f
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts
@@ -0,0 +1,370 @@
+import { encodeFunctionData } from "viem";
+import { EvmWalletProvider } from "../../wallet-providers";
+import { MintclubActionProvider } from "./mintclubActionProvider";
+import { MCV2_BOND_ABI, ERC20_ABI, getBondAddress } from "./constants";
+import {
+ getTokenBond,
+ getTokenInfo,
+ getBuyQuote,
+ getSellQuote,
+ getUsdRate,
+ needsApproval,
+} from "./utils";
+import {
+ MintclubGetTokenInfoInput,
+ MintclubGetTokenPriceInput,
+ MintclubBuyTokenInput,
+ MintclubSellTokenInput,
+ MintclubCreateTokenInput,
+} from "./schemas";
+
+jest.mock("./utils", () => ({
+ getTokenBond: jest.fn(),
+ getTokenInfo: jest.fn(),
+ getBuyQuote: jest.fn(),
+ getSellQuote: jest.fn(),
+ getUsdRate: jest.fn(),
+ needsApproval: jest.fn(),
+}));
+
+describe("MintclubActionProvider", () => {
+ const MOCK_TOKEN_ADDRESS =
+ "0x1234567890123456789012345678901234567890" as `0x${string}`;
+ const MOCK_RESERVE_TOKEN =
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`;
+ const MOCK_TOKENS_WEI = "1000000000000000000";
+ const MOCK_RESERVE_AMOUNT = "1000000";
+ const MOCK_TX_HASH = "0xabcdef1234567890";
+ const MOCK_WALLET_ADDRESS =
+ "0x9876543210987654321098765432109876543210" as `0x${string}`;
+ const MOCK_MAX_SUPPLY = "1000000000000000000000000";
+
+ let provider: MintclubActionProvider;
+ let mockWallet: jest.Mocked;
+
+ beforeEach(() => {
+ mockWallet = {
+ getAddress: jest.fn().mockReturnValue(MOCK_WALLET_ADDRESS),
+ getNetwork: jest
+ .fn()
+ .mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }),
+ sendTransaction: jest
+ .fn()
+ .mockResolvedValue(MOCK_TX_HASH as `0x${string}`),
+ waitForTransactionReceipt: jest.fn().mockResolvedValue({}),
+ readContract: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ provider = new MintclubActionProvider();
+
+ (getTokenInfo as jest.Mock).mockResolvedValue({
+ symbol: "TEST",
+ decimals: 18,
+ totalSupply: "1000000000000000000000",
+ formattedSupply: "1000.0",
+ });
+
+ (getTokenBond as jest.Mock).mockResolvedValue({
+ creator: MOCK_WALLET_ADDRESS,
+ mintRoyalty: 100,
+ burnRoyalty: 100,
+ createdAt: 1640995200,
+ reserveToken: MOCK_RESERVE_TOKEN,
+ reserveBalance: "1000000000000000000",
+ });
+
+ (getBuyQuote as jest.Mock).mockResolvedValue({
+ reserveAmount: MOCK_RESERVE_AMOUNT,
+ royalty: "10000",
+ });
+
+ (getSellQuote as jest.Mock).mockResolvedValue({
+ refundAmount: MOCK_RESERVE_AMOUNT,
+ royalty: "10000",
+ });
+
+ (getUsdRate as jest.Mock).mockResolvedValue(1.0);
+ (needsApproval as jest.Mock).mockResolvedValue(false);
+ });
+
+ describe("Input Validation", () => {
+ it("should reject invalid address in getTokenInfo", () => {
+ const result = MintclubGetTokenInfoInput.safeParse({
+ tokenAddress: "0xinvalid",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should accept valid getTokenInfo input", () => {
+ const result = MintclubGetTokenInfoInput.safeParse({
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject invalid amount in getTokenPrice", () => {
+ const result = MintclubGetTokenPriceInput.safeParse({
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ amount: "abc",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject decimal wei in buyToken", () => {
+ const result = MintclubBuyTokenInput.safeParse({
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToMint: "1.5",
+ maxReserveAmount: MOCK_RESERVE_AMOUNT,
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should accept valid buyToken input", () => {
+ const result = MintclubBuyTokenInput.safeParse({
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToMint: MOCK_TOKENS_WEI,
+ maxReserveAmount: MOCK_RESERVE_AMOUNT,
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject royalty exceeding max in createToken", () => {
+ const result = MintclubCreateTokenInput.safeParse({
+ name: "Test",
+ symbol: "TST",
+ reserveToken: MOCK_RESERVE_TOKEN,
+ maxSupply: MOCK_MAX_SUPPLY,
+ stepRanges: [MOCK_MAX_SUPPLY],
+ stepPrices: ["1000000000000000"],
+ mintRoyalty: 6000,
+ burnRoyalty: 100,
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should accept valid createToken input", () => {
+ const result = MintclubCreateTokenInput.safeParse({
+ name: "Test Token",
+ symbol: "TST",
+ reserveToken: MOCK_RESERVE_TOKEN,
+ maxSupply: MOCK_MAX_SUPPLY,
+ stepRanges: ["500000000000000000000000", MOCK_MAX_SUPPLY],
+ stepPrices: ["1000000000000000", "10000000000000000"],
+ mintRoyalty: 100,
+ burnRoyalty: 100,
+ });
+ expect(result.success).toBe(true);
+ });
+ });
+
+ describe("supportsNetwork", () => {
+ it("should support base-mainnet", () => {
+ expect(
+ provider.supportsNetwork({
+ protocolFamily: "evm",
+ networkId: "base-mainnet",
+ }),
+ ).toBe(true);
+ });
+
+ it("should not support base-sepolia", () => {
+ expect(
+ provider.supportsNetwork({
+ protocolFamily: "evm",
+ networkId: "base-sepolia",
+ }),
+ ).toBe(false);
+ });
+
+ it("should not support non-EVM networks", () => {
+ expect(
+ provider.supportsNetwork({
+ protocolFamily: "bitcoin",
+ networkId: "base-mainnet",
+ }),
+ ).toBe(false);
+ });
+ });
+
+ describe("getTokenInfo", () => {
+ it("should return token and bond information", async () => {
+ const response = await provider.getTokenInfo(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ });
+
+ expect(getTokenInfo).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS);
+ expect(getTokenBond).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS);
+ expect(response).toContain("TEST");
+ expect(response).toContain("1000.0");
+ expect(response).toContain("1.00%");
+ });
+
+ it("should handle non-existent token", async () => {
+ (getTokenInfo as jest.Mock).mockResolvedValue(null);
+
+ const response = await provider.getTokenInfo(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ });
+ expect(response).toContain("Error");
+ });
+
+ it("should handle non-Mint Club token", async () => {
+ (getTokenBond as jest.Mock).mockResolvedValue(null);
+
+ const response = await provider.getTokenInfo(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ });
+ expect(response).toContain("not a Mint Club V2 token");
+ });
+ });
+
+ describe("getTokenPrice", () => {
+ it("should return price with USD value", async () => {
+ const response = await provider.getTokenPrice(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ amount: "1.0",
+ });
+
+ expect(getBuyQuote).toHaveBeenCalled();
+ expect(response).toContain("Price for 1.0 TEST");
+ expect(response).toContain("USD Value");
+ });
+
+ it("should handle missing quote", async () => {
+ (getBuyQuote as jest.Mock).mockResolvedValue(null);
+
+ const response = await provider.getTokenPrice(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ amount: "1.0",
+ });
+ expect(response).toContain("Error");
+ });
+ });
+
+ describe("buyToken", () => {
+ it("should buy tokens without needing approval", async () => {
+ const response = await provider.buyToken(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToMint: MOCK_TOKENS_WEI,
+ maxReserveAmount: MOCK_RESERVE_AMOUNT,
+ });
+
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1);
+ expect(response).toContain("Successfully minted");
+ expect(response).toContain(MOCK_TX_HASH);
+ });
+
+ it("should handle approval + buy when needed", async () => {
+ (needsApproval as jest.Mock).mockResolvedValue(true);
+
+ const response = await provider.buyToken(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToMint: MOCK_TOKENS_WEI,
+ maxReserveAmount: MOCK_RESERVE_AMOUNT,
+ });
+
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(2);
+ expect(response).toContain("Successfully minted");
+ });
+
+ it("should handle non-Mint Club token", async () => {
+ (getTokenBond as jest.Mock).mockResolvedValue(null);
+
+ const response = await provider.buyToken(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToMint: MOCK_TOKENS_WEI,
+ maxReserveAmount: MOCK_RESERVE_AMOUNT,
+ });
+ expect(response).toContain("not a valid Mint Club V2 token");
+ });
+
+ it("should handle transaction failure", async () => {
+ mockWallet.sendTransaction.mockRejectedValue(new Error("tx failed"));
+
+ const response = await provider.buyToken(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToMint: MOCK_TOKENS_WEI,
+ maxReserveAmount: MOCK_RESERVE_AMOUNT,
+ });
+ expect(response).toContain("Error buying");
+ });
+ });
+
+ describe("sellToken", () => {
+ beforeEach(() => {
+ mockWallet.readContract.mockResolvedValue(
+ BigInt("2000000000000000000"),
+ );
+ });
+
+ it("should sell tokens successfully", async () => {
+ const response = await provider.sellToken(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToBurn: MOCK_TOKENS_WEI,
+ minRefund: MOCK_RESERVE_AMOUNT,
+ });
+
+ expect(response).toContain("Successfully sold");
+ expect(response).toContain(MOCK_TX_HASH);
+ });
+
+ it("should reject when balance is insufficient", async () => {
+ mockWallet.readContract.mockResolvedValue(BigInt("100"));
+
+ const response = await provider.sellToken(mockWallet, {
+ tokenAddress: MOCK_TOKEN_ADDRESS,
+ tokensToBurn: MOCK_TOKENS_WEI,
+ minRefund: MOCK_RESERVE_AMOUNT,
+ });
+ expect(response).toContain("Insufficient balance");
+ });
+ });
+
+ describe("createToken", () => {
+ beforeEach(() => {
+ // Mock creationFee
+ mockWallet.readContract.mockResolvedValue(BigInt(0));
+ });
+
+ it("should create a token successfully", async () => {
+ const response = await provider.createToken(mockWallet, {
+ name: "Test Token",
+ symbol: "TST",
+ reserveToken: MOCK_RESERVE_TOKEN,
+ maxSupply: MOCK_MAX_SUPPLY,
+ stepRanges: [MOCK_MAX_SUPPLY],
+ stepPrices: ["1000000000000000"],
+ mintRoyalty: 100,
+ burnRoyalty: 100,
+ });
+
+ expect(mockWallet.sendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: getBondAddress("base-mainnet"),
+ value: BigInt(0),
+ }),
+ );
+ expect(response).toContain("Successfully created");
+ expect(response).toContain("Test Token");
+ expect(response).toContain("TST");
+ });
+
+ it("should handle creation failure", async () => {
+ mockWallet.sendTransaction.mockRejectedValue(
+ new Error("create failed"),
+ );
+
+ const response = await provider.createToken(mockWallet, {
+ name: "Test Token",
+ symbol: "TST",
+ reserveToken: MOCK_RESERVE_TOKEN,
+ maxSupply: MOCK_MAX_SUPPLY,
+ stepRanges: [MOCK_MAX_SUPPLY],
+ stepPrices: ["1000000000000000"],
+ mintRoyalty: 100,
+ burnRoyalty: 100,
+ });
+ expect(response).toContain("Error creating");
+ });
+ });
+});
diff --git a/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts
new file mode 100644
index 000000000..21e23e6a4
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts
@@ -0,0 +1,461 @@
+import { z } from "zod";
+import { ActionProvider } from "../actionProvider";
+import { EvmWalletProvider } from "../../wallet-providers";
+import { CreateAction } from "../actionDecorator";
+import { Network } from "../../network";
+import {
+ SUPPORTED_NETWORKS,
+ MCV2_BOND_ABI,
+ ERC20_ABI,
+ getBondAddress,
+} from "./constants";
+import {
+ getTokenBond,
+ getTokenInfo,
+ getBuyQuote,
+ getSellQuote,
+ getUsdRate,
+ needsApproval,
+} from "./utils";
+import { encodeFunctionData, formatUnits } from "viem";
+import {
+ MintclubGetTokenInfoInput,
+ MintclubGetTokenPriceInput,
+ MintclubBuyTokenInput,
+ MintclubSellTokenInput,
+ MintclubCreateTokenInput,
+} from "./schemas";
+
+/**
+ * MintclubActionProvider is an action provider for Mint Club V2 protocol interactions.
+ *
+ * Mint Club V2 is a permissionless bonding curve protocol on Base. Tokens are created
+ * with programmable price curves backed by reserve assets. The protocol handles minting,
+ * burning, and price discovery through smart contracts.
+ *
+ * @see https://mint.club
+ * @see https://docs.mint.club
+ */
+export class MintclubActionProvider extends ActionProvider {
+ /**
+ * Constructor for the MintclubActionProvider class.
+ */
+ constructor() {
+ super("mintclub", []);
+ }
+
+ /**
+ * Gets detailed information about a Mint Club token including bonding curve details.
+ *
+ * @param walletProvider - The wallet provider to get token information from.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the token information.
+ */
+ @CreateAction({
+ name: "get_token_info",
+ description: `
+This tool gets detailed information about a Mint Club V2 bonding curve token on Base.
+
+Inputs:
+- Token contract address
+
+Returns token details (symbol, decimals, supply) and bonding curve details
+(creator, reserve token, reserve balance, royalties).
+
+Important notes:
+- Only works with Mint Club V2 tokens that have bonding curves
+- Supported on Base mainnet only`,
+ schema: MintclubGetTokenInfoInput,
+ })
+ async getTokenInfo(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ try {
+ const [tokenInfo, bondInfo] = await Promise.all([
+ getTokenInfo(walletProvider, args.tokenAddress),
+ getTokenBond(walletProvider, args.tokenAddress),
+ ]);
+
+ if (!tokenInfo) {
+ return `Error: Could not fetch token information for ${args.tokenAddress}. Verify this is a valid ERC20 token address.`;
+ }
+
+ if (!bondInfo) {
+ return `Error: ${args.tokenAddress} is not a Mint Club V2 token (no bonding curve found).`;
+ }
+
+ const reserveTokenInfo = await getTokenInfo(walletProvider, bondInfo.reserveToken);
+ const reserveSymbol = reserveTokenInfo?.symbol || "Unknown";
+ const reserveDecimals = reserveTokenInfo?.decimals || 18;
+ const formattedReserveBalance = formatUnits(
+ BigInt(bondInfo.reserveBalance),
+ reserveDecimals,
+ );
+
+ return `Token Information for ${args.tokenAddress}:
+
+Token: ${tokenInfo.symbol}
+Decimals: ${tokenInfo.decimals}
+Total Supply: ${tokenInfo.formattedSupply}
+Creator: ${bondInfo.creator}
+Reserve Token: ${reserveSymbol} (${bondInfo.reserveToken})
+Reserve Balance: ${formattedReserveBalance} ${reserveSymbol}
+Mint Royalty: ${(bondInfo.mintRoyalty / 100).toFixed(2)}%
+Burn Royalty: ${(bondInfo.burnRoyalty / 100).toFixed(2)}%
+Created: ${new Date(bondInfo.createdAt * 1000).toISOString()}`;
+ } catch (error) {
+ return `Error getting token information: ${error}`;
+ }
+ }
+
+ /**
+ * Gets the current price of a Mint Club token.
+ *
+ * @param walletProvider - The wallet provider to get price information from.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the token price information.
+ */
+ @CreateAction({
+ name: "get_token_price",
+ description: `
+This tool gets the current price of a Mint Club V2 token on Base.
+
+Inputs:
+- Token contract address
+- Amount of tokens to price (in whole units, e.g., "1" or "100")
+
+Returns the cost in reserve tokens (including royalty) and USD estimate.
+
+Important notes:
+- Price depends on the bonding curve position — larger amounts have higher price impact
+- Includes royalty fees in the total cost
+- Supported on Base mainnet only`,
+ schema: MintclubGetTokenPriceInput,
+ })
+ async getTokenPrice(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ try {
+ const [tokenInfo, bondInfo] = await Promise.all([
+ getTokenInfo(walletProvider, args.tokenAddress),
+ getTokenBond(walletProvider, args.tokenAddress),
+ ]);
+
+ if (!tokenInfo || !bondInfo) {
+ return `Error: ${args.tokenAddress} is not a valid Mint Club V2 token.`;
+ }
+
+ const tokensInWei = BigInt(
+ Math.floor(Number(args.amount) * 10 ** tokenInfo.decimals),
+ ).toString();
+
+ const buyQuote = await getBuyQuote(walletProvider, args.tokenAddress, tokensInWei);
+ if (!buyQuote) {
+ return `Error: Could not get price quote. The bonding curve may have insufficient remaining supply.`;
+ }
+
+ const reserveTokenInfo = await getTokenInfo(walletProvider, bondInfo.reserveToken);
+ const reserveSymbol = reserveTokenInfo?.symbol || "Unknown";
+ const reserveDecimals = reserveTokenInfo?.decimals || 18;
+
+ const reserveCost = formatUnits(BigInt(buyQuote.reserveAmount), reserveDecimals);
+ const royalty = formatUnits(BigInt(buyQuote.royalty), reserveDecimals);
+ const totalCost = formatUnits(
+ BigInt(buyQuote.reserveAmount) + BigInt(buyQuote.royalty),
+ reserveDecimals,
+ );
+
+ // Get USD rate for the reserve token
+ const usdRate = await getUsdRate(walletProvider, bondInfo.reserveToken);
+
+ let result = `Price for ${args.amount} ${tokenInfo.symbol}:
+
+Reserve Cost: ${reserveCost} ${reserveSymbol}
+Royalty: ${royalty} ${reserveSymbol}
+Total: ${totalCost} ${reserveSymbol}`;
+
+ if (usdRate !== null && usdRate > 0) {
+ const totalReserve =
+ Number(buyQuote.reserveAmount) + Number(buyQuote.royalty);
+ const usdValue = (totalReserve / 10 ** reserveDecimals) * usdRate;
+ result += `\nUSD Value: ~$${usdValue.toFixed(2)}`;
+ }
+
+ return result;
+ } catch (error) {
+ return `Error getting token price: ${error}`;
+ }
+ }
+
+ /**
+ * Buys Mint Club tokens via the bonding curve.
+ *
+ * @param walletProvider - The wallet provider to buy tokens with.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the purchase details.
+ */
+ @CreateAction({
+ name: "buy_token",
+ description: `
+This tool buys (mints) Mint Club V2 tokens via the bonding curve on Base.
+Do not use this tool for buying other types of tokens.
+
+Inputs:
+- Token contract address
+- Amount of tokens to mint (in wei, e.g., "1000000000000000000" for 1 token with 18 decimals)
+- Maximum reserve to spend (in wei, for slippage protection)
+
+Important notes:
+- Amounts are in wei (no decimal points). 1 token = 10^decimals wei.
+- The max reserve amount protects against slippage — the transaction reverts if the cost exceeds this.
+- If the reserve token is an ERC20, approval is handled automatically.
+- Only supported on Base mainnet.`,
+ schema: MintclubBuyTokenInput,
+ })
+ async buyToken(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ try {
+ const bondAddress = getBondAddress(walletProvider.getNetwork().networkId!);
+ const recipient = args.recipient || walletProvider.getAddress();
+
+ const bondInfo = await getTokenBond(walletProvider, args.tokenAddress);
+ if (!bondInfo) {
+ return `Error: ${args.tokenAddress} is not a valid Mint Club V2 token.`;
+ }
+
+ // Check and handle ERC20 approval for reserve token
+ const approvalNeeded = await needsApproval(
+ walletProvider,
+ bondInfo.reserveToken,
+ walletProvider.getAddress(),
+ args.maxReserveAmount,
+ );
+
+ if (approvalNeeded) {
+ const approveData = encodeFunctionData({
+ abi: ERC20_ABI,
+ functionName: "approve",
+ args: [bondAddress as `0x${string}`, BigInt(args.maxReserveAmount)],
+ });
+
+ const approveTxHash = await walletProvider.sendTransaction({
+ to: bondInfo.reserveToken as `0x${string}`,
+ data: approveData,
+ });
+
+ await walletProvider.waitForTransactionReceipt(approveTxHash);
+ }
+
+ const mintData = encodeFunctionData({
+ abi: MCV2_BOND_ABI,
+ functionName: "mint",
+ args: [
+ args.tokenAddress as `0x${string}`,
+ BigInt(args.tokensToMint),
+ BigInt(args.maxReserveAmount),
+ recipient as `0x${string}`,
+ ],
+ });
+
+ const txHash = await walletProvider.sendTransaction({
+ to: bondAddress as `0x${string}`,
+ data: mintData,
+ });
+
+ await walletProvider.waitForTransactionReceipt(txHash);
+
+ return `Successfully minted Mint Club tokens. Transaction hash: ${txHash}`;
+ } catch (error) {
+ return `Error buying Mint Club tokens: ${error}`;
+ }
+ }
+
+ /**
+ * Sells Mint Club tokens via the bonding curve.
+ *
+ * @param walletProvider - The wallet provider to sell tokens from.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the sale details.
+ */
+ @CreateAction({
+ name: "sell_token",
+ description: `
+This tool sells (burns) Mint Club V2 tokens via the bonding curve on Base.
+Do not use this tool for selling other types of tokens.
+
+Inputs:
+- Token contract address
+- Amount of tokens to burn (in wei)
+- Minimum refund to receive (in wei, for slippage protection)
+
+Important notes:
+- Amounts are in wei (no decimal points). 1 token = 10^decimals wei.
+- The min refund protects against slippage — the transaction reverts if refund is less.
+- Token approval for the Bond contract is handled automatically.
+- Only supported on Base mainnet.`,
+ schema: MintclubSellTokenInput,
+ })
+ async sellToken(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ try {
+ const bondAddress = getBondAddress(walletProvider.getNetwork().networkId!);
+ const recipient = args.recipient || walletProvider.getAddress();
+
+ // Verify sufficient balance
+ const balance = (await walletProvider.readContract({
+ address: args.tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: "balanceOf",
+ args: [walletProvider.getAddress() as `0x${string}`],
+ })) as bigint;
+
+ if (balance < BigInt(args.tokensToBurn)) {
+ return `Error: Insufficient balance. You have ${balance.toString()} wei but are trying to sell ${args.tokensToBurn} wei.`;
+ }
+
+ // Check and handle token approval
+ const approvalNeeded = await needsApproval(
+ walletProvider,
+ args.tokenAddress,
+ walletProvider.getAddress(),
+ args.tokensToBurn,
+ );
+
+ if (approvalNeeded) {
+ const approveData = encodeFunctionData({
+ abi: ERC20_ABI,
+ functionName: "approve",
+ args: [bondAddress as `0x${string}`, BigInt(args.tokensToBurn)],
+ });
+
+ const approveTxHash = await walletProvider.sendTransaction({
+ to: args.tokenAddress as `0x${string}`,
+ data: approveData,
+ });
+
+ await walletProvider.waitForTransactionReceipt(approveTxHash);
+ }
+
+ const burnData = encodeFunctionData({
+ abi: MCV2_BOND_ABI,
+ functionName: "burn",
+ args: [
+ args.tokenAddress as `0x${string}`,
+ BigInt(args.tokensToBurn),
+ BigInt(args.minRefund),
+ recipient as `0x${string}`,
+ ],
+ });
+
+ const txHash = await walletProvider.sendTransaction({
+ to: bondAddress as `0x${string}`,
+ data: burnData,
+ });
+
+ await walletProvider.waitForTransactionReceipt(txHash);
+
+ return `Successfully sold Mint Club tokens. Transaction hash: ${txHash}`;
+ } catch (error) {
+ return `Error selling Mint Club tokens: ${error}`;
+ }
+ }
+
+ /**
+ * Creates a new Mint Club token with a bonding curve.
+ *
+ * @param walletProvider - The wallet provider to create the token from.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the token creation details.
+ */
+ @CreateAction({
+ name: "create_token",
+ description: `
+This tool creates a new Mint Club V2 ERC20 token with a bonding curve on Base.
+
+Inputs:
+- Token name and symbol
+- Reserve token address (the asset that backs the bonding curve)
+- Maximum supply (in wei)
+- Step ranges and step prices arrays (define the bonding curve shape)
+- Mint and burn royalty (in basis points, e.g., 100 = 1%)
+
+The bonding curve is defined by parallel arrays of step ranges and prices:
+- stepRanges: cumulative supply thresholds (e.g., ["500000000000000000000000", "1000000000000000000000000"])
+- stepPrices: price per token at each step (e.g., ["10000000000000000", "100000000000000000"])
+The last stepRange must equal maxSupply.
+
+Important notes:
+- May require a creation fee (paid in ETH)
+- Royalties are in basis points: 100 = 1%, 10000 = 100%
+- Only supported on Base mainnet`,
+ schema: MintclubCreateTokenInput,
+ })
+ async createToken(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ try {
+ const bondAddress = getBondAddress(walletProvider.getNetwork().networkId!);
+
+ // Check creation fee
+ const creationFee = (await walletProvider.readContract({
+ address: bondAddress as `0x${string}`,
+ abi: MCV2_BOND_ABI,
+ functionName: "creationFee",
+ args: [],
+ })) as bigint;
+
+ const tokenParams = {
+ name: args.name,
+ symbol: args.symbol,
+ };
+
+ const bondParams = {
+ mintRoyalty: args.mintRoyalty,
+ burnRoyalty: args.burnRoyalty,
+ reserveToken: args.reserveToken as `0x${string}`,
+ maxSupply: BigInt(args.maxSupply),
+ stepRanges: args.stepRanges.map((r: string) => BigInt(r)),
+ stepPrices: args.stepPrices.map((p: string) => BigInt(p)),
+ };
+
+ const createData = encodeFunctionData({
+ abi: MCV2_BOND_ABI,
+ functionName: "createToken",
+ args: [tokenParams, bondParams],
+ });
+
+ const txHash = await walletProvider.sendTransaction({
+ to: bondAddress as `0x${string}`,
+ data: createData,
+ value: creationFee,
+ });
+
+ await walletProvider.waitForTransactionReceipt(txHash);
+
+ return `Successfully created Mint Club token "${args.name}" (${args.symbol}). Transaction hash: ${txHash}
+
+The token contract address can be found in the transaction logs (TokenCreated event).`;
+ } catch (error) {
+ return `Error creating Mint Club token: ${error}`;
+ }
+ }
+
+ /**
+ * Checks if the Mint Club action provider supports the given network.
+ *
+ * @param network - The network to check.
+ * @returns True if the network is supported, false otherwise.
+ */
+ supportsNetwork = (network: Network) =>
+ network.protocolFamily === "evm" &&
+ SUPPORTED_NETWORKS.includes(network.networkId!);
+}
+
+export const mintclubActionProvider = () => new MintclubActionProvider();
diff --git a/typescript/agentkit/src/action-providers/mintclub/schemas.ts b/typescript/agentkit/src/action-providers/mintclub/schemas.ts
new file mode 100644
index 000000000..b977fadae
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/schemas.ts
@@ -0,0 +1,147 @@
+import { z } from "zod";
+import { isAddress } from "viem";
+
+const ethereumAddress = z.custom<`0x${string}`>(
+ (val) => typeof val === "string" && isAddress(val),
+ "Invalid Ethereum address",
+);
+
+/**
+ * Input schema for getting token information.
+ */
+export const MintclubGetTokenInfoInput = z
+ .object({
+ tokenAddress: ethereumAddress.describe(
+ "The Mint Club V2 token contract address to get information for",
+ ),
+ })
+ .strip()
+ .describe("Instructions for getting Mint Club token information");
+
+/**
+ * Input schema for getting token price.
+ */
+export const MintclubGetTokenPriceInput = z
+ .object({
+ tokenAddress: ethereumAddress.describe(
+ "The Mint Club V2 token contract address to get price for",
+ ),
+ amount: z
+ .string()
+ .regex(/^\d+(\.\d+)?$/, "Must be a valid number")
+ .describe(
+ "Amount of tokens to price in whole units (e.g., '1' or '100.5')",
+ ),
+ })
+ .strip()
+ .describe("Instructions for getting Mint Club token price");
+
+/**
+ * Input schema for buying tokens.
+ */
+export const MintclubBuyTokenInput = z
+ .object({
+ tokenAddress: ethereumAddress.describe(
+ "The Mint Club V2 token contract address to buy",
+ ),
+ tokensToMint: z
+ .string()
+ .regex(/^\d+$/, "Must be a valid wei amount (no decimals)")
+ .describe(
+ "Amount of tokens to mint in wei (e.g., '1000000000000000000' for 1 token with 18 decimals)",
+ ),
+ maxReserveAmount: z
+ .string()
+ .regex(/^\d+$/, "Must be a valid wei amount (no decimals)")
+ .describe(
+ "Maximum reserve tokens to spend in wei (slippage protection — reverts if cost exceeds this)",
+ ),
+ recipient: ethereumAddress
+ .optional()
+ .describe(
+ "Address to receive the minted tokens (defaults to sender if omitted)",
+ ),
+ })
+ .strip()
+ .describe("Instructions for buying Mint Club tokens via bonding curve");
+
+/**
+ * Input schema for selling tokens.
+ */
+export const MintclubSellTokenInput = z
+ .object({
+ tokenAddress: ethereumAddress.describe(
+ "The Mint Club V2 token contract address to sell",
+ ),
+ tokensToBurn: z
+ .string()
+ .regex(/^\d+$/, "Must be a valid wei amount (no decimals)")
+ .describe(
+ "Amount of tokens to burn in wei (e.g., '1000000000000000000' for 1 token with 18 decimals)",
+ ),
+ minRefund: z
+ .string()
+ .regex(/^\d+$/, "Must be a valid wei amount (no decimals)")
+ .describe(
+ "Minimum reserve tokens to receive in wei (slippage protection — reverts if refund is less)",
+ ),
+ recipient: ethereumAddress
+ .optional()
+ .describe(
+ "Address to receive the reserve token refund (defaults to sender if omitted)",
+ ),
+ })
+ .strip()
+ .describe("Instructions for selling Mint Club tokens via bonding curve");
+
+/**
+ * Input schema for creating tokens.
+ */
+export const MintclubCreateTokenInput = z
+ .object({
+ name: z
+ .string()
+ .min(1)
+ .describe("The name of the token to create (e.g., 'My Token')"),
+ symbol: z
+ .string()
+ .min(1)
+ .describe("The symbol of the token to create (e.g., 'MTK')"),
+ reserveToken: ethereumAddress.describe(
+ "The reserve token address that backs the bonding curve (e.g., HUNT, USDC, or WETH address)",
+ ),
+ maxSupply: z
+ .string()
+ .regex(/^\d+$/, "Must be a valid wei amount (no decimals)")
+ .describe("Maximum token supply in wei"),
+ stepRanges: z
+ .array(z.string().regex(/^\d+$/, "Must be a valid wei amount"))
+ .min(1)
+ .describe(
+ "Cumulative supply thresholds for each bonding curve step in wei. Last value must equal maxSupply.",
+ ),
+ stepPrices: z
+ .array(z.string().regex(/^\d+$/, "Must be a valid wei amount"))
+ .min(1)
+ .describe(
+ "Price per token at each bonding curve step (multiplied by 10^18 for precision). Must have same length as stepRanges.",
+ ),
+ mintRoyalty: z
+ .number()
+ .int()
+ .min(0)
+ .max(5000)
+ .describe(
+ "Mint royalty in basis points (0–5000, where 100 = 1%). Max 50%.",
+ ),
+ burnRoyalty: z
+ .number()
+ .int()
+ .min(0)
+ .max(5000)
+ .describe(
+ "Burn royalty in basis points (0–5000, where 100 = 1%). Max 50%.",
+ ),
+ })
+ .strip()
+ .describe("Instructions for creating a new Mint Club V2 token with bonding curve");
diff --git a/typescript/agentkit/src/action-providers/mintclub/utils.ts b/typescript/agentkit/src/action-providers/mintclub/utils.ts
new file mode 100644
index 000000000..0b75ea9a8
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/mintclub/utils.ts
@@ -0,0 +1,244 @@
+import { EvmWalletProvider } from "../../wallet-providers";
+import {
+ MCV2_BOND_ABI,
+ ERC20_ABI,
+ SPOT_PRICE_AGGREGATOR_ABI,
+ getBondAddress,
+ getSpotPriceAggregatorAddress,
+ getUsdcAddress,
+} from "./constants";
+import { formatUnits } from "viem";
+
+/**
+ * Gets detailed information about a token bond.
+ *
+ * @param wallet - The wallet provider to use for contract calls.
+ * @param tokenAddress - Address of the token contract.
+ * @returns Token bond information or null if not a Mint Club token.
+ */
+export async function getTokenBond(
+ wallet: EvmWalletProvider,
+ tokenAddress: string,
+): Promise<{
+ creator: string;
+ mintRoyalty: number;
+ burnRoyalty: number;
+ createdAt: number;
+ reserveToken: string;
+ reserveBalance: string;
+} | null> {
+ try {
+ const bondAddress = getBondAddress(wallet.getNetwork().networkId!);
+
+ const bondInfo = (await wallet.readContract({
+ address: bondAddress as `0x${string}`,
+ abi: MCV2_BOND_ABI,
+ functionName: "tokenBond",
+ args: [tokenAddress as `0x${string}`],
+ })) as [string, number, number, number, string, bigint];
+
+ // createdAt == 0 means the token is not registered on Mint Club
+ if (bondInfo[3] === 0) {
+ return null;
+ }
+
+ return {
+ creator: bondInfo[0],
+ mintRoyalty: bondInfo[1],
+ burnRoyalty: bondInfo[2],
+ createdAt: bondInfo[3],
+ reserveToken: bondInfo[4],
+ reserveBalance: bondInfo[5].toString(),
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Gets basic token information (symbol, decimals, total supply).
+ *
+ * @param wallet - The wallet provider to use for contract calls.
+ * @param tokenAddress - Address of the token contract.
+ * @returns Token information or null if the contract call fails.
+ */
+export async function getTokenInfo(
+ wallet: EvmWalletProvider,
+ tokenAddress: string,
+): Promise<{
+ symbol: string;
+ decimals: number;
+ totalSupply: string;
+ formattedSupply: string;
+} | null> {
+ try {
+ const [symbol, decimals, totalSupply] = await Promise.all([
+ wallet.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: "symbol",
+ args: [],
+ }) as Promise,
+ wallet.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: "decimals",
+ args: [],
+ }) as Promise,
+ wallet.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: "totalSupply",
+ args: [],
+ }) as Promise,
+ ]);
+
+ return {
+ symbol,
+ decimals,
+ totalSupply: totalSupply.toString(),
+ formattedSupply: formatUnits(totalSupply, decimals),
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Gets quote for buying tokens from the bonding curve.
+ *
+ * @param wallet - The wallet provider to use for contract calls.
+ * @param tokenAddress - Address of the token contract.
+ * @param tokensToMint - Amount of tokens to mint (in wei).
+ * @returns The quote containing reserve amount and royalty, or null on failure.
+ */
+export async function getBuyQuote(
+ wallet: EvmWalletProvider,
+ tokenAddress: string,
+ tokensToMint: string,
+): Promise<{
+ reserveAmount: string;
+ royalty: string;
+} | null> {
+ try {
+ const bondAddress = getBondAddress(wallet.getNetwork().networkId!);
+
+ const quote = (await wallet.readContract({
+ address: bondAddress as `0x${string}`,
+ abi: MCV2_BOND_ABI,
+ functionName: "getReserveForToken",
+ args: [tokenAddress as `0x${string}`, BigInt(tokensToMint)],
+ })) as [bigint, bigint];
+
+ return {
+ reserveAmount: quote[0].toString(),
+ royalty: quote[1].toString(),
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Gets quote for selling tokens to the bonding curve.
+ *
+ * @param wallet - The wallet provider to use for contract calls.
+ * @param tokenAddress - Address of the token contract.
+ * @param tokensToBurn - Amount of tokens to burn (in wei).
+ * @returns The quote containing refund amount and royalty, or null on failure.
+ */
+export async function getSellQuote(
+ wallet: EvmWalletProvider,
+ tokenAddress: string,
+ tokensToBurn: string,
+): Promise<{
+ refundAmount: string;
+ royalty: string;
+} | null> {
+ try {
+ const bondAddress = getBondAddress(wallet.getNetwork().networkId!);
+
+ const quote = (await wallet.readContract({
+ address: bondAddress as `0x${string}`,
+ abi: MCV2_BOND_ABI,
+ functionName: "getRefundForTokens",
+ args: [tokenAddress as `0x${string}`, BigInt(tokensToBurn)],
+ })) as [bigint, bigint];
+
+ return {
+ refundAmount: quote[0].toString(),
+ royalty: quote[1].toString(),
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Gets the USD price of a reserve token using 1inch Spot Price Aggregator.
+ * Returns the rate as USD per one full unit of the reserve token.
+ *
+ * @param wallet - The wallet provider to use for contract calls.
+ * @param reserveTokenAddress - Address of the reserve token.
+ * @returns USD price per token or null if unable to fetch.
+ */
+export async function getUsdRate(
+ wallet: EvmWalletProvider,
+ reserveTokenAddress: string,
+): Promise {
+ try {
+ const aggregatorAddress = getSpotPriceAggregatorAddress(
+ wallet.getNetwork().networkId!,
+ );
+ const usdcAddress = getUsdcAddress(wallet.getNetwork().networkId!);
+
+ // getRate returns USDC-denominated rate per 1 full unit of srcToken
+ // The returned value has the same decimals as dstToken (USDC = 6)
+ const rate = (await wallet.readContract({
+ address: aggregatorAddress as `0x${string}`,
+ abi: SPOT_PRICE_AGGREGATOR_ABI,
+ functionName: "getRate",
+ args: [
+ reserveTokenAddress as `0x${string}`,
+ usdcAddress as `0x${string}`,
+ false,
+ ],
+ })) as bigint;
+
+ // Rate is in USDC units (6 decimals)
+ return Number(rate) / 1e6;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Checks if approval is needed for the Bond contract to spend tokens.
+ *
+ * @param wallet - The wallet provider to use for contract calls.
+ * @param tokenAddress - Address of the token to check allowance for.
+ * @param ownerAddress - Address of the token owner.
+ * @param amountNeeded - Amount that needs to be approved (in wei).
+ * @returns True if approval is needed, false otherwise.
+ */
+export async function needsApproval(
+ wallet: EvmWalletProvider,
+ tokenAddress: string,
+ ownerAddress: string,
+ amountNeeded: string,
+): Promise {
+ try {
+ const bondAddress = getBondAddress(wallet.getNetwork().networkId!);
+
+ const allowance = (await wallet.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: "allowance",
+ args: [ownerAddress as `0x${string}`, bondAddress as `0x${string}`],
+ })) as bigint;
+
+ return allowance < BigInt(amountNeeded);
+ } catch {
+ return true;
+ }
+}
From c559b1b764dde1f09cbf603b8d316dd914a2b14e Mon Sep 17 00:00:00 2001
From: h1-hunt
Date: Fri, 13 Feb 2026 16:47:33 +0000
Subject: [PATCH 2/2] fix: add trailing newlines to README.md and index.ts
---
typescript/agentkit/src/action-providers/mintclub/README.md | 2 +-
typescript/agentkit/src/action-providers/mintclub/index.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/typescript/agentkit/src/action-providers/mintclub/README.md b/typescript/agentkit/src/action-providers/mintclub/README.md
index 458df5623..181791c65 100644
--- a/typescript/agentkit/src/action-providers/mintclub/README.md
+++ b/typescript/agentkit/src/action-providers/mintclub/README.md
@@ -39,4 +39,4 @@ The Mint Club provider supports Base mainnet only.
Mint Club V2 is a bonding curve token protocol that allows anyone to create tokens backed by reserve assets. The protocol uses mathematical curves to determine token prices based on supply and demand.
-For more information on the **Mint Club V2 protocol**, visit [Mint Club Documentation](https://mint.club/).
\ No newline at end of file
+For more information on the **Mint Club V2 protocol**, visit [Mint Club Documentation](https://mint.club/).
diff --git a/typescript/agentkit/src/action-providers/mintclub/index.ts b/typescript/agentkit/src/action-providers/mintclub/index.ts
index 9379c974a..fe8322bd9 100644
--- a/typescript/agentkit/src/action-providers/mintclub/index.ts
+++ b/typescript/agentkit/src/action-providers/mintclub/index.ts
@@ -1,2 +1,2 @@
export * from "./schemas";
-export * from "./mintclubActionProvider";
\ No newline at end of file
+export * from "./mintclubActionProvider";