Chainly is an AI agent for Ethereum Sepolia (chain ID 11155111). You ask questions in plain English. It reads live onchain data, analyzes wallet activity, and prepares transactions that you approve in your own wallet. It never signs anything for you.
Built by the CSEC-ASTU Blockchain Team for the Blockchain × AI session. Contributions welcome. See CONTRIBUTING.md.
| Route | What it is |
|---|---|
/ |
Landing page: hero with an animated product preview, capabilities, how it works, interactive examples |
/chat |
The agent workspace: collapsible history sidebar, streaming chat, tool steps, result cards, wallet |
- 💬 Conversational agent: a streaming chat with Markdown answers. Each tool call shows an expandable step with its raw JSON input and output, plus a rich result card.
- 🔎 Read: ETH balances, ERC-20 balances (USDC, WETH and LINK by symbol, or any contract), transaction details, gas prices, block info.
- 🧾 Decode transactions: what a transaction actually did, in plain English: the function it called with its arguments, token transfers, mints, approvals (unlimited ones are flagged), NFT moves and WETH wraps.
- 📜 Explain contracts: name, verification, token standards, proxy and upgradeability, owner, paused state, functions, and the privileged ones (mint, pause, upgrade…) that give someone special powers.
- 📊 Analyze: explorer history turned into money in and out, net flow, gas spent, counterparties, contract usage, a daily chart and insights backed by the data.
- ✍️ Prepare: ETH and ERC-20 token transfers. Validates the address, amount, balance and network, estimates gas, and shows a preview card. You sign in your wallet.
- 🏷️ ENS names: use
alice.ethanywhere an address is expected. Names resolve on Sepolia's own ENS registry (mainnet names may not exist there), and previews show the name next to the resolved address that your wallet signs. - 🛡️ Safety:
- No signing tools.
- The sender is locked to the connected wallet.
- The preview is re-checked in the browser before the wallet opens.
- Previews expire after 10 minutes.
- A preview that was already submitted can't be signed again from chat history.
- 🗂️ Local history: conversations are saved in
localStorage, with no database. Keyboard shortcuts:⌘Btoggles the sidebar,⇧⌘Ostarts a new chat. - 🌗 Dark by default, plus a light theme with no flash on load.
pnpm install
cp .env.example .env.local # add your AI Gateway key
pnpm dev # http://localhost:3000By default Chainly uses the Vercel AI Gateway with the free nvidia/nemotron-3.5-lightning-free model. Minimum .env.local:
AI_GATEWAY_API_KEY=... # Vercel dashboard → AI Gateway → API KeysOn Vercel deployments no key is needed: the gateway authenticates with the project's OIDC token. To try another gateway model, set LLM_MODEL (for example reflection/beam-free, or a paid one like anthropic/claude-sonnet-5-5).
To use a provider directly instead, set its key (and optionally LLM_PROVIDER):
LLM_PROVIDER=openai # openai | anthropic | google | openai-compatible
OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEYAny OpenAI-compatible provider (Groq, OpenRouter, Ollama) works too:
LLM_PROVIDER=openai-compatible
OPENAI_COMPATIBLE_BASE_URL=https://api.groq.com/openai/v1
OPENAI_COMPATIBLE_API_KEY=gsk_...
LLM_MODEL=llama-3.3-70b-versatilePick a model that supports tool calling.
| Variable | Purpose | Default |
|---|---|---|
SEPOLIA_RPC_URL |
Server RPC (can hold an Alchemy/Infura key; never sent to the browser) | public fallbacks: publicnode → drpc → 1rpc |
NEXT_PUBLIC_SEPOLIA_RPC_URL |
Browser RPC for the live block and wallet balance | publicnode |
ETHERSCAN_API_KEY |
Wallet history via Etherscan V2 (chainid=11155111) |
keyless Blockscout (eth-sepolia.blockscout.com) |
Explorer links point to sepolia.etherscan.io. Get test ETH from the Google Cloud Sepolia faucet. The wallet button prompts Switch to Sepolia when your wallet is on another network.
| Command | What it does |
|---|---|
pnpm dev |
Dev server |
pnpm build && pnpm start |
Production build and run |
pnpm lint |
ESLint |
pnpm typecheck |
Next route types + tsc --noEmit |
pnpm test |
Vitest unit tests |
Browser (React, wagmi) Server (Next.js route handler)
┌───────────────────────────────┐ POST ┌──────────────────────────────────────┐
│ /chat useChat ── messages ───┼────────────▶│ /api/chat │
│ + connected address │ │ streamText(model, instructions, │
│ │ SSE stream │ tools, stopWhen=8 steps) │
│ tool steps + result cards ◀───┼◀────────────┤ │ ▲ │
│ │ │ ▼ │ JSON │
│ TransactionPreviewCard │ │ lib/agent/tools.ts (10 typed tools) │
│ └ signing-guard (re-check) │ │ │ │
│ └ wallet.sendTransaction ◀── user click │ ▼ │
└──────────────┬────────────────┘ │ lib/blockchain/* (pure, tested) │
│ user signs │ reader · activity · transfer │
▼ │ explorer (Blockscout / Etherscan) │
Ethereum Sepolia ◀──────── viem RPC ───┤ │
└──────────────────────────────────────┘
src/
├─ app/
│ ├─ page.tsx ← landing page
│ ├─ chat/page.tsx ← agent workspace
│ ├─ api/chat/route.ts ← the agent loop (LLM ⇄ tools, streamed)
│ └─ api/status/route.ts ← non-secret config (LLM configured? which explorer?)
├─ lib/
│ ├─ agent/ ← model provider, tool definitions, instructions, types
│ ├─ blockchain/ ← chain config, reader, explorer, activity, transfer,
│ │ token-transfer, contracts, decode, ens,
│ │ signing-guard, tx-records, validation, errors
│ ├─ web3/wagmi.ts ← browser wallet config (injected wallets, Sepolia)
│ └─ chat-store.ts ← localStorage chat history
└─ components/
├─ landing/ ← hero, product preview, sections
├─ chat/ ← workspace, sidebar, chat view, composer, messages, tool steps
├─ cards/ ← balance, token, tx, decoded tx, contract, network, block, activity, tx preview
├─ wallet/ ← wallet button + menu, network indicator, identicon
└─ ui/ ← shadcn/ui-style primitives
The network lives in one file, src/lib/blockchain/chain.ts. Changing chains means editing that file (plus the token list in reader.ts).
| Tool | Input | Source | Returns |
|---|---|---|---|
getEthBalance |
address? (defaults to connected wallet) |
RPC | balance, EOA/contract, block |
getTokenBalance |
address?, token (symbol or contract) |
RPC (erc20Abi) |
balance with decimals, token metadata |
getTransaction |
hash |
RPC tx + receipt + block | status, value, fee, gas, confirmations |
decodeTransaction |
hash |
RPC tx + receipt, verified ABI from the explorer, standard ABIs | decoded call, events with plain-English summaries, flags |
explainContract |
address (or ENS name) |
RPC (code, EIP-1967 slot, probes), verified ABI from the explorer | standards, proxy, owner, functions, privileged functions |
getNetworkStatus |
none | RPC | latest block, gas price, base/priority fee, utilization |
getBlock |
blockNumber? |
RPC | block stats |
analyzeWallet |
address?, limit |
Blockscout / Etherscan V2 | stats, counterparties, daily chart, insights |
prepareTransaction |
to, amountEth |
RPC (fees, balance, estimateGas, getCode) | a preview with checks, readyToSign |
prepareTokenTransfer |
token, to, amount |
RPC (token metadata, balances, fees, estimateGas) | a preview with transfer calldata and checks, readyToSign |
- No capability: the toolbox has nothing that signs or broadcasts, and the server holds no keys.
- No spoofing: the sender comes from the connected wallet (captured in a closure), not from the LLM's arguments. This is unit-tested.
- Validated intent:
- checksummed addresses and a zero-address block
- locale-safe amounts (
"1,5"is rejected, not guessed) - a 10 ETH ceiling
- a balance check covering amount plus max gas (for tokens: the token balance, plus ETH for gas)
- no token transfers to the token's own contract
- a warning when the recipient is a contract
- Human in the loop: only a click on Approve in wallet opens the wallet.
- Defense in depth:
signing-guard.tsre-checks the chain ID, sender, amount and gas bounds, and a 10-minute expiry. For token transfers it decodes the calldata itself and refuses anything buttransfer(recipient, amount)matching the card.tx-records.tsstops a submitted preview from being re-signed when a chat is reopened. - No leaks: viem errors (which can include RPC URLs with keys) are mapped to clean messages.
- Untrusted contract data:
explainContractnever passes raw source code to the model, and names from ABIs are filtered to plain identifiers, so text planted in a contract can't easily steer the agent.
- Type: Geist Sans for UI and Geist Mono for data (addresses, hashes, tool names).
- Color: a monochrome neutral scale with a single violet accent (
--accent), used for focus, live and active states only. Status colors (success, warning, destructive) are muted. - Surfaces:
background→surface→surface-2with hairline borders (8% and 14% alpha). Tokens live insrc/app/globals.css. - Motion: short fade-up and fade-in entrances, a shimmer for the "thinking" state, scroll reveals, and an animated hero replay. All respect
prefers-reduced-motion.
- Open
/and let the hero preview cycle through Read → Analyze → Prepare. Then walk through How it works (Ask → Reason → Execute tools → Respond). - In Examples, pick a query and click Try it in Chainly. The query arrives pre-filled in the composer.
- "What's the ETH balance of 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14?" (Sepolia WETH). Expand the tool step to show the raw JSON.
- "How busy is Ethereum Sepolia right now?": gas, base fee and block utilization.
- Connect MetaMask on Sepolia, then: "Analyze my wallet's recent activity."
- "Check my balance, then prepare a transaction to send 0.001 ETH to 0x…". Review the checks, click Approve in wallet, sign, and watch it confirm. Click Ask agent to verify.
- Try to break it: a typo'd address,
"1,5 ETH", or "send from 0xSomeoneElse". - Code tour:
api/chat/route.ts→agent/tools.ts→blockchain/transfer.ts→signing-guard.ts.
pnpm testThe tests cover:
- validation and error redaction
- explorer parsing (Blockscout and Etherscan V2 for chainid 11155111)
- the wallet analytics math
- transaction preparation
- the signing guard and tx records
- the agent tool loop with a mocked LLM, including a prompt-injected
fromthat is ignored
- "LLM not configured" notice: set
AI_GATEWAY_API_KEY(or another provider's key) in.env.localand restart. - RPC errors: public endpoints are rate limited; set
SEPOLIA_RPC_URL. - Explorer rate limit: add a free
ETHERSCAN_API_KEY. - Wrong network: use the Switch to Sepolia button in the header.