Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tool to purchase heurist credits #16

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ Example query to Claude:

> "Buy $20 worth of OpenRouter credits."

### buy_heurist_credits

Purchases Heurist API credits with cryptocurrency on the Base chain.

Parameters:

- `tokenSymbol`: The token to use for payment (USDC, HEU, or WETH)
- `amount`: The amount of tokens to spend (minimum: 1 USDC, 10 HEU, or 0.001 WETH)

Example query to Claude:

> "Purchase 2 USDC worth of Heurist API credits."
> "Buy 10 HEU tokens of Heurist credits."
> "Buy Heurist API credits using 0.002 WETH."

## Security Considerations

- The configuration file contains sensitive information (API keys and seed phrases). Ensure it's properly secured and not shared.
Expand Down
82 changes: 82 additions & 0 deletions examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,88 @@ _Claude uses the `buy_openrouter_credits` tool with the following parameters:_

**Claude:** I've initiated the purchase of $20 worth of OpenRouter credits using your USDC. The transaction hash is: 0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234

## Buying Heurist API Credits

The `buy_heurist_credits` tool allows you to purchase Heurist API credits using cryptocurrency on the Base chain. You can use USDC, HEU, or WETH tokens for payment.

### Example 1: Purchasing with USDC

**User:** I'd like to purchase some Heurist API credits. Can I use 2 USDC?

**Claude:**

I'll help you purchase Heurist API credits using USDC.

First, I'll check your USDC balance to make sure you have enough funds.

[Claude uses the erc20_balance tool to check your USDC balance]

Great! You have sufficient USDC. I'll now purchase Heurist API credits for you.

[Claude uses the buy_heurist_credits tool]

✅ Successfully purchased Heurist API credits with 1 USDC.

Transaction: https://basescan.org/tx/0xf9f569c79fe246c40171c1c25e517a13e6a02551093b1b4309d090292d7ceed4

You can check your credits balance and manage your API keys by visiting https://www.heurist.ai/credits and connecting with the same wallet address that you used for this purchase.

### Example 2: Purchasing with HEU Tokens

**User:** I want to buy Heurist API credits using 15 HEU tokens.

**Claude:**

I'll help you purchase Heurist API credits using HEU tokens.

First, I'll check if you have enough HEU tokens in your wallet.

[Claude uses the erc20_balance tool to check your HEU balance]

You have enough HEU tokens. I'll now purchase the Heurist API credits for you.

[Claude uses the buy_heurist_credits tool]

✅ Successfully purchased Heurist API credits with 15 HEU.

Transaction: https://basescan.org/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

You can check your credits balance and manage your API keys by visiting https://www.heurist.ai/credits and connecting with the same wallet address (0x1234567890abcdef1234567890abcdef12345678) that you used for this purchase.

### Example 3: Purchasing with WETH

**User:** Can I buy Heurist API credits with WETH? I want to spend 0.005 WETH.

**Claude:**

I'll help you purchase Heurist API credits using WETH.

First, I'll verify your WETH balance.

[Claude uses the erc20_balance tool to check your WETH balance]

You have sufficient WETH. I'll now purchase Heurist API credits for you.

[Claude uses the buy_heurist_credits tool]

✅ Successfully purchased Heurist API credits with 0.005 WETH.

Transaction: https://basescan.org/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

You can check your credits balance and manage your API keys by visiting https://www.heurist.ai/credits and connecting with the same wallet address (0x1234567890abcdef1234567890abcdef12345678) that you used for this purchase.

### Tips for Buying Heurist API Credits

- Minimum amounts required:
- USDC: 1.0 USDC
- HEU: 10 HEU
- WETH: 0.001 WETH
- Make sure you have sufficient ETH for gas fees
- The transaction requires approval for the Heurist contract to spend your tokens (this is handled automatically)
- Credits are allocated to the wallet address used for the purchase
- After purchase, visit https://www.heurist.ai/credits to check your balance and manage your API keys
- Connect with the same wallet address you used for purchasing credits

