ottopumpOpen app →
Technical Whitepaper · Draft

A non-custodial command copilot for Solana.

Type a command in plain English. Review a preview built from the real transaction. Approve, sign with your own wallet, and get a receipt. The server never holds a key and never signs.

Abstract

OttoPump is a non-custodial Solana command copilot. A user connects a wallet, types a plain-English command, reviews a structured preview built from the actual transaction, approves, signs with their own wallet, and receives a receipt. The server never sees a private key and never signs.

The product is a trust copilot, not an autonomous trading agent and not a token-launch farm. Its defensible surface is the path between a natural-language command and a signed transaction: a deterministic intent compiler, a policy engine, and a preview that is generated from real account and token diffs rather than from model prose. The model proposes; app code decides; the wallet signs.

The problem

Most Solana power tooling forces a bad trade. Custodial bots and “agent wallets” are fast but ask the user to hand over keys or fund a hot wallet the app controls. Raw wallets and explorers are safe but slow and unforgiving. A mistyped address or an unverified mint is irreversible.

The recent wave of LLM “do-anything” agents makes this worse, not better. A thin wrapper sends a prompt to a model, lets the model pick a tool, executes from an agent wallet, and then shows the user whatever the model claims happened. That design is neither inspectable nor non-custodial: the user is trusting model text as proof of an on-chain effect.

Anti-pattern
Prompt → model → tool call → spend from agent wallet → display the model’s summary. The user never sees the real transaction and the keys are not theirs. OttoPump is explicitly the opposite of this.

Thesis

Do Solana actions safely from one command box. The moat is trust, UX, and distribution; the execution layer is composed from existing Solana tooling. OttoPump wins by making the outcome of an action predictable before signing, not by having exclusive access to liquidity or routes.

OttoPump is good when:

  • The user can predict exactly what will happen before they sign.
  • The app can explain why an action is blocked, in concrete terms.
  • Any provider can be swapped without changing the UX contract.
  • The non-custodial guarantee holds even when the LLM is wrong.
  • The product refuses profitable-but-untrustworthy flows.

The non-custody rule

This is the core invariant. Every production write must satisfy all of the following, in order. If any step cannot be met, the action is not executable.

  1. The server never stores or sees user private keys.
  2. The server may build or request an unsigned transaction.
  3. The client presents a human-readable preview.
  4. The connected wallet signs.
  5. The app sends the signed transaction plus a preview token to /api/execute.
  6. The server verifies the exact signed message and submits it.
  7. The app records a receipt.
Invariant
OttoPump is non-custodial by construction. There is no server-side user-signing path, no plaintext key storage, no key import, and no production write from an agent wallet. The server only verifies and broadcasts bytes the user’s own wallet already signed.

The trust spine

Every write action follows one path. The preview is the product: it is the contract the user approves, and the same message the server later verifies byte-for-byte.

User command
  → POST /api/intent      natural language → typed intent
  → POST /api/preview     validate · build unsigned tx · simulate · sign token
  → wallet.signTransaction(...)        connected wallet signs
  → POST /api/execute     verify exact message · submit to RPC
  → GET  /api/receipts/status          poll confirmation
  → receipt
Primary flow. Read-only actions stop at preview with transaction: null.

The two halves are separated on purpose. Intent parsing can be wrong or ambiguous without risk, because it cannot produce transaction bytes. Only the preview stage builds a transaction, and only after deterministic policy checks, a fresh blockhash, a fee estimate, and a successful simulation.

Architecture

OttoPump is a Next.js App Router application. The route handlers are the API surface; the core product modules are kept provider-independent so any RPC, market, or risk source can be swapped behind an adapter without changing the UX contract.

Stack

  • Next.js App Router + TypeScript.
  • Tailwind v4 (all resets in @layer base, tokens via @theme).
  • Solana Wallet Adapter (Wallet Standard / MWA auto-detection).
  • Helius-compatible RPC for reads, simulation, and submission.
  • An LLM provider for intent parsing (advisory only).

Provider-independent core

No provider is allowed to define the product contract. Each is wrapped behind an internal adapter that returns typed results: UnsignedTransaction, PreviewDiff, ProviderQuote, ProviderRisk.

  • Intent compiler: natural language → typed action.
  • Policy engine: deterministic allow/deny before any build.
  • Preview/diff engine: structured account & token diffs.
  • Transaction adapter registry: app-owned unsigned tx construction.
  • Submission adapter registry: standard RPC now; Sender/Jito later.
  • Wallet signing client: deserialize, check fee payer, sign, serialize.
  • Receipt normalizer: uniform receipts across action types.

Route surface

