| Audited | sendaifun/solana-agent-kit at commit 1254fe550872ee48d42f168fc8ca854c87ab6eff |
|---|---|
| Date | 11 October 2026 |
| How it ran | cloud session, full audit, Standard review |
| Verdict after review | Fail (rule: Fail if a High finding remains after review, otherwise Pass with notes) |
Each finding keeps the number it has in the audit report. The rating shown first is the one after review; the first automated rating is listed with it. 113 findings were first rated Medium or High; 28 of them are Medium or High after review. Text marked "From the report" is quoted from the audit report; fixes are suggestions and were not tested. Code locations link to the file and line at the audited commit.
High after review (3)
packages/plugin-defi/src/lavarage/tools/lavarage.ts:40. Also:From the report
plugin-defi/src/drift/tools/drift.ts:222and 288, anddrift_vault.ts:639- the Flash tools
plugin-misc/src/gibwork/tools/create_gibwork_task.ts:62plugin-misc/src/crossmint/tools/checkout.ts:104plugin-blinks/src/sendarcade/tools/rock_paper_scissor.ts:99, which also reports the prize as claimed without confirmationplugin-nft/src/metaplex/tools/deploy_token2022.ts:80
if (agent.wallet.signAndSendTransaction) {
const { signature } = await agent.wallet.signAndSendTransaction(tx);
A host that configured sign-only, for example to have a human approve each transaction, still has funds moved without approval.
Route every send through one function that honours sign-only. Fix the cross-version instanceof check, for example with 'version' in tx, instead of working around it.
Review: lavarage.ts:39-49 calls agent.wallet.signAndSendTransaction(tx) and falls back to signTransaction + sendRawTransaction, never looking at agent.config.signOnly. Same in drift.ts:221-225 and :286-291, drift_vault.ts:639 (txSender.sendVersionedTransaction), create_gibwork_task.ts:61-62, crossmint/checkout.ts:102-106, rock_paper_scissor.ts:99-102, the Flash tools (perpClient.sendTransaction, flash_open_trade.ts:155) and deploy_token2022.ts:80 (umi sendAndConfirm). Only about a dozen tools honour the flag, so a host that set signOnly as its human-approval gate still has funds moved. This is the one defect that defeats an explicit safety control, which is why it stays HIGH. Upstream: 2 searches, no match found.
Upstream: No exact upstream report; related items: #599 (PR, open).
From the report
phantom-agent-starter/src/app/utils/provider.ts:9crossmint-sak-v2/src/lib/ai/providers.ts:9privy-agent-tanstack-starter/src/lib/ai/providers.ts:9privy-sak-react-native/src/lib/ai/providers.ts:20
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
Any visitor, or anyone who unpacks the APK, can copy the key and spend on the account.
Call the model from an authenticated server route, and rotate the keys.
Review: All five cited files build the provider from a client-visible variable: process.env.NEXT_PUBLIC_OPENAI_API_KEY (turnkey and phantom, utils/provider.ts:9), import.meta.env.VITE_OPENAI_API_KEY (crossmint-sak-v2 and privy-agent-tanstack, providers.ts:9) and @env OPENAI_API_KEY (React Native, providers.ts:7,20, compiled into the APK). Anyone who opens the deployed page or unpacks the app gets the deployer's key. Examples are not shipped to npm, but the template itself causes the leak, so it keeps HIGH. Upstream: 2 searches, no match found.
Upstream: No matching upstream report found (11 October 2026).
From the report
const API_PRIVATE_KEY = process.env.NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY as string
Anyone can create wallets and sign for the organisation.
Keep the key server-side, and rotate it.
Review: turnkey-agent-starter/src/app/chat/page.tsx:35 reads NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY and uses it at :56-61 to build a Turnkey API client in a client component; .env.local.example:7 tells users to put it in that variable. The key lets the holder create wallets and sign for the organisation. Upstream: 2 searches, no match found.
Upstream: No matching upstream report found (11 October 2026).
Medium after review (25)
examples/embedded-wallets/crossmint-sak-v2/src/functions/session.ts:17. Same flow in examples/embedded-wallets/privy-agent-tanstack-starter/src/functions/session.ts:17.From the report
const user = await getUser(data.email, data.walletAddress);
...
await session.update({ id: user[0].id,
loginFn and signupFn open a session for whichever user matches the posted email or wallet address. They check no signature and no token. Wallet addresses are public, so anyone can log in as any user and read or act on their chats. An unknown user also crashes the login: getUser returns an array, an empty array is truthy, and user[0].walletAddress then throws.
Verify a Crossmint or Privy auth token, or a signed challenge, on the server before you create the session. Test user.length === 0 for the not-found case.
Review: session.ts:17-42 opens a session for whoever getUser matches, with no proof of ownership (confirmed in both starters). The !user test on an array is also wrong (an empty array is truthy, so user[0].walletAddress throws). Impact is chat history only: the wallet is held by Crossmint/Privy and signs client-side. Placeholder session secret: see 117.
Upstream: No matching upstream report found (11 October 2026).
examples/embedded-wallets/privy-sak-react-native/server/middleware/auth.ts:17. See also server/routes/userRoutes.ts:11.From the report
const walletAddress = req.headers['x-wallet-address'] as string || req.query.walletAddress as string;
Wallet addresses are public on chain, so anyone can read, overwrite or delete any user's chats. A query such as ?walletAddress[$ne]=x reaches findOne as an operator object, which bypasses the login altogether. POST /user changes any user's profile without any check.
Verify a Privy access token or a signed nonce, then issue a session. Reject values that are not strings. Put /user behind the same middleware.
Review: POST /user (userRoutes.ts:11) has no middleware. Real, but chat data in a demo backend, not funds.
Upstream: No matching upstream report found (11 October 2026).
From the report
const txBuffer = Buffer.from(transactionData.substring(2), "hex");
const transaction = VersionedTransaction.deserialize(txBuffer);
The bridge-execution tool takes a hex transaction from the model's tool arguments and signs it with the agent wallet. A prompt injection can make the model pass a transaction that drains the wallet.
Execute only orders this tool created itself (keep them server-side and look them up by id). Decode the instructions and check the programs, the fee payer, the amounts and the destinations before signing.
Review: execute_bridge_order.ts:15-31 deserialises a hex string from the tool argument, swaps the blockhash and calls signOrSendTX (so sign-only is honoured). It is the second half of a create-then-execute flow, and a prompt-injected model can already call the plain transfer tool, so this adds arbitrary program calls rather than a new class of loss. A fix (server-side order lookup, program allow-list) would be good hardening.
Upstream: No matching upstream report found (11 October 2026).
From the report
let evmWallet: Wallet | null;
In a process that serves several agents or tenants, later agents sign EVM swaps with the first agent's private key and spend from the wrong wallet.
Build the wallet from the current agent's configuration on each call, or cache it per agent.
Review: mayan/tools/swap.ts:147-160: let evmWallet is created from the first agent's ETHEREUM_PRIVATE_KEY and reused for every later agent in the process. Real cross-tenant bug, but it needs several agents with different raw EVM keys in one process.
Upstream: No matching upstream report found (11 October 2026).
examples/embedded-wallets/para-plugin-example/utils/init_server.ts:20. The unauthenticated app/api/wallet/init/route.ts:15 installs the posted key share.From the report
solanaAgentWithPara.wallet = {
Concurrent users sign and send with each other's wallets. Anyone can post a key share or drive the chat route.
Create one agent per authenticated session or request, and require authentication on both routes.
Review: init_server.ts:6,20 mutates the module-level solanaAgentWithPara.wallet per request; wallet/init/route.ts and chat/route.ts have no authentication. Concurrent users would sign with each other's wallets, but this is a single-user demo.
Upstream: No matching upstream report found (11 October 2026).
From the report
const memory = new MemorySaver();
...
thread_id: "Solana Agent Kit!",
- Conversations from different visitors merge and can leak to each other.
- History is duplicated on every request.
- The unauthenticated route lets any visitor direct the server wallet (line 46).
Key the thread by an authenticated session, send only the new message, require authentication, and limit spending.
Review: chat/route.ts:30,58: one module-level MemorySaver and a fixed thread_id: "Solana Agent Kit!" for every visitor, and the client also sends the full history each time. The unauthenticated-server-wallet half is the same root cause as 122.
Upstream: No matching upstream report found (11 October 2026).
From the report
accountLoader: new BulkAccountLoader(agent.connection, "processed", 10),
Each early throw leaves a poller hitting the RPC for the life of the process. Repeated failures stack pollers until the provider rate-limits the agent. One deposit opens four or more clients.
Call cleanUp() in finally blocks, use one client per operation, and poll about every second.
Review: drift.ts:71-74 builds BulkAccountLoader(connection, "processed", 10); cleanUp() is called only on success paths (e.g. depositToDriftUserAccount:199 throws "create a Drift user account first" before it). The same applies to the vault tools, each of which opens its own client. A failed call leaves a 100 req/s poller running for the life of the process. Upstream: 2 searches, no match found.
Upstream: No matching upstream report found (11 October 2026).
packages/core/src/types/wallet.ts:141From the report
txSigs.push(signature);
}
throw new Error(
The throw runs on every loop iteration. Every array call sends transaction 1 and then reports failure, so the rest are never sent. Callers may retry and pay twice. Affected callers include cancelling limit orders and the Raydium launch.
Put the throw in an else branch, or check support once before the loop. Add a two-transaction test.
Review: wallet.ts:134-144: the throw follows the if unconditionally, so for any array the first transaction is signed and sent, then the call throws. jupiter/cancel_limit_orders.ts:22 and the Raydium launch use this path. Real and in core, but the damage is a false error plus unsent follow-up transactions, not a loss of funds. Upstream: 1 search, no match found.
Upstream: No matching upstream report found (11 October 2026).
packages/plugin-defi/src/lavarage/tools/lavarage.ts:6: a Lavarage API key whose value starts lv2_.From the report
const API_KEY = "lv2_…";
Everyone who installs the package gets the key and can use up its quota.
Revoke and rotate the key. Read it from the agent configuration and fail fast when it is missing.
Review: lavarage.ts:6 holds lv2_prod_... in plugin source that ships in dist. Rotate it. Upstream: 2 searches, no match found.
Upstream: No matching upstream report found (11 October 2026).
From the report
let REFERRAL_WALLET = new PublicKey(
"FPfG…",
That address is passed as the token creator (line 88), so creator-fee ownership goes to a fixed outside wallet unless the user overrides it.
Default the creator to the agent wallet, and make any referral an explicit, documented opt-in.
Review: launchPumpfunToken.ts:62-67,83-90 passes FPfGD3kA8Z... (a fixed wallet) as the creator unless PUMP_FUN_REFERRAL_WALLET is set; the variable is declared in types/index.ts:41 but documented nowhere in the repo. Creator-fee ownership goes to a wallet the user did not choose. (The launch itself is broken, see 66.)
Upstream: No matching upstream report found (11 October 2026).
packages/plugin-token/src/mayan/tools/swap.ts:109. Also:From the report
plugin-defi/src/flash/tools/utils/flashUtils.ts:275plugin-defi/src/adrena/tools/utils/anchor/AdrenaClient.ts:45plugin-defi/src/okx/tools/execute_swap.ts:33
agent.wallet.signTransaction,
The default keypair wallet reads this.payer, so a detached call throws. Mayan, Flash and Adrena trades cannot be signed.
Pass wrappers such as (tx) => agent.wallet.signTransaction(tx).
Review: mayan/tools/swap.ts:101,109 passes agent.wallet.signTransaction/signAllTransactions as bare functions; flashUtils.ts:275-276 and AdrenaClient.ts:45-46 put them in an object literal. KeypairWallet methods read this.payer (keypairWallet.ts:52-55), so this is undefined or the literal and signing throws. Mayan, Flash and Adrena fail with the default wallet and need a wallet whose methods are closures. The OKX item (execute_swap.ts:31-36) is not affected: the ...agent.wallet spread copies the own payer property. Fail-closed, so MEDIUM. Upstream: 2 searches, no match found.
Upstream: No matching upstream report found (11 October 2026).
packages/plugin-defi/src/manifest/tools/manifest_trade.ts:85. Also:From the report
- lines 113, 138 and 274
plugin-defi/src/drift/tools/drift.ts:215and 280plugin-token/src/solana/tools/close_empty_token_accounts.ts:60
const txn = new Transaction().add(...depositPlaceOrderIx);
txn.recentBlockhash = blockhash;
The keypair wallet's partialSign cannot compile a message without a fee payer. Every Manifest tool, Drift deposit and Drift withdraw fails.
Set feePayer = agent.wallet.publicKey, or build a v0 message with an explicit payer.
Review: manifest_trade.ts:39-43,85-87,113-115,138-140, drift.ts:215-220,280-285 and close_empty_token_accounts.ts:26,57-60 never set it, so with KeypairWallet about eight tools always fail. Systemic but fail-closed. Upstream: 2 searches, no match found.
Upstream: No matching upstream report found (11 October 2026).
packages/core/src/openai/utils.ts:172 and 127.From the report
const required = Object.keys(properties);
...
throw new Error(`Unsupported Zod type: ${typeName}`);
The model has to invent values, for example a mint for a plain SOL transfer. One union schema in the misc plugin breaks creation of the whole tool list.
Emit optional fields as nullable. Handle unions, records and any. Skip, and log, actions that cannot be converted.
Review: openai/utils.ts:123,172 lists all keys as required and unwraps ZodOptional without making the property nullable (isNullable is only set for ZodNullable, :57); :127 throws on any unsupported type, so one union schema breaks creation of the whole tool list.
Upstream: No matching upstream report found (11 October 2026).
packages/adapter-mcp/src/index.ts:43From the report
result[key] = isZodOptional(value) ? value.unwrap() : value;
Keep the optional schema, and parse inputs through the action schema.
Review: adapter-mcp/src/index.ts:43 unwraps ZodOptional before passing the shape to server.tool, so clients must supply every optional field (for example tokenAddress on the balance action).
Upstream: Already reported upstream: #600 (issue, open), #601 (PR, open).
From the report
const txs = await Promise.all(
transactions.map(async (tx) => {
A failure partway leaves accounts half-created, with their rent spent.
Send the transactions in order, confirm each one, and report how far it got.
Review: openbook_create_market.ts:49-55 runs Promise.all over the Raydium marketV2.create transactions, each fetching its own blockhash. The SDK's account-creation and initialise transactions depend on each other; the code part is confirmed, the exact SDK ordering was not read. A partial failure leaves rent spent.
Upstream: No matching upstream report found (11 October 2026).
From the report
const adjustedPrice = price.mul(new BN(100));
Tokens priced below one cent are reported as 0.00.
Format using the full exponent.
Review: pyth_fetch_price.ts:77-85: scaling by 100 and slicing the last two digits. Worse than reported: for a price of 0.05 the scaled string is "5", "5".slice(0,-2) is empty and the result is "0.5", ten times too high (verified with a one-line node -e on the same expression). Prices from 0.01 to 0.099 are wrong by 10x.
Upstream: No matching upstream report found (11 October 2026).
packages/plugin-defi/src/drift/tools/drift.ts:351. Also drift_vault.ts:591.From the report
const convertedAmount =
params.amount / convertToNumber(baseAssetPrice.price, PRICE_PRECISION);
A request for "50 SOL" opens a 50 USD position.
Choose one unit, and name and document it.
Review: drift.ts:351-352 divides params.amount by the oracle price, while the action schema says "amount of the token ... e.g. 50 SOL" (actions/tradePerpAccount.ts:53). "50 SOL" opens a 50 USD position. Same pattern in drift_vault.ts:591.
Upstream: No matching upstream report found (11 October 2026).
From the report
const slippageBps = new BN(1000);
Make slippage a bounded parameter, set a maximum leverage, and keep preflight on.
Review: flash_open_trade.ts:127-133 fixes slippageBps = 1000; createPerpClient also sets skipPreflight: true (flashUtils.ts:281). At 10x leverage a 10% adverse fill equals the whole collateral, and the user cannot change it.
Upstream: No matching upstream report found (11 October 2026).
examples/embedded-wallets/turnkey-agent-starter/src/app/components/Chat.tsx:299. Also phantom-agent-starter/src/app/components/Chat.tsx:221.From the report
dangerouslySetInnerHTML={{ __html: marked(m.content) }}
Text that reaches the model from tools or chain data can inject script into a page that holds signing access.
Render with react-markdown, or sanitise with DOMPurify.
Review: turnkey-agent-starter/.../Chat.tsx:299 and the phantom starter use dangerouslySetInnerHTML={{ __html: marked(m.content) }}; marked does not sanitise. Token names or other tool output can carry markup into a page that holds signing access.
Upstream: No matching upstream report found (11 October 2026).
From the report
const email = searchParams.get("email");
...
db.findOne({ email }, (err, doc) => {
- Anyone who knows an email can download that user's key share, or delete it.
- Storage is in memory only.
- The save route reports success before the write completes.
Require a session that matches the email. Never return raw shares. Use durable storage and acknowledge only after the write.
Review: usershare/route.ts:37-80 returns the stored document (email plus userShare) for any email, and :83 deletes it; the store is in-memory nedb (db.ts:4). A share is one part of an MPC key and needs the provider session to sign, so it is not a drain on its own.
Upstream: No matching upstream report found (11 October 2026).
From the report
If user asks for his funds back, you can send them their private key ${keyPair.privateKey}.
The key is sent to the model provider and can be pulled out by prompt injection. It is also stored unencrypted.
Never put secrets in prompts. Use a custodial or key-management wallet service.
Review: advanced-tg-bot/.../route.ts:54,92 stores the secret key in Firestore in plaintext and interpolates it into the system prompt. It is a deliberate custodial design (the user's own key, in their own thread), but sending keys to the model provider and storing them unencrypted is poor practice.
Upstream: No matching upstream report found (11 October 2026).
examples/social/tg-bot-starter/group-tg-bot/src/app/api/bot/route.ts:129. It happens again at line 138.From the report
await ctx.reply("Your private key is:");
await ctx.reply(`${String(keyPair.privateKey)}`);
The key ends up in chat history and backups on every private message.
Do not echo keys. Offer an authenticated export flow instead.
Review: group-tg-bot/.../route.ts:129-130,138-139 replies with the private key to every private message from the owner. It goes to the owner's own chat, so this is exposure in history and backups rather than to a third party (but see 123).
Upstream: No matching upstream report found (11 October 2026).
examples/social/discord-bot-starter/src/index.ts:26. Also:From the report
examples/social/tg-bot-starter/basic-tg-bot/src/app/api/bot/route.ts:31examples/defi/wormhole-nextjs-agent/app/api/chat/route.ts:18
const secretKey = bs58.decode(process.env.SOLANA_PRIVATE_KEY as string);
Anyone who can message the bot or call the route can transfer or swap the wallet's funds.
Add allow-lists or authentication, spending caps and confirmation steps.
Review: discord-bot-starter/src/index.ts:26 and basic-tg-bot/.../route.ts:31 load one SOLANA_PRIVATE_KEY; the Discord bot answers any DM and shares one memory thread. The wormhole citation is moot: that route uses the removed v1 API and cannot start (see 129). This is the nature of the demo, with only generic README advice (README.md:912).
Upstream: No matching upstream report found (11 October 2026).
From the report
const handler = webhookCallback(bot, "std/http");
A forged update can claim any user id and drive that user's custodial wallet.
Register the webhook with a secret token and verify it.
Review: route.ts:183 uses webhookCallback(bot, "std/http") with no secretToken, so a forged update can claim any user id. Exploiting it needs the (unpublished) webhook URL and the victim's numeric id, but combined with 120/121 it would leak keys.
Upstream: No matching upstream report found (11 October 2026).
From the report
const shouldRetry = isTransactionRelated(messageContent) && retryCount < MAX_RETRIES;
A transfer or swap can run twice.
Retry only idempotent calls, and deduplicate by signature.
Review: useChat.ts:330-352,449-463 wraps generateText({ tools, maxSteps: 5 }) in executeWithRetry, retried up to twice for any error when the message contains words like "send" or "swap". A tool call that already landed a transaction is run again.
Upstream: No matching upstream report found (11 October 2026).
Low after review (110)
packages/plugin-token/src/jupiter/tools/trade.ts:97. Same pattern in:From the report
plugin-misc/src/gibwork/tools/create_gibwork_task.ts:59plugin-blinks/src/sendarcade/tools/rock_paper_scissor.ts:26plugin-defi/src/lulo/tools/lend.ts:25plugin-defi/src/solayer/tools/stake_with_solayer.ts:33plugin-misc/src/crossmint/tools/checkout.ts:102
const transaction = VersionedTransaction.deserialize(swapTransactionBuf);
return await signOrSendTX(agent, transaction);
A compromised, spoofed or buggy API response receives a full wallet signature. Several of these paths also send with skipPreflight: true.
Before signing, check:
- the fee payer is the agent wallet
- the program ids are on an allow-list
- the token outflows match the requested mint, amount and recipient
Review: the "skipPreflight: true" remark does not apply to the Jupiter path (KeypairWallet.signAndSendTransaction passes no options); it does apply to checkout.ts:105 and the Lavarage fallback.
From the report
const { tipLamports } = await lavaApi("/bundle/tip");
... body: { ...body, astralaneTipLamports: tipLamports },
An abnormal value is paid in full. Transactions go out with skipPreflight: true (line 48), so failed trades still land on chain and pay fees.
Cap the tip in configuration, inspect the returned transaction, and keep preflight on.
packages/core/src/utils/send_tx.ts:172. The loop starts at line 160.From the report
if (statuses.value[0]) {
if (!statuses.value[0].err) {
return signature;
- A "processed" status can still be rolled back, but the caller is told the transfer or swap happened.
- The same signed transaction is re-sent for up to 90 s without tracking the block height until which its blockhash is valid.
- A transaction that lands between polls can come back as "already processed" or "Blockhash not found", and that gets reported as a failure. The caller may then retry and pay twice.
Require at least "confirmed" status. Use confirmTransaction with lastValidBlockHeight, and treat "already processed" as a status check, not an error.
packages/plugin-misc/src/gibwork/tools/create_gibwork_task.ts:71. Also:From the report
plugin-token/src/solana/tools/request_faucet_funds.ts:20plugin-nft/src/metaplex/tools/deploy_token2022.ts:80, which confirms only at "processed"
await agent.connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
A transaction that reverts is reported as success.
Use the blockhash of the signed transaction. Throw when value.err is set, and confirm at "confirmed" or higher.
From the report
throw new Error(`Transaction ${swapRes.serializedTrx} reverted!`);
...
if (res.status !== 200) { throw error;
The revert error is thrown inside the same try block, so the catch returns the signature as success whenever the vendor explorer answers 200.
Handle result.value.err outside the try and always fail on it. Fall back to the explorer only for RPC timeouts.
packages/plugin-token/src/solana/tools/transfer.ts:67. Also:From the report
plugin-defi/src/drift/tools/drift.ts:143, plus lines 208, 274, 601 and 683plugin-defi/src/meteora/tools/create_meteora_dlmm_pool.ts:34
const mintInfo = await getMint(agent.connection, mint);
...
createTransferInstruction(
Token-2022 mints fail or derive the wrong token account. An unchecked transfer also lets a wrong-decimals amount through.
- Read the mint's owner program and pass it to the mint lookup, the ATA derivation and the instruction builders.
- Use
createTransferCheckedInstruction. - Create the ATA with the idempotent instruction.
packages/plugin-defi/src/raydium/tools/raydium_create_cpmm.ts:38. Also raydium_create_clmm.ts:37 and meteora/actions/createMeteoraDynamicAMMPool.ts:87.From the report
programId: mintInfoA.owner.toString(),
Liquidity can be deposited into a pool for a mint with a permanent delegate, a transfer hook or a transfer fee, and none of these are checked.
Require the owner to be one of the two token programs. Decode the mint, and reject or warn on risky extensions and on active mint or freeze authorities.
packages/plugin-token/src/solana/tools/get_token_balances.ts:30. Also:From the report
plugin-defi/src/sanctum/tools/sanctum_get_owned_lst.ts:12plugin-misc/src/helius/tools/send_transaction_with_priority.ts:99plugin-nft/src/tensor/tools/tensor_trade.ts:22
programId: TOKEN_PROGRAM_ID,
Token-2022 holdings are left out of portfolios and lookups.
Query both token programs and merge the results.
From the report
const ix = await pumpSdk.collectCoinCreatorFeeInstructions(REFERRAL_WALLET);
...
payerKey: REFERRAL_WALLET,
The agent cannot provide that wallet's signature, so the claim fails. It also targets a third party's fees instead of the agent's.
Use agent.wallet.publicKey as both the payer and the creator.
packages/plugin-defi/src/ranger/tools/ranger_perp_trading.ts:28. Also plugin-defi/src/okx/actions/executeSwap.ts:48.From the report
fee_payer: agent.wallet.publicKey.toBase58(),
...
...rest,
...rest can override the fee payer, and the OKX schema lets the model choose the user wallet. The Ranger module is not registered at the moment, but it will become reachable once someone registers it.
Set these fields from the agent wallet, after the spread. Remove them from the schemas.
packages/plugin-token/src/jupiter/tools/trade.ts:95. Also:From the report
jupiter/tools/stake_with_jup.ts:28mayan/tools/swap.ts:99dexscreener/tools/get_token_data.ts:19plugin-defi/src/drift/tools/drift.ts:754plugin-blinks/src/sendarcade/tools/rock_paper_scissor.ts:68plugin-misc/src/messari/tools/ask_messari_ai.ts:31
const { swapTransaction } = await (
...
const swapTransactionBuf = Buffer.from(swapTransaction, "base64");
An outage or an error body ends as an obscure TypeError, or is returned to the user as data with status "success".
Check response.ok, validate the fields you need, and pass the vendor's error message on.
packages/plugin-token/src/pyth/tools/pyth_fetch_price.ts:65. Also plugin-defi/src/flash/tools/utils/flashUtils.ts:126 (isStale: false).From the report
const data = await response.json();
const parsedData = data.parsed;
Stale or wide-confidence prices size positions and collateral.
Reject prices older than a threshold or with a large confidence interval, and check the HTTP status.
packages/plugin-defi/src/sanctum/tools/sanctum_swap_lst.ts:46. Also sanctum_add_liquidity.ts and sanctum_remove_liquidity.ts.From the report
programId: messages.staticAccountKeys[ix.programIdIndex],
keys: ix.accountKeyIndexes.map((i) => ({
pubkey: messages.staticAccountKeys[i],
Indexes that point into lookup tables resolve to undefined, so the swap crashes or is built wrongly.
Use the vendor message as delivered and only refresh the blockhash. Otherwise, decompile it with the lookup-table accounts.
packages/core/src/utils/send_tx.ts:176. Also plugin-defi/src/orca/tools/orca_create_single_sided_liquidity_pool.ts:420.From the report
`Transaction failed: ${statuses.value[0].err.toString()}`,
Users see [object Object] or {} instead of the reason the transaction failed.
Serialise the error object, or map known errors to text. Include the signature.
Review: send_tx.ts:176 is a real [object Object]; the Orca citation uses JSON.stringify(error), which gives {} for Error objects.
packages/plugin-token/src/mayan/tools/swap.ts:131 puts the whole serialised transaction into the error. Also:From the report
plugin-defi/src/drift/tools/drift.ts:615says "Failed to get APYs" in the staking function.examples/misc/agent-kit-nextjs-langchain/app/api/chat/route.ts:79returns raw exception text.examples/embedded-wallets/para-plugin-example/app/api/wallet/init/route.ts:19returns raw exception text.plugin-nft/src/tensor/tools/tensor_trade.ts:35swallows the ownership error.
throw new Error(`Transaction ${swapRes.serializedTrx} reverted!`);
Give a short, accurate message with the signature, and keep the technical detail in the logs.
packages/plugin-token/src/solana/tools/get_balance.ts:23. Also plugin-defi/src/drift/tools/drift_vault.ts:268.From the report
return token_account.value.uiAmount || 0;
get_balancepasses the mint address togetTokenAccountBalance, which expects a token account, and a null result becomes 0.- In the vault tool, an explicit fee or hurdle of 0 is treated as "not provided".
- Derive the owner's token account from the mint.
- Use the raw amount and the decimals.
- Treat null as an error.
- Test
!== undefined, not truthiness.
Review: with a mint address getTokenAccountBalance throws; the "returns 0" claim is only true for a null uiAmount.
|| and ?? defaults hide missing datapackages/plugin-token/src/jupiter/tools/get_token_by_ticker.ts:23. Also plugin-defi/src/raydium/tools/raydium_create_launchlab_token.ts:47.From the report
.toSorted((a, b) => (b.daily_volume ?? 0) - (a.daily_volume ?? 0))
Handle a missing field explicitly. Use ?? so that a value of 0 survives, and do not mutate the caller's parameters.
From the report
`https://worker.jup.ag/blinks/swap/So111.../jupSoLaH.../${amount}`
Move to a documented public API, or put this endpoint behind a configuration flag with a clear error when it stops working.
examples/embedded-wallets/phantom-agent-starter/src/app/components/Chat.tsx:99. Also SolanaTransactionExample.tsx:18.From the report
const transactionHash = await phantom.solana.signAndSendTransaction(tx);
return { signature: transactionHash };
The provider returns an object, so the "signature" is an object. Rendering the signMessage result as a string crashes the component.
Read .signature and encode it before you store or display it.
From the report
const phantomInstance = await createPhantom({
Check window.isSecureContext and show a clear message when the page is served over plain HTTP.
examples/embedded-wallets/privy-sak-react-native/src/utils/transactions/transactionUtils.ts:364From the report
if (wallet.provider === 'mwa' || Platform.OS === 'android') {
- On Android, a user of the embedded wallet would be routed to a different wallet.
- The cluster is hard-coded to devnet, and the authorization result, including its token, is logged.
- The file imports a module that does not exist, so today it is dead code.
Route only on wallet.provider === 'mwa', take the chain from configuration, and stop logging auth tokens.
From the report
const prefersDark = window.matchMedia('(prefers-color-scheme: light)').matches;
First-time visitors get the opposite of their system theme.
Query (prefers-color-scheme: dark).
examples/embedded-wallets/crossmint-sak-v2/src/utils/ThemeContext.tsx:13. Also examples/embedded-wallets/privy-sak-react-native/android/app/src/main/res/values/colors.xml:2.From the report
const [theme, setTheme] = useState<Theme>("light");
Dark-mode users see a light flash on every load. The Android splash is white, with no night override, before the app's dark UI appears.
Set the theme with an inline head script before hydration. Make the splash colour match the app background.
examples/embedded-wallets/privy-sak-react-native/android/app/src/main/res/values/styles.xml:7. Also examples/defi/wormhole-nextjs-agent/components/Header.tsx:8, where a white logo sits on a white light-mode header.From the report
<item name="android:statusBarColor">#ffffff</item>
Use colour resources with night variants, and switch the logo per theme.
sizes is invalidFrom the report
sizes="100%"
The browser falls back to 100vw and downloads a full-width image for a small avatar.
Pass the drawn width, for example sizes="48px".
From the report
if (!authenticated && e.target.value.trim() !== "") {
checkAuthAndShowModal();
Ask for sign-in on send, which the submit handler already does.
packages/plugin-token/src/solutiofi/tools/solutiofi.ts:9. Line 20 has the same problem.From the report
let solutiofiClient: SolutioFi | null = null;
Later agents reuse the first agent's API key. Parallel calls can use the client before authenticate() has finished.
Cache one in-flight promise per agent or per API key, and clear it on failure.
packages/plugin-misc/src/crossmint/tools/confirm-order.ts:42. The function returns at line 79.From the report
const pollInterval = setInterval(async () => {
...
return { success: false, error: "Payment confirmation timeout",
Values returned from timer callbacks go nowhere. A paid order is reported as failed, which invites a re-order, and the timers fire after the function has returned.
Write an awaited loop that polls, sleeps and checks a deadline, returns the real status, and leaves no dangling timers.
packages/core/src/utils/send_tx.ts:171From the report
const statuses = await agent.connection.getSignatureStatuses([signature]);
This is about 180 RPC calls per transaction.
Use confirmTransaction with the blockhash and block height, and re-send at a slower interval.
packages/core/src/agent/index.ts:81From the report
plugin.initialize(this as SolanaAgentKit);
...
throw new Error(`Method ${methodName} already exists in methods`);
A name collision throws after some methods are already bound. The plugin is left half-registered, and a retry fails.
Check all names first, then update the methods, actions and plugin map together.
From the report
JSON.stringify(await action.handler(solanaAgentKit, inputs)),
Run every adapter through executeAction, so that validation and error shape are the same everywhere.
packages/core/src/utils/keypairWallet.ts:77. Also examples/social/tg-bot-starter/advanced-tg-bot/src/app/api/bot/route.ts:83, which runs checkpointer.setup() on every message.From the report
const connection = new Connection(this.rpcUrl);
Create these once.
From the report
await new Promise((resolve) => setTimeout(resolve, 2000));
Wait for the init transaction to confirm, fetch the account again, and rethrow unexpected errors.
Review: the sleep is at drift_vault.ts:77, not :69. 52: the import is at wallet.ts:13-17, not :104. All other LOW locations match.
examples/defi/market-making-agent/index.ts:96. Same in examples/misc/persistent-agent/index.ts:95 and examples/misc/orbofi-personality-engine/index.ts:98. In examples/social/tg-bot-starter/basic-tg-bot/src/app/api/bot/route.ts:45, a new memory is created for every message.From the report
if (walletDataStr) {
fs.writeFileSync(WALLET_DATA_FILE, walletDataStr);
Remove the no-op read and write, and create the checkpointer once.
examples/social/tg-bot-starter/advanced-tg-bot/src/app/api/bot/route.ts:148. Same pattern in group-tg-bot at lines 164–195.From the report
await updateDoc(userDocRef, { inProgress: true });
Two messages can both pass the check. A function killed at its time limit leaves the user locked out permanently.
Use a transactional compare-and-set with a lease timestamp that expires, and reset the flag in one place.
examples/embedded-wallets/privy-sak-react-native/server/controllers/messageController.ts:127From the report
const existingMessage = await Message.findOne({ id });
Ownership is checked only against the chat id in the request, so a caller can supply a message id that belongs to another user's chat.
Look the message up by both { id, chatId }.
From the report
const messageId = `${ctx.chatId}-${ctx.message.message_id}`;
...
id: messageId + 1,
"<chat>-45" + 1 becomes "<chat>-451", which later collides with a real message id. The batch insert then fails and the chat history silently stops growing.
Use UUIDs, or prefixes such as u- and a-.
From the report
walletAddress: text("walletAddress").notNull().default(""),
Concurrent sign-ups create duplicate users, and every user without a wallet matches ''.
Add unique constraints and a check that an email or a wallet address is present. Use an upsert.
From the report
const agent = new SolanaAgentKit(keypairWallet, keypairWallet.rpcUrl, {});
No plugins are loaded, so the server starts without error but exposes no tools. The start promise is also left unhandled.
Load the plugins, build the configuration from the environment, and exit with an error when the tool list is empty.
From the report
"x-api-key": process.env.FLEXLEND_API_KEY!,
A key supplied through the configuration is ignored. If the variable is unset, the request fails with an unrelated TypeError.
Read the key from agent.config, fail fast when it is missing, and check response.ok.
packages/plugin-defi/src/okx/tools/get_quote.ts:24, and the same in five other OKX tools.From the report
apiKey: agent.config.OKX_API_KEY ?? "",
Add one credential helper that throws a clear error.
packages/plugin-defi/src/drift/tools/drift.ts:1020 (1,057 lines). Also packages/plugin-defi/src/index.ts:258 (527 lines of hand-kept lists).From the report
export async function getLendingAndBorrowAPY(
Split the file into client, user account, insurance fund, swap and market data modules. Have each protocol export its own bundle of methods and actions.
packages/core/src/claude/index.ts:54. Also:From the report
- the openai, langchain and vercel-ai adapters
packages/core/src/types/action.ts:25versustypes/index.ts:115- the no-op
initializeloop copied into every plugin (packages/plugin-token/src/index.ts:156)
for (const action of actions.slice(0, 127)) {
Add one helper for limiting actions and building tool descriptions, and keep one Action type definition.
packages/core/src/types/wallet.ts:104From the report
import { type feeTiers, getComputeBudgetInstructions, sendTx } from "../utils/send_tx";
Move signOrSendTX into utils/ and keep types/ for declarations only.
.github/workflows/build.yml:28From the report
- name: Run lint and fix
run: pnpm run lint:fix
Run pnpm run lint instead. Add a test job, including a smoke test that loads every plugin and builds each tool list. Align the pnpm version with the one in package.json.
test/programmaticTests/index.ts:17. The test/tools/okx_*.test.ts files and test/tools/wormhole.ts import ../../src, which does not exist.From the report
const tokenData = await agent.methods.getAsset(
...
// Add your DeFi plugin test here
Write real assertions against a mocked RPC, port or delete the old-API files, and exit non-zero on failure.
packages/plugin-defi/package.json:27. No *.test.ts or *.spec.ts exists under packages/ (searched).From the report
"test": "jest"
Add a test runner with a configuration and smoke tests, or remove the scripts.
packages/plugin-defi/src/ranger/tools/ranger_perp_trading.ts:35. Also lines 80, 181, 226 and 275.From the report
adjustment_type: "Increase", // TODO: Confirm if this should be "Increase" or another type for open
Check each value against the provider's API before you enable this module.
packages/core/src/constants/index.ts:6. Also in the jupiter, adrena, fluxbeam and sns constants. The core copy is not exported.From the report
export const TOKENS = {
SEND: new PublicKey("SENDdRQt..."),
Keep one exported table in core, import it everywhere, and fix the RERERRAL_FEE typo once.
packages/plugin-misc/src/helius/tools/send_transaction_with_priority.ts:51. Also line 115 and packages/core/src/utils/send_tx.ts:70.From the report
const response = await fetch(
`https://mainnet.helius-rpc.com/?api-key=${agent.config?.HELIUS_API_KEY}`,
Extract one helper in core.
packages/plugin-token/src/solana/tools/get_balance_other.ts:33. Also packages/plugin-token/src/jupiter/tools/get_open_limit_orders.ts:9.From the report
console.warn(
`No token account found for wallet ${wallet_address.toString()} ...
Log once, at the boundary that handles the error, through an injectable logger.
.github/workflows/build.yml:26From the report
run: pnpm install --no-frozen-lockfile
Use --frozen-lockfile, and publish from CI with provenance.
scripts/check-langchain-tool-duplicates.ts:4. Also:From the report
- the root
generatescript points at a missing file .lintstagedrctargets ESLint, but the project uses Biome and the.huskyhook directory is gitignored
import { SolanaAgentKit } from "../src";
Port the duplicate-tool check to the current API and run it in CI, or delete these files.
packages/plugin-misc/src/allora/tools/get_all_topics.ts:18: an Allora key whose value starts UP-d. The same literal is in two sibling files.From the report
agent.config?.ALLORA_API_KEY || "UP-d…";
Remove the literal, require the key from configuration, and rotate it.
packages/plugin-token/src/pumpfun/tools/launchPumpfunToken.ts:140. The keypair is created at line 60.From the report
const mint = Keypair.generate();
...
const txHash = await signOrSendTX(agent, tx);
The create instruction needs the mint's signature, so no token can ever be launched.
Call tx.sign([mint]) before the wallet signs, in both the send and sign-only branches.
Review: (Pump.fun launch, Manifest market creation, close-empty-accounts, compressed airdrop, NFT mint, SNS register) are all real and each tool fails closed: no mint signature (launchPumpfunToken.ts:130-140); no market keypair signature (manifest_trade.ts:45); token_2022[i] is undefined when i >= token_2022.length (close_empty_token_accounts.ts:34-36); the action calls sendCompressedAirdrop(mintAddress, ...) without agent and with strings (compressedAirdrop.ts:79), and the tool serialises a transaction with no blockhash (send_compressed_airdrop.ts:125); mint_nft.ts:73,84 sends twice; register_domain.ts:28-31 passes (owner, mint) to getAssociatedTokenAddressSync(mint, owner). Rated LOW only because nothing is lost; they would be MEDIUM in a product-quality ranking.
packages/plugin-token/src/pumpfun/tools/launchPumpfunToken.ts:107. The action passes initialLiquiditySOL at actions/launchPumpfunToken.ts:86.From the report
new BN(amount),
A value such as 0.1 is truncated to 0 or throws. The slippage and priority-fee inputs are dropped.
Convert SOL to lamports, derive the token amount explicitly, and pass slippage and fee through.
From the report
return [await signOrSendTX(agent, tx), marketKeypair.publicKey.toBase58()];
Creating the account requires the new account's signature, so market creation always fails.
Call tx.partialSign(marketKeypair), or pass the keypair as an extra signer.
Review: (Pump.fun launch, Manifest market creation, close-empty-accounts, compressed airdrop, NFT mint, SNS register) are all real and each tool fails closed: no mint signature (launchPumpfunToken.ts:130-140); no market keypair signature (manifest_trade.ts:45); token_2022[i] is undefined when i >= token_2022.length (close_empty_token_accounts.ts:34-36); the action calls sendCompressedAirdrop(mintAddress, ...) without agent and with strings (compressedAirdrop.ts:79), and the tool serialises a transaction with no blockhash (send_compressed_airdrop.ts:125); mint_nft.ts:73,84 sends twice; register_domain.ts:28-31 passes (owner, mint) to getAssociatedTokenAddressSync(mint, owner). Rated LOW only because nothing is lost; they would be MEDIUM in a product-quality ranking.
From the report
for (let i = 0; i < Math.max(0, MAX_INSTRUCTIONS - spl_token.length); i++) {
transaction.add(token_2022[i]);
undefined is added and throws, including when there is nothing to close.
Bound the loop by token_2022.length, move the empty check above the loops, and report the number actually closed.
Review: (Pump.fun launch, Manifest market creation, close-empty-accounts, compressed airdrop, NFT mint, SNS register) are all real and each tool fails closed: no mint signature (launchPumpfunToken.ts:130-140); no market keypair signature (manifest_trade.ts:45); token_2022[i] is undefined when i >= token_2022.length (close_empty_token_accounts.ts:34-36); the action calls sendCompressedAirdrop(mintAddress, ...) without agent and with strings (compressedAirdrop.ts:79), and the tool serialises a transaction with no blockhash (send_compressed_airdrop.ts:125); mint_nft.ts:73,84 sends twice; register_domain.ts:28-31 passes (owner, mint) to getAssociatedTokenAddressSync(mint, owner). Rated LOW only because nothing is lost; they would be MEDIUM in a product-quality ranking.
From the report
const txs = await sendCompressedAirdrop(
mintAddress,
The agent argument is missing and strings are passed where PublicKeys are expected, so the action always fails. The tool also serialises a transaction that has no blockhash or fee payer (send_compressed_airdrop.ts:125).
Pass the agent and PublicKey values, and build complete transactions.
Review: (Pump.fun launch, Manifest market creation, close-empty-accounts, compressed airdrop, NFT mint, SNS register) are all real and each tool fails closed: no mint signature (launchPumpfunToken.ts:130-140); no market keypair signature (manifest_trade.ts:45); token_2022[i] is undefined when i >= token_2022.length (close_empty_token_accounts.ts:34-36); the action calls sendCompressedAirdrop(mintAddress, ...) without agent and with strings (compressedAirdrop.ts:79), and the tool serialises a transaction with no blockhash (send_compressed_airdrop.ts:125); mint_nft.ts:73,84 sends twice; register_domain.ts:28-31 passes (owner, mint) to getAssociatedTokenAddressSync(mint, owner). Rated LOW only because nothing is lost; they would be MEDIUM in a product-quality ranking.
From the report
const sigOrTx = await signOrSendTX(agent, tx);
...
await signOrSendTX(agent, tx);
A successful mint ends in a duplicate-send error, and the agent may mint again.
Delete the second call.
Review: (Pump.fun launch, Manifest market creation, close-empty-accounts, compressed airdrop, NFT mint, SNS register) are all real and each tool fails closed: no mint signature (launchPumpfunToken.ts:130-140); no market keypair signature (manifest_trade.ts:45); token_2022[i] is undefined when i >= token_2022.length (close_empty_token_accounts.ts:34-36); the action calls sendCompressedAirdrop(mintAddress, ...) without agent and with strings (compressedAirdrop.ts:79), and the tool serialises a transaction with no blockhash (send_compressed_airdrop.ts:125); mint_nft.ts:73,84 sends twice; register_domain.ts:28-31 passes (owner, mint) to getAssociatedTokenAddressSync(mint, owner). Rated LOW only because nothing is lost; they would be MEDIUM in a product-quality ranking.
From the report
const buyerTokenAccount = getAssociatedTokenAddressSync(
agent.wallet.publicKey,
TOKENS.USDC,
The wallet is passed as the mint and USDC as the owner, so registration always fails.
Use getAssociatedTokenAddressSync(TOKENS.USDC, agent.wallet.publicKey).
Review: (Pump.fun launch, Manifest market creation, close-empty-accounts, compressed airdrop, NFT mint, SNS register) are all real and each tool fails closed: no mint signature (launchPumpfunToken.ts:130-140); no market keypair signature (manifest_trade.ts:45); token_2022[i] is undefined when i >= token_2022.length (close_empty_token_accounts.ts:34-36); the action calls sendCompressedAirdrop(mintAddress, ...) without agent and with strings (compressedAirdrop.ts:79), and the tool serialises a transaction with no blockhash (send_compressed_airdrop.ts:125); mint_nft.ts:73,84 sends twice; register_domain.ts:28-31 passes (owner, mint) to getAssociatedTokenAddressSync(mint, owner). Rated LOW only because nothing is lost; they would be MEDIUM in a product-quality ranking.
packages/core/src/utils/send_tx.ts:109From the report
fees.sort((a, b) => a.prioritizationFee - b.prioritizationFee)[
Math.floor(fees.length * feeTiers[feeTier])
].prioritizationFee,
On localnet, devnet or a quiet RPC, every send fails.
Fall back to a default fee and clamp the index.
From the report
const signedTransaction = await agent.wallet.signTransaction(transaction);
...
return sendTx(
Browser and hardware wallets prompt the user several times. A valid signed transaction is handed to the fee-estimate provider.
Sign once. Estimate fees from an unsigned serialisation.
packages/core/src/utils/send_tx.ts:102From the report
priorityFee = data.result.priorityFeeEstimate;
An abnormal estimate is paid in full. On devnet with a Helius key configured, every send is blocked.
Validate and cap the estimate, check response.ok, and pick the host from the configured cluster.
packages/plugin-token/src/solana/tools/transfer.ts:35 and 68. Also:From the report
plugin-nft/src/metaplex/tools/deploy_token.ts:64plugin-defi/src/fluxbeam/tools/create_pool.ts:30plugin-misc/src/helius/tools/send_transaction_with_priority.ts:45plugin-defi/src/pumpfunAmm/tools/removeLiquidity.ts:31, which also assumes 6 decimals
lamports: amount * LAMPORTS_PER_SOL,
1.1 * 1e9 is not an integer, so web3.js rejects ordinary amounts. Large values lose precision.
Convert decimal strings to bigint using the mint's decimals.
Review: the report's example is wrong. 1.1 * 1e9 evaluates to exactly 1100000000. About 4.5% of two-decimal SOL amounts (for example 1.07 or 2.01) are not integers after the multiplication, and web3.js rejects them, so it fails closed.
packages/core/src/vercel-ai/index.ts:19From the report
tools[index.toString()] = tool({
The model sees tools named "0", "1" and so on, and the names change whenever plugins change. Descriptions are cut at 1,023 characters.
Use tools[action.name].
packages/core/src/langchain/index.ts:16. Same in the openai, vercel-ai and claude adapters.From the report
const tools = actions.slice(0, 127).map((action) => {
Use one constant for the check, the slice and the message.
packages/plugin-misc/src/coingecko/tools/get_trending_tokens.ts:7. Also get_token_price_data.ts:10.From the report
`.../search/trending${agent.config?.COINGECKO_DEMO_API_KEY && `?x_cg_demo_api_key=...`}`
Use a ternary that falls back to "", and send the key in a header.
From the report
console.error("Error creating Crossmint Amazon order:", error);
The logged error includes the request headers, and with them the production key.
Log only the message, the status and the response body.
packages/plugin-misc/src/helius/tools/send_transaction_with_priority.ts:126. Line 146 also creates the ATA with the non-idempotent instruction.From the report
transaction: bs58.encode(transaction.serialize()),
Every SPL transfer through this action fails. When the recipient already has a token account, it fails anyway.
Add the instructions first, serialise with signature checks disabled for the estimate, and use the idempotent ATA instruction.
From the report
const openResult = await test("OPEN_POSITION (2x long cbBTC/USDC, $2)", () =>
If the position lookup is slow, the position stays open with real funds and the errors are swallowed.
Delete the script, or gate it behind an explicit opt-in on devnet and close the position in a finally block.
Review: packages/plugin-defi/package.json lists "files": ["dist"], so test-lavarage.mjs is not published.
packages/plugin-defi/package.json:63From the report
"solana-agent-kit": "2.0.7"
Core is at 2.0.10, so installs hit peer conflicts. A second copy of core can be installed, which breaks shared class checks.
Make core a peer dependency with a range only.
require inside an ES-module packagepackages/core/src/utils/owsWallet.ts:26From the report
const { exportWallet } = require("@open-wallet-standard/core") as {
The package is "type": "module", and require is undefined in its ES-module build.
Use a static import, createRequire, or a dynamic import().
From the report
.sort((a: any, b: any) => (b.fdv || 0) - (a.fdv || 0));
A copycat token with an inflated supply wins the ranking, and value-moving actions then use its mint.
Resolve tickers against a verified list, or require the mint address.
From the report
} catch (_error) {
continue;
Partial or failed irreversible operations are reported as success.
Collect a result per transaction and throw on failure.
From the report
inputAmount: z.number().positive("Input amount must be positive"),
Calls that follow the documented examples are rejected.
Remove the field, or make it optional.
From the report
_slippageBps: number = DEFAULT_OPTIONS.SLIPPAGE_BPS,
Users cannot limit swap slippage.
Send slippageBps, or cap dynamic slippage with it.
From the report
`https://voltr.xyz/api/remaining-accounts/deposit-strategy?vault=...`
Withdrawals revert, or run the wrong strategy instruction.
Call the withdraw endpoint.
From the report
await driftClient.connection.getAccountInfo(
deriveInsuranceFundStakeAccount,
getAccountInfo returns null rather than throwing, so first-time stakes fail.
Set shouldCreateAccount = info === null.
From the report
numberToSafeBN(amount, QUOTE_PRECISION),
For a 9-decimal vault, the amount is off by a factor of 1,000.
Use the precision of the vault's asset.
packages/plugin-defi/src/adrena/tools/adrena_perp_trading.ts:445 (open short). Also line 66 (close short).From the report
const position = AdrenaClient.findPositionAddress(owner, principalCustody, "long",
Short trades target the wrong account.
Use "short".
From the report
.describe("Leverage in basis points (2000 = 2x)")
On chain, 10,000 means 1x, so 2000 is 0.2x, and the confirmation text shows the wrong multiplier.
Accept a multiplier and convert it once.
From the report
affiliateFeeRecipient: params.affiliateFeeRecipient,
A prompt injection can divert a share of bridged funds.
Take these values from trusted configuration only, with a cap.
From the report
clientOrderId: Number(Math.random() * 1000),
A u64 id field receives a non-integer from a small range, so ids collide.
Use an integer that increases monotonically or is generated with a cryptographic RNG.
From the report
slippageBps: z.number().optional().default(50)
Add .int().min(1).max(500) or a configured ceiling.
From the report
if (agent.connection.rpcEndpoint.includes("mainnet")) {
Valid custom endpoints are rejected as an unsupported network.
Use the genesis hash or explicit configuration.
From the report
]).start(() => {
// Loop the animation
animateDots();
stopAnimation completes the sequence, which starts the loop again, so it costs battery and CPU indefinitely.
Restart only when finished is true.
From the report
Clipboard.setString(walletAddress);
The core Clipboard has been removed, so copying the address fails.
Use expo-clipboard, which is already a dependency.
From the report
...transactions?api-key=${HELIUS_API_KEY}`;
Proxy the calls through the app's server, and rotate the key.
From the report
signingConfig signingConfigs.debug
The keystore and its password are public, so anyone can sign a replacement update for this wallet app.
Add a release signing configuration fed from CI secrets.
Review: are the stock Expo/React Native prebuild defaults (debug keystore for release with a "Caution! generate your own keystore" comment; overlay permission in the main manifest).
examples/embedded-wallets/crossmint-sak-v2/src/functions/chats.ts:92. Also:From the report
privy-agent-tanstack-starter/src/functions/chats.ts:93examples/misc/privy-server-wallet-agent/app/(chat)/actions.ts:37and 46, which also changes chat visibilitycrossmint-sak-v2/src/functions/chats.ts:133, where saving a message checks only that some session exists
export const deleteTrailingMessages = createServerFn({ method: "GET" })
Anyone can delete other users' history or make their chats public. Using GET also makes the delete triggerable by cross-site requests.
Require a session, check that the chat belongs to the caller, and use POST or DELETE.
examples/embedded-wallets/crossmint-sak-v2/src/utils/session.ts:6. Also privy-agent-tanstack-starter/src/utils/session.ts:6. The value starts Chan.From the report
password: "Chan…",
Anyone with the repository can forge session cookies.
Load a random secret from the environment and refuse to start without it.
Review: the "hard-coded secret" is the string ChangeThisBeforeShippingToProdOrYouWillBeFired, an explicit placeholder (same in privy-agent-tanstack-starter).
From the report
router.post('/upload', upload.single('file'), uploadFile);
Require authentication, add per-user quotas, and serve uploads from a separate origin.
examples/embedded-wallets/privy-sak-react-native/server/controllers/messageController.ts:106From the report
text: 'Message content unavailable'
Tool, image and reasoning parts are lost on save.
Persist the real part shapes, and reject invalid input.
Review: the placeholder text is only inserted into empty text fields (...part is kept), so non-text parts are not dropped.
examples/social/tg-bot-starter/advanced-tg-bot/src/app/api/bot/route.ts:149. Same in group-tg-bot and basic-tg-bot.From the report
for await (const chunk of (await Promise.race([
stream,
The stream is already resolved when the race starts, so a hung model call runs until the platform kills the function.
Apply the deadline to the whole iteration.
From the report
if (codeData?.usedBy != null) {
One invite code can be redeemed several times. A second concurrent setDoc replaces a keypair whose address was already shown to the user, so funds sent to it are lost.
Use Firestore transactions, or create-if-absent writes.
From the report
userChatHistory.push(new HumanMessage(agentMessage));
Store replies as AI messages, cap the history, and persist it.
From the report
db.delete(usersTable).where(eq(usersTable.id, userId))
- There is no confirmation and no balance check, so funds become unreachable.
- The messages that the help text promises to delete remain.
- History is read in no defined order and with no limit.
Ask for confirmation, check the balance, delete the messages, and read history ordered and limited.
examples/defi/market-making-agent/index.ts:110. Same in persistent-agent and orbofi-personality-engine.From the report
while (true) {
try {
const thought = "Be creative and do something interesting on the blockchain. " +
Add spending caps, allow-lists and a dry-run default, and trim the conversation history.
examples/defi/wormhole-nextjs-agent/app/api/chat/route.ts:18. Also line 78.From the report
const solanaAgent = new SolanaAgentKit(
process.env.SOLANA_PRIVATE_KEY!,
The route fails at start. Once that is fixed, special characters in user input throw or backtrack badly.
Migrate to the current API, and escape the input or use startsWith.
Review: the wormhole example imports solana-agent-kit/dist/langchain and calls new SolanaAgentKit(privateKey, rpc, openAiKey), the removed v1 API; the whole example is stale.
From the report
const keypairWallet = new KeypairWallet(keypair, process.env.RPC_URL as string);
The template defines only SOLANA_RPC_URL, so the wallet has no RPC.
Use one variable and fail fast if it is missing.
Review: discord-bot-starter/.env.template defines only SOLANA_RPC_URL, while KeypairWallet is built from RPC_URL (index.ts:28), so new Connection(undefined) fails on send.
From the report
if (authority?.isMutable !== undefined) {
defaultAuthority.isMutable = authority?.isMutable;
Tokens are created mutable, with the wallet as update authority, whatever the user asked for.
Pass the options into the create call, or reject them.
packages/plugin-nft/src/3land/actions/create3LandCollectibleAction.ts:89. Also line 125.From the report
const result = await createCollection({}, collectionOpts);
Provide the agent's signer, and return the minted asset's address.
From the report
fromPubkey: agent.wallet.publicKey,
Use the vault PDA as the sender, and pass the vault index through.
examples/embedded-wallets/privy-sak-react-native/src/walletProviders/hooks/useWallet.ts:234From the report
return Buffer.from(signature);
Decode with the encoding the provider documents, and check that the result is 64 bytes.
examples/embedded-wallets/privy-sak-react-native/android/app/src/main/AndroidManifest.xml:5From the report
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Drawing over other apps makes overlay attacks on the signing prompts possible.
Keep that permission in the debug manifest only, and drop the permissions the app does not use.
Review: are the stock Expo/React Native prebuild defaults (debug keystore for release with a "Caution! generate your own keystore" comment; overlay permission in the main manifest).
From the report
signTransaction: async (tx) => {
return tx;
Throw "not supported", or implement real signing.
signAllTransactions builds malformed transactionsFrom the report
signatures: [signedTransaction],
Deserialise and return the signed transaction. Make the stub sendTransaction throw.
From the report
console.log("keyPair", keyPair);
Log only the public key.
examples/defi/okx-dex-starter/index.ts:74. Also line 117.From the report
Exchange Rate: ${formatNumber(result.priceImpactPercentage)}%
Compute the rate from both token amounts. Give the model only the quote tool, and run the swap only after an explicit yes.
Excluded on review (2)
Findings the review showed to be wrong or a repeat of another finding. They are not counted above.
- 5. Vendor-supplied account metas, including signer flags, go into signed instructions (duplicate): Repeats another finding.
- 110. Back-handler cleanup uses a removed API (wrong):
privy-sak-react-native/package.json:63pins react-native 0.76.9, whereBackHandler.removeEventListenerstill exists (deprecated, removed later).