## Tips for Effective Use

1. **Be specific in your requests** - Provide all necessary details when asking Claude to perform blockchain operations.
Expand Down
Binary file added public/BuyHeuristCredits.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
201 changes: 201 additions & 0 deletions src/tools/heurist/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import {
erc20Abi,
formatUnits,
parseUnits,
type PublicActions,
type WalletClient,
} from 'viem';
import { waitForTransactionReceipt } from 'viem/actions';
import { base } from 'viem/chains';
import type { z } from 'zod';
import {
checkToolSupportsChain,
constructBaseScanUrl,
} from '../utils/index.js';
import { BuyHeuristCreditsSchema, MINIMUM_AMOUNTS, SUPPORTED_TOKENS } from './schemas.js';

// Heurist Contract ABI (partial, just what we need)
const HEURIST_CONTRACT_ABI = [
{
inputs: [
{
internalType: 'address',
name: 'tokenAddress',
type: 'address',
},
{
internalType: 'address',
name: 'creditedAddress',
type: 'address',
},
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'purchaseCredits',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'tokenAddress',
type: 'address',
}
],
name: 'isAcceptedToken',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
}
],
stateMutability: 'view',
type: 'function',
}
] as const;

// Heurist contract address on Base
const HEURIST_CONTRACT_ADDRESS = '0x59d944b7ff8c432ff395683f5c95d97ca0237986' as const;

// Token decimals mapping
const TOKEN_DECIMALS = {
USDC: 6, // USDC typically has 6 decimals
HEU: 18, // HEU has 18 decimals (standard ERC20)
WETH: 18, // WETH has 18 decimals
} as const;

export async function buyHeuristCreditsHandler(
wallet: WalletClient & PublicActions,
args: z.infer<typeof BuyHeuristCreditsSchema>,
): Promise<string> {
const { tokenSymbol, amount } = args;

// Check if we're on Base chain
checkToolSupportsChain({
chainId: wallet.chain?.id,
supportedChains: [base],
});

const address = wallet.account?.address;
if (!address) {
throw new Error('No wallet address found');
}

// Get token address
const tokenAddress = SUPPORTED_TOKENS[tokenSymbol];
const decimals = TOKEN_DECIMALS[tokenSymbol];

// Check if the token is accepted by the contract
let isTokenAccepted = false;
try {
isTokenAccepted = await wallet.readContract({
address: HEURIST_CONTRACT_ADDRESS,
abi: HEURIST_CONTRACT_ABI,
functionName: 'isAcceptedToken',
args: [tokenAddress],
});
} catch {
// Silently continue - will fail later if token is not accepted
}

if (isTokenAccepted === false) {
// We got a valid response that the token is not accepted
throw new Error(`${tokenSymbol} (${tokenAddress}) is not currently accepted by the Heurist contract. Please try another token.`);
}

// For small amounts, ensure they meet minimum requirements
// The contract might have minimum purchase amounts
let adjustedAmount = amount;
const minimumAmount = MINIMUM_AMOUNTS[tokenSymbol];

if (amount < minimumAmount) {
adjustedAmount = minimumAmount;
// Silently adjust the amount to meet minimum requirements
}

// Convert amount to token units
const amountInTokenUnits = parseUnits(adjustedAmount.toString(), decimals);

// Check user's token balance
const tokenBalance = await wallet.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address],
});

if (tokenBalance < amountInTokenUnits) {
const formattedBalance = formatUnits(tokenBalance, decimals);
throw new Error(
`Insufficient ${tokenSymbol} balance. You have ${formattedBalance} ${tokenSymbol}, but ${adjustedAmount} ${tokenSymbol} is required.`
);
}

// Check if token is approved for the contract
const allowance = await wallet.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'allowance',
args: [address, HEURIST_CONTRACT_ADDRESS],
});

// Approve token if needed
if (allowance < amountInTokenUnits) {
const approveTxHash = await wallet.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [HEURIST_CONTRACT_ADDRESS, amountInTokenUnits],
chain: wallet.chain ?? undefined,
account: wallet.account ?? null,
});