RoutePurpose
POST /api/intentCommand → typed intent. Never returns transaction bytes. Ambiguity returns a clarification, not an action.
POST /api/previewValidate the action, enforce policy, build the unsigned tx, simulate, and return a signed preview token. Read-only previews return transaction: null.
POST /api/executeVerify the preview token and the exact signed message, then broadcast. Never signs.
GET /api/receipts/statusNormalize pending / confirmed / finalized / failed signature states.
GET /api/wallet/summarySOL balance, token counts, closeable accounts, top holdings, degraded-DAS state.
GET /api/tokens/safetyParse the raw SPL mint: mint/freeze authority, supply, decimals, risk warnings.
GET /api/healthService status without secrets.

Intent compiler

Natural language compiles into a typed ActionIntent before any tool is called. The LLM can propose an intent, but app code validates it. Raw model text never reaches execution code.

  • Parse a command into a typed action with explicit fields.
  • Detect missing fields and ask for them rather than guessing.
  • Classify a risk level for the action.
  • Return a clarification (not a transaction) for anything ambiguous.
  • Refuse blocked behavior at the intent layer, before preview.

/api/intent returns a typed envelope:

{ intentId, status, confidence, intent, action,
  riskLevel, warnings, missingFields, clarification }

When status is clarification, both intent and action are null, so the path cannot proceed to a build.

Why this is owned, not glued
A wrapper would let the model both decide and execute. Here the model only proposes a structured object. Determinism, refusals, and risk classification live in app code, so the safety properties do not depend on the model behaving.

Preview engine

The preview is the trust product. It is generated from structured data (source and destination accounts, token mints and amounts, fee estimates, account creations and closures, slippage and price impact, and risk flags), never from LLM prose describing what a transaction “should” do.

SOL transfer preview

For a send, the server builds the exact transaction the user will sign and binds it to a stateless token:

format     VersionedTransaction (v0), no address lookup tables
instruction SystemProgram.transfer
fee payer  connected wallet public key
signer     connected wallet public key (exactly one)
fee        getFeeForMessage  (not a static constant)
simulate   simulateTransaction { sigVerify: false, commitment: "confirmed" }
expiry     recentBlockhash + lastValidBlockHeight
token      HMAC over previewId · messageHash · feePayer · blockhash · LVBH
The preview is the message; the preview token is its signature.

A successful simulation sets executable: true and approval.canExecute: true. A failed simulation keeps the preview non-executable and surfaces warnings. The user is never offered a signature for a transaction that would fail. Preview-token payloads are schema-validated after HMAC verification.

Policy runs first, and fails closed

  • Request bodies must contain no private keys, seed phrases, mnemonics, or keypairs.
  • SEND.source must match walletPublicKey when a wallet is provided.
  • SOL sends must stay under user/default limits.
  • Source and destination cannot be the same wallet.
  • Close-account previews include only zero-balance accounts owned by the connected wallet.
  • Swap previews fail closed when JUPITER_API_KEY is absent or Jupiter returns no taker-owned transaction.

Execute boundary

/api/execute is the narrowest gate in the system. It broadcasts only when every one of these is true:

  • The request carries no private-key material.
  • The preview-token signature is valid and its payload passes schema validation.
  • The previewId matches the token.
  • The signed transaction's message hash matches the preview exactly.
  • Fee payer and transfer source match walletPublicKey.
  • The transaction is v0 with no address lookup tables.
  • There is exactly one required signer and one SystemProgram.transfer instruction.
  • The wallet signature is present and nonzero.
  • The preview blockhash is still valid.

The server never signs. It verifies the bytes the wallet already signed and submits them through standard RPC sendTransaction:

{ skipPreflight: false,
  preflightCommitment: "confirmed",
  maxRetries: 0 }

Modified, expired, unsigned, or wrong-wallet transactions are rejected even if they are otherwise valid Solana transactions.

Why not client-side sendTransaction?

Letting the client broadcast directly would bypass server-side exact-message verification, the core guarantee that what the user approved is what lands on chain. So client-side sendTransaction is out of scope for OttoPump write paths, even though it is a common shortcut elsewhere. Server-side user signing is likewise out of scope.

Additional write paths

  • Close-account: exact preview-token message hash plus SPL Token close-account instructions whose owner and destination are the connected wallet.
  • Swap: exact server-built Jupiter message hash plus a connected-wallet fee-payer signature. Arbitrary client-built swap transactions are rejected by preview-token mismatch.

Action model

Execution operates on typed action objects, never on raw model text. Each action type has its own preview builder and its own execute-side verification shape.

