ChatGPT changed how we interact with information. Assistants like Alexa and Siri made our lives easier with just a voice command. As the internet evolves, crypto needs the same shift: intuitive experiences that lower barriers for everyone.
Solana proved that better UX wins. AI agents are the next step: autonomous programs that process data, adapt, and execute tasks with minimal human input. Imagine an AI that moves USDC to the best yield farm, sends tokens for rent every month, or analyzes DAO proposals - all without manual intervention.
Today, we'll guide you through building a Neon EVM plugin for ElizaOS, helping devs integrate AI-driven automation into dApps deployed on Solana via Neon EVM.
Ai16z’s ElizaOS is a TypeScript-based multi-agent AI framework that enables developers to create autonomous AI agents for DeFi applications. It supports:
Neon EVM is Solana’s network extension that brings Ethereum dApps to Solana. It allows EVM smart contracts to run on Solana’s high-performance network while preserving EVM compatibility.
Our plugin will act as a bridge, enabling ElizaOS to execute EVM transactions on Neon. We will use token transfers as an example, but the same approach can be applied to automate a variety of on-chain actions.
This serves as a first step: Neon’s EVM-Solana interoperability allows developers to expand AI agent capabilities on Solana, unlocking:
All while tapping into Solana’s unified liquidity and user base.
Let’s get started!
In this tutorial, we will go through the steps to implement a plugin action that allows the transfer of the ERC-20 NEON token on Neon EVM. If you have NEON as SPL tokens on Solana, please use NeonPass to receive the ERC-20 version.
Note: ElizaOS is still under active development. The exact steps and APIs might change. This section provides a conceptual outline and illustrative code snippets to guide you. You'll need to adapt these to the specific ElizaOS and Neon EVM versions you are working with.
Clone the template repository and install dependencies.
git clone https://github.com/tmc/eliza-plugin-starter
cd eliza-plugin-starter
pnpm install
Open up the eliza-starter code in your code editor and ensure that you are in the right directory
cd eliza-starter
Run the following command to install all necessary packages.
pnpm i
Duplicate the example env file template
cp .env.example .env
And now here you will have to add in env variables for NEONADDRESS, NEONPRIVATEKEY, NEONRPCURL and finally the LLM’s API Key in our case it will be OPENAIKEY
Now let's get our API key. In this tutorial I will be using OpenAI API key, but you can try other models as well.
Click over Start building -> Login -> Get API key -> Create API key -> Give a name to your API key.
Replace it under the .env file under the keyword OPENAI_KEY.
In this example I will be using a Metamask wallet. Here’s how to do it:
Now let’s implement the action we want Eliza to be able to perform on Neon.
Below is a step-by-step guide to building a NEON token transfer action for your Eliza plugin. We will walk you through setting up your environment, writing the code, and understanding each section of the file.
-Create or Navigate to Your Plugin Project: If you don’t already have a plugin project for Eliaza, create a new one or navigate to your existing project directory.
-Install Required Dependencies: Ensure you have the following dependencies installed in your project:
npm install @elizaos/core viem zod
These packages include:
Create a new TypeScript file (e.g., transfer.ts) inside your plugin’s directory. This file will house the Neon token transfer action.
Start by importing necessary types, functions, and constants from the core libraries and helper modules:
import type { Action } from "@elizaos/core";
import {
ActionExample,
Content,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
elizaLogger,
composeContext,
generateObject,
} from "@elizaos/core";
import { validateAbstractConfig } from "../environment";
import {
Address,
createWalletClient,
erc20Abi,
http,
parseEther,
isAddress,
parseUnits,
createPublicClient,
} from "viem";
import { neonDevnet, neonMainnet } from "viem/chains";
import { normalize } from "viem/ens";
import { z } from "zod";
import { ValidateContext } from "../utils";
import { NEON_ADDRESS, ERC20_OVERRIDE_INFO } from "../constants";
import { useGetAccount, useGetWalletClient } from "../hooks";
Explanation:
Create a public client that targets the Neon mainnet (or devnet, depending on your use case):
const ethereumClient = createPublicClient({
chain: neonMainnet,
transport: http(),
});
Explanation:
This client is used later to resolve ENS names and interact with the blockchain.
Use zod to define a schema that validates the data required for a token transfer:
const TransferSchema = z.object({
tokenAddress: z.string().nullable(),
recipient: z.string(),
amount: z.string(),
});
Explanation:
Define an interface extending the core Content interface to include transfer-specific fields:
export interface TransferContent extends Content {
tokenAddress: string | null;
recipient: string;
amount: string | number;
}
This prompt instructs the language model to extract necessary transfer details from a conversation:
const transferTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
Here are several frequently used addresses. Use these for the corresponding tokens:
- ETH/eth: 0x000000000000000000000000000000000000800A
- USDC/usdc: 0xe4c7fbb0a626ed208021ccaba6be1566905e2dfc
Example response:
\\`\\`\\`json
{
"tokenAddress": "0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E",
"recipient": "0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62",
"amount": "1000"
}
\\`\\`\\`
{{recentMessages}}
Given the recent messages, extract the following information about the requested token transfer:
- Token contract address
- Recipient wallet address
- Amount to transfer
Respond with a JSON markdown block containing only the extracted values.`;
Explanation:
This template is used to generate a structured object from the conversation history, ensuring consistency in the expected input format.
Now, define the transferAction object that implements the Action interface:
export const transferAction: Action = {
name: "SEND_TOKEN",
similes: [
"TRANSFER_TOKEN_ON_NEON",
"TRANSFER_TOKENS_ON_NEON",
"SEND_TOKENS_ON_NEON",
"SEND_NEON",
"PAY_ON_NEON",
"MOVE_TOKENS_ON_NEON",
"MOVE_NEON",
],
validate: async (runtime: IAgentRuntime, message: Memory) => {
// TODO: Add Neon-specific validation if needed
return true;
},
description: "Transfer tokens from the agent's wallet to another address",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: { [key: string]: unknown },
callback?: HandlerCallback
): Promise<boolean> => {
elizaLogger.log("Starting Neon SEND_TOKEN handler...");
// Initialize or update state
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}
// Compose transfer context using the prompt template
const transferContext = composeContext({
state,
template: transferTemplate,
});
// Generate transfer content using the schema
let content = (
await generateObject({
runtime,
context: transferContext,
modelClass: ModelClass.SMALL,
schema: TransferSchema,
})
).object as unknown as TransferContent;
// Resolve ENS name if the recipient address is not valid
if (!isAddress(content.recipient, { strict: false })) {
elizaLogger.log("Resolving ENS name...");
try {
const name = normalize(content.recipient.trim());
const resolvedAddress = await ethereumClient.getEnsAddress({
name,
});
if (isAddress(resolvedAddress, { strict: false })) {
elizaLogger.log(`${name} resolved to ${resolvedAddress}`);
content.recipient = resolvedAddress;
}
} catch (error) {
elizaLogger.error("Error resolving ENS name:", error);
}
}
// Use default token address if none provided (for NEON)
if (content.tokenAddress == null || content.tokenAddress == undefined) {
content.tokenAddress = NEON_ADDRESS;
}
console.log("CONTENT", content);
try {
const account = useGetAccount(runtime);
const walletClient = useGetWalletClient();
let hash;
// Check if the token is native NEON or an ERC20 token
if (
content.tokenAddress.toLowerCase() != NEON_ADDRESS.toLowerCase()
) {
// For ERC20 token transfer on Neon EVM
const tokenInfo =
ERC20_OVERRIDE_INFO[content.tokenAddress.toLowerCase()];
const decimals = tokenInfo?.decimals ?? 18; // Default decimals to 18 if unspecified
const tokenAmount = parseUnits(
content.amount.toString(),
decimals
);
hash = await walletClient.writeContract({
account,
chain: neonDevnet,
address: content.tokenAddress as Address,
abi: erc20Abi,
functionName: "transfer",
args: [content.recipient as Address, tokenAmount],
});
} else {
// For native NEON transfer
hash = await walletClient.sendTransaction({
account: account,
chain: neonDevnet,
to: content.recipient as Address,
value: parseEther(content.amount.toString()),
kzg: undefined,
});
}
elizaLogger.success(
"Transfer completed successfully! Transaction hash: " + hash
);
if (callback) {
callback({
text:
"Transfer completed successfully! Transaction hash: " +
hash,
content: {},
});
}
return true;
} catch (error) {
elizaLogger.error("Error during token transfer:", error);
if (callback) {
callback({
text: `Error transferring tokens: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
// Example conversation flows to illustrate usage
examples: [
[
{
user: "{{user1}}",
content: {
text: "Send 0.01 NEON to 0x114B242D931B47D5cDcEe7AF065856f70ee278C4",
},
},
{
user: "{{agent}}",
content: {
text: "Sure, I'll send 0.01 NEON to that address now.",
action: "SEND_TOKEN",
},
},
{
user: "{{agent}}",
content: {
text: "Successfully sent 0.01 NEON to 0x114B242D931B47D5cDcEe7AF065856f70ee278C4\\nTransaction: 0xdde850f9257365fffffc11324726ebdcf5b90b01c6eec9b3e7ab3e81fde6f14b",
},
},
],
// Additional examples follow...
] as ActionExample[][],
};
Clone the main Eliza repository — We will be using this having a client for us to interact with the character that we have now created.
git clone [email protected]:elizaOS/eliza.git
And change the directory into eliza and install the dependencies by doing
pnpm i
You can start the Eliza client by just executing the command
pnpm start:client
And once done right, it should give you a localhost link where the client is hosted.
Sending 1 NEON using the plugin looks like this:
We did it! We’ve just connected AI interactions (ElizaOS) together with EVM-compatible on-chain actions (Neon EVM).
Now it’s time to push AI agents on Solana even further by leveraging Neon’s interoperability.
Thanks for joining us on this journey to the intersection of AI and Neon. Whether you’re building a futuristic DeFi manager, a DAO governance sidekick, or just a helpful on-chain “cashier,” we can’t wait to see what you come up with.
Got questions, feedback, or just wanna vibe? Our Discord is the place to be.
Happy Building!
About Neon EVM:
Neon EVM is the first of its kind—a Network Extension on Solana—designed to seamlessly integrate Ethereum Virtual Machine (EVM) compatibility into Solana’s high-performance ecosystem. By operating natively within Solana’s base layer, Neon EVM provides Ethereum developers with a fast, high-throughput pathway to deploy their EVM dApps on Solana, without the need to rewrite their contracts in Rust.
For more information about Neon EVM and future updates, visit neonevm.org and connect with the community on Twitter or Discord.
Other articles