await waitForTransactionReceipt(wallet, {
hash: approveTxHash,
});

// Token approved for Heurist contract
}

// Call purchaseCredits function
try {
const txHash = await wallet.writeContract({
address: HEURIST_CONTRACT_ADDRESS,
abi: HEURIST_CONTRACT_ABI,
functionName: 'purchaseCredits',
args: [tokenAddress, address, amountInTokenUnits],
chain: wallet.chain ?? undefined,
account: wallet.account ?? null,
});

await waitForTransactionReceipt(wallet, {
hash: txHash,
});

const txUrl = constructBaseScanUrl(base, txHash);

return `Successfully purchased Heurist API credits with ${adjustedAmount} ${tokenSymbol}. Transaction: ${txUrl}

You can check your credits balance and manage your API keys by visiting https://www.heurist.ai/credits and connecting with the same wallet address (${address}) that you used for this purchase.`;
} catch (error) {
// Error details for debugging but comply with linter rules

if (error instanceof Error) {
// Try with slightly more gas
if (error.message.includes('execution reverted')) {
throw new Error(`Transaction failed: The contract rejected the transaction. This might be due to:\n1. The contract not accepting this specific token currently\n2. The amount being too small or too large (try at least 1 USDC or equivalent)\n3. A temporary issue with the contract\n\nPlease try again with a different token or larger amount.`);
} else if (error.message.includes('insufficient funds')) {
throw new Error(`Transaction failed: Insufficient funds for gas. Please ensure you have enough ETH for transaction fees.`);
}
throw new Error(`Transaction failed: ${error.message}`);
}

throw new Error('Transaction failed for an unknown reason. Please try again with a different token or amount.');
}
}
10 changes: 10 additions & 0 deletions src/tools/heurist/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateTool } from '../../utils.js';
import { buyHeuristCreditsHandler } from './handlers.js';
import { BuyHeuristCreditsSchema } from './schemas.js';

export const buyHeuristCreditsTool = generateTool({
name: 'buy_heurist_credits',
description: 'Purchase Heurist API credits with cryptocurrency on Base chain',
inputSchema: BuyHeuristCreditsSchema,
toolHandler: buyHeuristCreditsHandler,
});
26 changes: 26 additions & 0 deletions src/tools/heurist/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from 'zod';

export const SUPPORTED_TOKENS = {
USDC: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
HEU: '0xef22cb48b8483df6152e1423b19df5553bbd818b',
WETH: '0x4200000000000000000000000000000000000006',
} as const;

// Define minimum amounts per token to avoid transaction failures
export const MINIMUM_AMOUNTS = {
USDC: 1, // Minimum 1 USDC
HEU: 10, // Minimum 10 HEU
WETH: 0.001, // Minimum 0.001 WETH
} as const;

export const BuyHeuristCreditsSchema = z.object({
tokenSymbol: z.enum(['USDC', 'HEU', 'WETH']).describe('The token to use for payment'),
amount: z.number().positive()
.refine(
(_val) => true, // Will be validated in handler
{
message: 'Amount must be greater than minimum required for the selected token',
}
)
.describe('The amount of tokens to spend (minimum: 1 USDC, 10 HEU, or 0.001 WETH). Credits will be allocated to your wallet address.'),
});
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { callContractTool } from './contracts/index.js';
import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js';
import { buyHeuristCreditsTool } from './heurist/index.js';
import { getMorphoVaultsTool } from './morpho/index.js';
import { getOnrampAssetsTool, onrampTool } from './onramp/index.js';
import { buyOpenRouterCreditsTool } from './open-router/index.js';
Expand All @@ -13,6 +14,7 @@ export const baseMcpTools: ToolWithHandler[] = [
erc20BalanceTool,
erc20TransferTool,
buyOpenRouterCreditsTool,
buyHeuristCreditsTool,
];

export const toolToHandler: Record<string, ToolHandler> = baseMcpTools.reduce<
Expand Down