// Research · 2026-09-28
Advice is not a control.
An agent that can buy a token has a path from the model's tool call to a signature. We read that path, file by file, in seven open-source toolkits that give a model a tool to buy tokens on Solana: Solana Agent Kit, GOAT, the ElizaOS Solana plugin, Coinbase AgentKit, Phantom's MCP server, OKX's Onchain OS skills and Jupiter's own CLI. In the open code of all seven, nothing between the tool call and the signature checks the token being bought.
Three of them do ship a token check. Solana Agent Kit and GOAT have a Rugcheck lookup, and OKX's CLI has a honeypot classifier and a security scan. In each case the check sits outside the command that signs. The model decides whether to call it, and the buy path never asks whether it did.
A check the model can decline is advice. The purchases a check exists to stop are the ones where the model's judgement has already been moved: by a persuasive user, by a token description written to persuade it, or by a symbol that names more than one token. A model in that state can skip advice. A control has to sit where the model cannot route around it, which is the signing path.
We include our own package, wormhole-x402, because it does sit in the signing path. It is a payment guard, not a token check. We gave it three Jupiter swaps. It refused all three and never passed one to the wallet's signing method, because a swap is not the payment it was written to allow. It cannot tell a good swap from a bad one.
Seven buy paths, read end to end
For each toolkit we started at the tool a model calls to buy, and followed every function to the point where a private key signs. The table records what sits on that path at the stated commit. "None" means not found after reading the whole path and tracing every caller of every token-check function in the repository.
| Toolkit · commit | Token check on the buy path | Token check the model may call | Simulation before signing | Slippage if unset | Other controls on the path |
|---|---|---|---|---|---|
| Solana Agent Kit · 1254fe5 | None | RUGCHECK action; nothing on the TRADE path calls it | None; RPC preflight after signing | Jupiter dynamic | Schema shape checks; optional signOnly; priority fee capped at 10,000,000 lamports. No spend cap, no confirmation |
| GOAT SDK · f2eedc2 | None | rugcheck plugin (5 tools); nothing outside the plugin calls it | None; RPC preflight after signing | Jupiter's default (measured: 50 bps) | None with the keypair client. Crossmint and Lit wallets sign outside the toolkit (not evaluated) |
| ElizaOS plugin-solana 1.2.6 · npm gitHead 646c632 | None | None found | None; RPC preflight after signing | Dynamic; service path starts at 200 bps, sell retries reach 3,200 | Keypair match; SOL balance on the service path. No spend cap, no confirmation |
| Coinbase AgentKit (TS) · 2e6dbaf | None | None found | None; RPC preflight after signing (keypair, CDP); Privy not visible | 50 bps | Solana mainnet only. No spend cap, no confirmation in toolkit code |
| Phantom MCP 1.2.8 · 618f616 | Unclear: none client-side; server step not visible | Unclear: simulate_transaction returns security warnings, contents not visible; buy_token never calls it | Server-side /prepare; the model cannot skip it; contents not visible | Auto, computed server-side | execute defaults to false (the model sets it); server daily USD limit |
| OKX Onchain OS · 9de8161 | None on swap execute | Honeypot classifier on quote output; security token-scan | Backend, before signing; aborts if the simulation reports failure | Auto, with slippagePercent 0.5 | Backend warning 81362 at broadcast, bypassed with --force |
| Jupiter CLI · d8d7386 | None | No check tool: spot tokens displays the isVerified label; a separate Jupiter skill tells the model to read it | None client-side; --dry-run available | Jupiter real-time estimate | Amount and slippage validation |
| Agent Wormhole wormhole-x402 0.9.9 · ec61fbd | None: a payment guard, not a token check | n/a | None; offline byte inspection | n/a | In the signing path; refused 3 of 3 swaps, 0 inner signs |
In all seven, nothing in the open code between the tool call and the signature looks at the token being bought. Several pass the transaction through a server step we cannot read: Phantom's /prepare, OKX's pre-transaction simulation, Jupiter's Ultra order and execute endpoints, the CDP or Privy signers AgentKit can use, and the Crossmint or Lit wallets GOAT can use. What those steps check is not in the code.
Four paths rely on the RPC's default preflight: Solana Agent Kit, GOAT, ElizaOS and AgentKit. Preflight simulates the already-signed transaction and rejects it only if it would fail on-chain. It says nothing about the token received. A purchase of a token that cannot later be sold does not fail. In none of these paths can the model change preflight.
Three of these buy paths do not reach a signature as published today. Solana Agent Kit's Jupiter path, GOAT's TypeScript Jupiter plugin and the SWAP_SOLANA action in ElizaOS plugin-solana 1.2.6 all call quote-api.jup.ag/v6. Solana Agent Kit and ElizaOS name that host in their own code (constants.ts:28 and tools/trade.ts:72; swap.ts:83 and 105). GOAT reaches it through the default base path of @jup-ag/api 6.0.31, the exact version it pins. On 2026-09-28 that host returned no A record, and curl could not resolve it. As published, those three paths fail at the quote fetch and sign nothing. The design finding applies to any fork or update that points them at a live host. The ElizaOS plugin has a second entry point, the service method that trading plugins call, which gets its quotes from an external JUPITER_SERVICE (service.ts:336-338). We did not examine that service, so we cannot say whether that path reaches a signature today. AgentKit resolves @jup-ag/api 6.0.40 from its lockfile and 6.0.48 on a fresh install. Created without an API key, as AgentKit creates it, 6.0.40 calls api.jup.ag/swap/v1 and 6.0.48 calls lite-api.jup.ag/swap/v1. Both hosts answered. GOAT's Python port calls api.jup.ag/swap/v1.
Solana Agent Kit has other actions that can buy. We checked one, OKX_EXECUTE_SWAP, which delegates to @okx-dex/okx-dex-sdk. We found no read of isHoneyPot in that SDK's JavaScript at 1.0.13 or 1.0.18. The field appears only in its type definitions. By npm downloads for 2026-09-21 to 2026-09-27, the most used packages here are Coinbase AgentKit (6,350, all chains), Phantom's MCP server (4,491) and Solana Agent Kit (1,257).
Where a check exists, the model decides
Solana Agent Kit registers a RUGCHECK action beside TRADE in the same plugin (plugin-token/src/index.ts:143). It fetches Rugcheck's summary report for a mint. Searching the whole repository, the only references are its own files, its registration and the docs. The TRADE action's schema requires the output mint to be a string of at least 32 characters (actions/trade.ts:57). The handler parses it as a public key, and the tool puts it into the quote URL (tools/trade.ts:47). SolanaAgentKit.use(), executeAction and all five framework adapters (Claude, LangChain, OpenAI, Vercel AI and MCP) pass the call through with no hook between them and the handler. The Claude, OpenAI and Vercel AI adapters go through executeAction, which parses the schema and calls the handler. The LangChain and MCP adapters call the handler directly (core/src/langchain/index.ts:20, adapter-mcp/src/index.ts:76).
GOAT ships Rugcheck as its own plugin with five read-only tools (plugins/rugcheck/src/rugcheck.service.ts:8-50). Outside that plugin and its Python port, the word appears only in the README, the Python project file and lockfiles. GOAT's core has no hook, approval or middleware mechanism: getTools only collects the tools. In the week to 2026-09-27 the rugcheck plugin had 11 npm downloads, against 78 for the Jupiter plugin.
OKX has the most developed check of the seven, and it shows the pattern most clearly. classify_swap_route marks a route block with the reason "to-token is a honeypot" when the backend's isHoneyPot flag is true:
// okx/onchainos-skills 9de8161 · cli/src/commands/risk_classify.rs:257-265
if token["isHoneyPot"].as_bool().unwrap_or(false) {
let (side_action, reason) = if is_buy {
(SwapAction::Block, "to-token is a honeypot")
} else {
(SwapAction::Warn, "from-token is a honeypot; exit allowed")
};
action = action.stricter(side_action);
reasons.push(reason.to_string());
}The classifier runs on the output of swap quote and swap swap, and of the MCP tools swap_quote and swap_swap (swap.rs:203, 312; mcp/mod.rs:1700, 1733). There it annotates and does not reject. The skill's reference says so: "exit code stays 0 — classification, not rejection" (swap.md:94). The command that signs is swap execute. Neither cmd_execute nor the batch path cmd_execute_batch calls the classifier or reads isHoneyPot. So a block verdict stops a purchase only if the model first ran a quote, read the field, and chose to stop. A separate security token-scan command detects honeypots, high tax and mint risks, and nothing on the execute path calls it either.
At broadcast, the backend can return risk warning 81362. Without --force, the CLI returns a result asking for confirmation. With --force, it sends skipWarning: true (transfer/mod.rs:594-596). The skill asks the model to get the user's confirmation before adding the flag. No code checks that it did.
The router builds the order either way
Jupiter is the router behind four of the other six paths, and its own CLI is the most direct way to put a model in front of it. jup spot swap resolves --to by taking the first hit of a token search (DatapiClient.ts:178-187). It then requests an Ultra order, signs it locally and posts it for execution (Swap.ts:49-116). Nothing on that path reads isVerified, the organic score or the isSus flag. The CLI prints isVerified in its spot tokens and spot portfolio output, not in quote or swap, and its own docs and llms.txt give no trust guidance. Jupiter's separate integrating-jupiter agent skill (jup-ag/agent-skills, SKILL.md:240) tells a model that the "primary trust signal is top-level isVerified". That is guidance, and it does not ship with the CLI.
We asked Ultra for an order on each of the 30 most recently listed tokens, and on each token we caught carrying Jupiter's isSus flag at the moment of the order:
300 of 3030 of 300 of 3030 of 3033 of 3This is not a defect in the router. A router is a market and is not asked to judge tokens. What it means is that a check the agent skips is not caught further down. Ultra returned the same kind of order for tokens Jupiter's own token API had flagged as for those it had not. Three flagged tokens is a small sample, and it shows only that a flag did not stop an order, not how often it would.
Two paths give the model nothing to consult
Coinbase AgentKit's Jupiter action converts the output mint with new PublicKey() and passes it to the quote (jupiterActionProvider.ts:48). Its description tells the model that input and output tokens "must be valid SPL token mints" (line 33). The code does not check the output mint. It reads only the input mint, with getMint, for its decimals (line 52), which fails unless the input is an SPL mint. AgentKit can sign through a local keypair, CDP or Privy. The latter two sign server-side under operator-configured policies that we did not evaluate.
ElizaOS plugin-solana 1.2.6 takes the token addresses from the conversation. The SWAP_SOLANA handler makes a separate model call over the recent messages to extract the input token, output token and amount (swap.ts:271-281). If that call returns a contract address, the handler uses it as it is. A symbol without an address is resolved only against tokens already in the wallet, so buying a new token requires its address to appear in the chat. That address is not checked.
Two controls the model cannot set
Phantom's /prepare is the one server step on a surveyed buy path that can refuse with a scanner verdict the model cannot override. The session builds its client as a user wallet (session/manager.ts:449, 495), so each Solana sign-and-send first posts to /prepare with a simulation config (PhantomClient.ts:320-346). That step can refuse with transaction-blocked, carrying a scanner result, or with spending-limit-exceeded, against a daily USD limit (client/src/types.ts:161-189). The model cannot skip it. The code does not show whether the scanner looks at the output token, so the table says unclear rather than none. OKX's backend also simulates every execute before signing, and no flag skips it (transfer/mod.rs:336). It aborts only when the simulation reports failure (369-380), and the risk warning 81362 that can follow at broadcast is overridden with --force.
The model does control the rest of Phantom's path. buy_token returns a quote unless execute is true:
// phantom/phantom-connect-sdk 618f616 · packages/cli/src/actions/buy-token.ts:291-299
if (!execute) {
logger.info("Returning quote only (execute: false)");
return { quoteRequest, quoteResponse };
}
logger.info(`Executing ${isCrossChain ? "cross-chain" : ""} swap transaction (execute: true)`);
const swapResult = await executeSwap({
quoteResponse,The model sets execute, so a single call with execute: true signs and broadcasts. buy_token never calls the separate simulate_transaction tool. The same codebase does enforce this pattern elsewhere: pay_api_access throws on a simulation block before signing (pay-api-access.ts:155-167). The buy path does not.
The second control is in code newer than the ElizaOS plugin surveyed above. On the elizaOS/eliza develop branch (d0cd039), Solana swaps live in plugins/plugin-wallet. Swaps the model requests through the plugin's wallet action pass a confirmation gate (chains/wallet-action.ts:486) that proceeds only after the user confirms in a later turn. The gate's own comment states that "the LLM cannot bypass this gate by setting request params" (security/wallet-financial-confirmation.ts:9-13), and the Solana helper isConfirmed returns false for any confirmed flag the model supplies (chains/solana/actions/confirmation.ts:17-20). The same action refuses financial writes on messages flagged as suspected prompt injection (chains/wallet-action.ts:409). The gate does not cover every route to a signature. SolanaService still exports the executeSwap entry point that trading plugins call (chains/solana/service.ts:2463), and nothing in it calls the gate. We did not read the plugin's Raydium swap service. For requests that come through the wallet action, this is a control in the path. It is not a token check: we found none in SolanaService.swap or buildJupiterSwapTransaction. The code ships as @elizaos/plugin-wallet (152 downloads in the week to 2026-09-27), and we did not confirm that the published versions match develop.
Of the refusals we could see in the surveyed toolkits' code, these two are the only ones that can stop a well-formed buy that would succeed on-chain, and that the model cannot override. Signing policies that operators configure for CDP, Privy, Crossmint or Lit may be others; we did not evaluate them. Neither of the two is a token check in code we can read. Phantom's step is not visible, and ElizaOS's asks a person.
Our own package is in the path, and cannot see the token
wormhole-x402 ships guardSigner, a wrapper around a Solana wallet's signing methods. It lets a signature through only when inspectPayment finds that the transaction matches an x402 payment quote, and it refuses when there is no quote. It inspects the bytes offline, with no RPC, and the wallet signs the bytes that were inspected, not the caller's object. That puts it in the position this article argues for. It was built for payments, so we ran it against real Jupiter swaps to see what it does outside that job.
abstain → refusedabstain → refusedrefuse · X402-003, X402-006, X402-0070 times// agent-wormhole ec61fbd · x402-guard/src/index.ts:1100-1106, 1118-1119
const quote = getQuote();
if (!quote) {
throw new Error(
`x402-guard: refusing to sign via ${method}() — no payment quote was ` +
"supplied, so there is nothing to check this transaction against.",
);
}
// …
const verdict = inspectPayment(serialized, quote, opts);
if (verdict.decision !== "allow") {The default routes abstain because a transaction that uses address lookup tables names accounts that are not in its own bytes, and the guard will not resolve them without RPC (index.ts:301-309). The legacy route is refused because it invokes Jupiter's program, which is outside the guard's program allowlist (X402-003). It also raised findings under two further rules, X402-006 and X402-007.
So the guard refuses a swap into USDC and a swap into anything else in the same way. It has no notion of the token and makes no judgement about it. An agent that needs to buy tokens cannot use it for that, and we do not suggest it.
Method
The code read needs no account. The live measurements used Jupiter's unauthenticated endpoints. For each toolkit we cloned the repository, recorded the commit with git rev-parse HEAD, and started at the tool the model calls to buy. We read every function from there to the signature. That included the adapters that expose the tool to a model framework and the alternative wallet clients a toolkit offers. We then searched each tree for token-check and control vocabulary and traced every caller of whatever turned up back toward the buy entry point. A check that no buy path calls is recorded as offered, not enforced.
# toolkits, cloned and pinned
sendaifun/solana-agent-kit 1254fe550872ee48d42f168fc8ca854c87ab6eff
packages/plugin-token/src/jupiter/actions/trade.ts TRADE handler
packages/plugin-token/src/jupiter/tools/trade.ts quote, swap, sign
packages/core/src/utils/keypairWallet.ts sign + send
packages/core/src/{claude,langchain,openai,vercel-ai}, packages/adapter-mcp
goat-sdk/goat f2eedc2f041589322c0db39d59a67757014910a0
typescript/packages/plugins/jupiter/src/jupiter.service.ts
typescript/packages/wallets/solana/src/SolanaKeypairWalletClient.ts
coinbase/agentkit 2e6dbaf725b9ec5f3b53003278100b0e655c214d
typescript/agentkit/src/action-providers/jupiter/jupiterActionProvider.ts
phantom/phantom-connect-sdk 618f6165112f0ecd4986773698f26fdcb7dbc7ca
packages/cli/src/actions/buy-token.ts
packages/cli/src/utils/swap.ts
packages/client/src/PhantomClient.ts /prepare
okx/onchainos-skills 9de8161d870fea8fad05c324f6c00a50cd88962d
cli/src/commands/swap.rs cmd_execute
cli/src/commands/risk_classify.rs classify_swap_route
cli/src/commands/agentic_wallet/transfer/mod.rs sign_and_broadcast
jup-ag/cli d8d7386691535469f7d17705ebfc5f432b96feba
src/lib/Swap.ts
jup-ag/agent-skills a2211e3f6a7caa03310c7a9a8d816e723b0bdc5f
skills/integrating-jupiter/SKILL.md
# ElizaOS: github.com/elizaos-plugins/plugin-solana returns 404, so read npm
npm pack @elizaos/plugin-solana@1.2.6
gitHead 646c632924826e2b75c2304a75ee56959fe4a460
sha256 dfd19648ecafc789bbd4c23fa5456b8927615aedc152c95993afc34dd03e7bc1
source dist/index.js.map -> sourcesContent -> src/actions/swap.ts, src/service.ts
elizaOS/eliza (develop) d0cd0399e71046278452c5db0c3d9997a0f558d6
plugins/plugin-wallet/src/chains/wallet-action.ts
plugins/plugin-wallet/src/security/wallet-financial-confirmation.ts
plugins/plugin-wallet/src/chains/solana/
# ours
runningoffcode/agent-wormhole ec61fbdf8e29679e4651f10a7ec6e4cdd4a7d610
x402-guard/src/index.ts guardSigner, inspectPayment (wormhole-x402@0.9.9)
# in each tree, then trace every caller back toward the buy entry point
grep -rniE 'rugcheck|honeypot|isverified|issus|confirm|approve|simulate|skippreflight|policy'We also read the JavaScript of @okx-dex/okx-dex-sdk 1.0.13 and 1.0.18 for Solana Agent Kit's OKX path. In @jup-ag/api we read the base path and createJupiterApiClient in 6.0.31, 6.0.39, 6.0.40, 6.0.44 and 6.0.48. We also read sendEncodedTransaction in @solana/web3.js 1.98.4 to confirm the preflight default.
Live measurements were all taken on 2026-09-28 between 16:58 and 17:06 UTC, from one machine, and all were read-only. Raw output was saved for each. DNS: quote-api.jup.ag returned no A record (NOERROR with no answer from 8.8.8.8 and 1.1.1.1; the local resolver returned NXDOMAIN, and its status has varied between runs), and curl exited with code 6. Default slippage: a SOL to USDC quote with no slippage parameter returned slippageBps 50 on both api.jup.ag/swap/v1 and lite-api.jup.ag/swap/v1.
Router: we took the 30 most recent tokens from lite-api.jup.ag/tokens/v2/recent and sent each to lite-api.jup.ag/ultra/v1/order, the endpoint the Jupiter CLI calls, with inputMint SOL, amount 10,000,000 lamports (0.01 SOL), and a taker address that holds at least that much SOL. Ultra returns no transaction for an unfunded taker: a freshly generated taker got "Insufficient funds" and an empty transaction, and no taker got null. For flagged tokens we read recent, toptrending/1h and toptraded/1h repeatedly, requested an order with the same parameters for each token that carried audit.isSus, and read the flag from token search within about a second of the order. The orders were built and never signed.
Our guard: we ran guardSigner from a fresh build of the commit above, which is byte-identical to the shipped dist/index.js. It wrapped a wallet that counts signing calls and was given real Jupiter swap transactions built on lite-api.jup.ag/swap/v1 for 1,000,000 lamports (0.001 SOL). The default routes were SOL to USDC and SOL to BONK, quoted with slippageBps 50. The legacy SOL to USDC with no lookup tables was quoted with onlyDirectRoutes=true&asLegacyTransaction=true and built with asLegacyTransaction: true in the /swap body. Setting asLegacyTransaction on the quote alone still returned a versioned transaction with one lookup table.
Limits
A code read is not a runtime test. We did not run any toolkit's buy path with funds. When we say what a path does, we mean what its code does at the stated commit. A configuration, wrapper or framework we did not read could add a check.
Server-side steps are not visible. Several steps run where we cannot read them: Phantom's /prepare scanner and its spending-limit value, OKX's pre-transaction simulation and warning 81362, Jupiter Ultra's order and execute endpoints, the CDP and Privy signing policies AgentKit can use, and the Crossmint and Lit wallets GOAT can use. Where a cell says unclear, that is what it means.
Toolkits change. Solana Agent Kit was last pushed on 2026-05-14 and GOAT on 2026-07-02. OKX was pushed on the day we read it. We read the ElizaOS plugin from its npm source map at 1.2.6, because its GitHub repository returns 404. That plugin already has a successor with different controls, which we read but did not fully trace. Its Raydium swap service is among what we did not read.
Some paths were read less fully than others. We checked Solana Agent Kit's OKX_EXECUTE_SWAP path only by searching the OKX SDK's JavaScript, because running it needs OKX API keys. The ElizaOS service path gets its quote and swap from an external JUPITER_SERVICE that we did not examine, so whether that path reaches a signature today is unknown. We did not trace Solana Agent Kit's Mayan, Drift, Sanctum or limit-order actions. We read only AgentKit's TypeScript package, and only GOAT Python's Jupiter service, which has the same shape as the TypeScript one. We read the web3.js preflight default in 1.98.4, while Solana Agent Kit's lockfile resolves 1.98.2.
The measurements come from one machine in one eight-minute window. DNS results, lookup-table counts and token flags vary with time and route. An earlier run the same day saw one lookup table on the SOL to USDC route, and the run reported here saw two. The flagged-token result rests on three tokens, and the flag is not stable: a fourth token was flagged in a list and unflagged in search fourteen seconds later, so we left it out, and a fifth, seen flagged in a list, was not ordered.
If we have misread a path, or missed a check that exists, send the file and line through agentwormhole.com/contact. We will correct this page and say that we did.
Why the position matters more than the check
The token checks in this survey are real and do useful work. Rugcheck summarises a mint, OKX's backend flags honeypots, and Jupiter labels verified and suspicious tokens. This survey is not about how good they are. It is about where they sit.
In every path we read, the open code judges the token only if the model decides it should. That works while the model is right. The purchases a check exists to stop are the ones where it is not: a user has argued it into a trade, a token description was written to persuade it, or a symbol names more than one token. In each case the model has been given a reason to believe this token is the right one, and a model that believes that has no reason to call a tool that might disagree.
Moving the check into the signing path means more than calling the same tool from a different place. The signer has to know which token the transaction buys. On the default routes we built, the accounts that answer that question sat behind two or three address lookup tables, which is why our own guard abstains. A token check in the signing path has to resolve those tables, or verify the transaction against a quote it trusts, and then judge the token. We have not built that, and this article does not announce that we have.
Any agent that can buy can be tested simply. Take the token check out of its tool list and ask it to buy. If the purchase still goes through, the check was advice.
Sources
- Solana Agent Kit at 1254fe5
- GOAT SDK at f2eedc2
- @elizaos/plugin-solana 1.2.6 on npm
- elizaOS/eliza develop at d0cd039: plugin-wallet
- Coinbase AgentKit at 2e6dbaf
- Phantom Connect SDK (MCP server, CLI, client) at 618f616
- Phantom MCP server tools documentation
- OKX Onchain OS skills at 9de8161
- Jupiter CLI at d8d7386
- Jupiter agent-skills at a2211e3
- Agent Wormhole x402-guard at ec61fbd
- wormhole-x402 on npm
Code read at the commits stated, on 2026-09-28. Live measurements were read-only and taken from one machine between 16:58 and 17:06 UTC on 2026-09-28; nothing was signed and nothing was broadcast. npm download counts are for 2026-09-21 to 2026-09-27. Negative findings mean “not found with substantial effort”, never “proven absent”. Corrections: agentwormhole.com/contact.