ActionStateNotes
READ_ONLYliveBalances, holdings, status. Preview returns transaction: null.
SENDliveUnsigned v0 SystemProgram.transfer, simulated before signing.
SPLITliveMultiple transfers in one transaction where feasible.
SWAPliveJupiter Ultra unsigned tx; requires a token-safety result or explicit override.
CLOSE_TOKEN_ACCOUNTSliveReclaim rent from zero-balance accounts the wallet owns.
TOKEN_CHECKliveRaw mint authority/supply/decimals + risk warnings.
CLAIMdeferredOnly when an exact unsigned user-signed flow is verified.
TOKEN_LAUNCH_DRAFTbasic liveBasic Pump.fun SOL create/dev-buy uses unsigned tx build, client mint co-sign, wallet sign, and exact-message execute.

What OttoPump owns

OttoPump must not become a thin Claude wrapper over MCP tools. The defensible surface is the set of modules that sit between the model and the chain, and that keep the safety properties true regardless of the model.

  1. Intent compiler. Typed intents, missing-field detection, risk classification, refusal of blocked behavior.
  2. Policy engine. Deterministic rules: connection, allowed action, spend limit, risk threshold, destination checks, high-risk confirmation.
  3. Preview engine. Trust generated from structured diffs, not model prose.
  4. Transaction adapter layer. Providers wrapped behind one internal contract.
  5. Signing boundary. Connected wallets sign; no server signing, no agent-wallet spend.
  6. Receipt & audit trail. Normalized receipts give the product memory beyond chat.
  7. Evaluation harness. Command fixtures (valid, ambiguous, blocked, high-risk, and prompt-injection) so intent compilation is reliable by test, not by hope.

Scope & exclusions

V1 optimizes for safe, everyday Solana actions. The exclusions are not a backlog; several are permanent product boundaries.

In scope (V1)
  • Wallet connect
  • Balances & holdings
  • Token safety checks
  • Send & split funds
  • Swap via Jupiter unsigned tx
  • Close empty accounts / reclaim rent
  • Receipts & action history
Out of scope
  • Embedded / hot wallets
  • Private-key import
  • Keeper automations & DCA
  • Take-profit / stop-loss execution
  • Snipers, bundlers, wash volume
  • Vamping, bypass attribution, uPloys
  • Fund-flow obfuscation / privacy claims
Permanently blocked
Wash trading, fake volume, coordinated multi-wallet demand simulation, snipers and launch bundlers, supply washing, same-block multi-wallet launch buys, attribution hiding, and any Jito bundle flow designed to fake demand or hide coordination. These are not exposed through UI, API, or prompt paths.

Threat model

ThreatMitigation
Server key compromise drains usersServer holds no user keys and never signs, so there is nothing to compromise into a spend.
Model hallucinates an actionLLM output is advisory; typed intents are validated and previews come from structured diffs, not model claims.
Prompt injection forces a transferBlocked behavior is refused at the intent layer; policy runs before any build; the user still sees and signs the real preview.
Preview / execute mismatchThe execute path verifies the exact message hash against the signed preview token; any modification is rejected.
Replay or stale broadcastPreviews carry blockhash + lastValidBlockHeight; expired previews fail verification.
Key material in a request bodyEvery route rejects private keys, seed phrases, mnemonics, and keypairs before business logic.
Malicious / unverified token in a swapToken safety check is required before swap preview; swap cannot proceed without a result or explicit override.

Data handling stays minimal: only preferences, limits, labels, receipts, and optional command history. The product does not claim on-chain activity is private, and does not sell or share wallet data.

Status & roadmap

The trust spine is built: intent, preview, wallet-signed execute, receipt status, portfolio reads, token safety, close-account rent reclaim, and Jupiter swaps all run through the non-custodial path above. Current and upcoming work:

  • Now. Production frontend: command workspace, preview / receipt cards, read-only holdings and token-safety surfaces.
  • Next. Real LLM behind the intent compiler, backed by the evaluation harness; enhanced receipt enrichment.
  • Later, behind adapters. Helius Sender and Jito landing for reliability only, never to introduce sniper/bundler behavior.
  • Gated. Fee claiming once an exact unsigned flow is verified; token launch only when the launch path returns an unsigned transaction for the connected wallet to sign.

Feedback

This document is a draft shared for technical review. The questions most worth a second pair of eyes:

  • Is the execute-side verification list complete enough to reject every transaction that doesn't match its preview?
  • Does binding the preview token to (previewId · messageHash · feePayer · blockhash · LVBH) close the replay and substitution gaps?
  • Are there write paths where server-built unsigned transactions could be coerced into something the preview didn't show?
  • Where would you expect the intent compiler to fail closed but doesn't?

Pushback on the scope boundaries, especially the permanently blocked list, is welcome too. If a flow seems profitable but we’ve ruled it out, that’s usually deliberate; tell us where the line looks wrong.