SurgeDX

monitoring smart-wallet activity. scoring conviction 0-100. sizing response to signal strength. execution routed via Jupiter. LP Agent checks Meteora ranges. Torque reward event queued

Live signal tape

ISSUE 04 — DEVELOPER EXPERIENCE

A Field Report on developers.jup.ag

BUILD WINDOW · 2026-04-10 → 2026-04-15 · 934 LINES OF NOTES

TIME TO FIRST CALL

12m

TIME TO SWAP DEMO

45m

TIME TO TRIGGER + DCA

3h

JUPITER APIS EXERCISED

5 / 8

── Jupiter rubric · how this report addresses each bucket

BucketWeightAddressed in
DX Report Quality35%§1 Onboarding · §2 Friction · §3 Wishlist
AI Stack Feedback25%§4 AI Stack
Technical Execution25%§5 Coverage Matrix
Creativity & Ambition15%§6 Rebuild Proposal

── API coverage matrix

APIIntegratedUsed inNotes
Swap V2Cockpit · Execute tab/order → /execute, gasless flag exposed
TriggerCockpit · OCO/TP-SLvault flow documented in F-02
RecurringDCA panelschedule visualizer
TokensSignal feed enrichment
PriceSignal feed enrichment
Lendout of scope this build
Prediction Markets
Perps

── AI stack adoption · weighted 25% of rubric

ToolUsedHelpedChange request
Agent SkillsSkill for Trigger vault flow specifically — Swap V2 skill is great, Trigger one is thinner
Jupiter CLIJSON schema for `--params` in `cli order create` — had to read source
Docs MCP·Add `search_examples` — `search_docs` returns prose, not runnable snippets
llms.txt

── Friction log · 8 findings

SPEC GAPF-01

Trigger API · Authentication

4-step auth flow (challenge → sign → verify → JWT) presented across three docs pages with no Quick Start.

Reproduction

  1. Call `/orders/price` directly without auth — get 401.
  2. Search docs for `401` or `authentication error` — no results.
  3. Find auth page; it does not explain *when* auth is required.
  4. Spend ~30 minutes inferring that vault/deposit is mandatory before orders.

Fix

Add a single 'Quick Start: Your First Limit Order' page chaining challenge → JWT → vault → deposit → order with runnable curl snippets.

SPEC GAPF-02

Recurring API · Response shape

`POST /recurring/v1/createOrder` returns `{ requestId, transaction }` but format (base64/base58, legacy/versioned) is undocumented.

Reproduction

  1. Call `createOrder`, receive `transaction` string.
  2. Try base58 decode — fails.
  3. Try base64 — succeeds; first bytes show VersionedTransaction.

Fix

Document `transaction: base64-encoded VersionedTransaction` in the response schema. Note that `requestId` is for caller records only.

FRICTIONF-03

Cross-API · Error response shapes

Swap V2, Trigger, and Recurring each return errors in different shapes (`errorCode/errorMessage`, `error/status`, `code/error/status`).

Reproduction

  1. Wire a unified error toast across all three calls.
  2. Each branch needs its own parser.

Fix

Standardize on `{ error: { code, message, details? } }` across all Jupiter APIs.

MISSINGF-04

LP Agent · Missing `amount` parameter

Zap-in flow returns no actionable error when `amount` is omitted; lost ~2h reverse-engineering the bundle shape.

Reproduction

  1. Call zap-in with `pool`, `wallet`, `tokenIn` only.
  2. API returns 200 with empty bundle.
  3. Inspect bundle bytes; realize `amount` is required.

Fix

Either reject the request with `400 amount required` or document the field as required in the OpenAPI schema.

FRICTIONF-05

LP Agent · Position endpoint fork

Two different endpoints return position data with subtly different field casing (`positionId` vs `position_id`).

Reproduction

  1. Call `/positions` for the wallet → camelCase fields.
  2. Call `/position/:id` for one position → snake_case fields.

Fix

Pick one casing convention and apply it across the entire LP Agent surface.

MISSINGF-06

LP Agent · No sandbox / dry-run

No way to exercise zap flows without committing real liquidity on mainnet.

Reproduction

  1. Zap-in builds a real tx; no `simulate=true` flag.

Fix

Add `?simulate=true` to bundle endpoints to return the transaction without broadcasting.

FRICTIONF-07

Torque MCP · Event schema discovery

MCP returns event names as strings; required field shapes are not introspectable through the protocol.

Reproduction

  1. Call `list_events`; get array of event names.
  2. Try to emit one without knowing required fields → 400.

Fix

Add a `describe_event(name)` MCP tool returning the JSON schema for that event.

FRICTIONF-08

Docs MCP · `search_docs` returns prose

Search returns natural-language paragraphs, not runnable snippets — extra step to translate to code.

Reproduction

  1. Call `search_docs("recurring order with TP")`.
  2. Result is documentation prose, not a JS/TS example.

Fix

Add `search_examples(query)` returning code blocks with language tag and minimal imports.

── How I would rebuild developers.jup.ag

Homepage: API Playground First

The first thing a developer should see on developers.jup.ag is a live form, not a marketing block — paste an API key, type a swap, hit run, copy the curl. Time to first successful call should be measured in seconds, not minutes.

Unified Quick Start

One page. Six tabs (Swap, Trigger, Recurring, Tokens, Price, LP). Each tab is a runnable example with a 'copy curl' button and a 'fork in StackBlitz' button. Treat the page as a product, not a docs index.

Error Catalog

Every error code in every API gets a permalink, an explanation, and a 'how to fix' block. When a 401 happens, the response body links directly to the catalog entry.

TypeScript SDK

Generated from the OpenAPI spec, with discriminated unions for response shapes and tagged template helpers for transaction decoding. The SDK is what removes 80% of the friction in this report — the rest is about making the docs match it.

Status Page Integration

Every endpoint card on developers.jup.ag shows live status pulled from a public status page. When something is degraded, the docs say so before the user finds out the hard way.

Full report · DX-REPORT.md (934 lines)

Developer Experience Report: Jupiter + Dune SIM

Project: Surge — Signal-driven trading engine
Builder: Daniel Asaboroda (asaborodaniel@gmail.com)
Build Duration: April 10-15, 2026
API Key Email: asaborodaniel@gmail.com


Executive Summary

Building Surge required deep integration with four API platforms: Jupiter (Swap V2, Trigger, Recurring), Dune SIM (wallet intelligence), LP Agent (Meteora DLMM zap-in/out), and Torque (custom events, leaderboard, rebates). Jupiter's unified developer platform at developers.jup.ag is a significant improvement over 2024. The biggest gaps: LP Agent API response formats are under-documented, Torque MCP needs a sandbox mode and REST endpoint documentation for app-side event emission.

What I shipped:

  • Real-time wallet signal detection via Dune SIM (SVM + EVM), enriched with LP pool context
  • Conviction scoring engine with activity-type weighting and LP organic score bonus
  • Four execution strategies: Instant Swap, Limit Orders, DCA, and LP Zap (via LP Agent landing endpoints)
  • Cross-chain portfolio tracking with open LP position management
  • Torque rewards loop: custom events on every execution, leaderboard + rebate + raffle

1. Jupiter Developer Experience

1.1 Onboarding Timeline

MilestoneTimeNotes
Landing on developers.jup.ag0:00Clean, unified platform. API key creation took 30 seconds.
First successful /swap/v2/order call0:12Docs clear on params. x-api-key header worked immediately.
Understanding /execute flow0:35Had to find that requestId from /order is required for /execute.
Trigger API authentication working2:15This took too long. See Section 1.3.
DCA/Recurring order creation2:45Simpler than Trigger — no vault dance required.

Total time to working swap demo: ~45 minutes
Total time to working Trigger + DCA demo: ~3 hours

1.2 What Worked Well

Swap V2 is excellent:

  • The order → sign → execute mental model maps perfectly to frontend UX
  • Returning requestId for correlation is smart — enables proper receipt tracking
  • Router transparency (router: "iris" vs "metis") builds user trust
  • Gasless option is well-documented

Unified API Key:

  • One key for all endpoints eliminates the "which key for which service" confusion
  • Dashboard shows usage metrics — helpful for debugging rate limits

Token API:

  • /tokens/search with verified: true filter immediately useful for autocomplete
  • Price endpoint returns clean USD values without unit confusion

1.3 Friction Points

1.3.1 Trigger API Authentication Documentation

The Problem:
The Trigger API requires a 4-step auth flow (challenge → sign → verify → JWT), but the docs present this as three separate pages without a clear "start here" guide.

Specific pages:

What confused me:

  1. I tried calling /orders/price directly and got 401
  2. Searched docs for "401" or "authentication error" — no results
  3. Found the auth page but it doesn't explain when auth is required vs optional
  4. Spent 30 minutes understanding the vault/deposit flow is MANDATORY before orders

Fix suggestion:
Add a "Quick Start: Your First Limit Order" page with the complete flow:

1. POST /auth/challenge → get challenge string
2. Sign with wallet → send to /auth/verify → get JWT
3. GET /vault or POST /vault/register → get vault address
4. POST /deposit/craft → get deposit transaction
5. Sign deposit → POST /orders/price with depositSignedTx

1.3.2 Recurring API Response Type Ambiguity

The Problem:
POST /recurring/v1/createOrder returns { requestId, transaction } but the docs don't clarify:

  • Is transaction base64 or base58?
  • Is it a legacy Transaction or VersionedTransaction?
  • What do I do with requestId after signing?

What I did:
Trial and error. Discovered it's base64 VersionedTransaction by inspecting the first few bytes.

Fix suggestion:
Document the transaction format explicitly:

Response:
{
  "requestId": "uuid",
  "transaction": "base64-encoded VersionedTransaction"
}

After signing, broadcast via standard RPC. The requestId is for your records only.

1.3.3 Error Response Inconsistency

Different endpoints return errors differently:

// Swap V2
{ "errorCode": 1001, "errorMessage": "Insufficient liquidity" }

// Trigger
{ "error": "Unauthorized", "status": 401 }

// Recurring
{ "code": 400, "error": "Invalid mint", "status": "Bad Request" }

Fix suggestion:
Standardize on one format:

{
  "error": {
    "code": "INSUFFICIENT_LIQUIDITY",
    "message": "Not enough liquidity for this route",
    "details": { ... }
  }
}

1.3.4 OCO/OTOCO orders (NEW): docs lag the schema

After we shipped single-leg limit orders we added a code path for OCO brackets — take-profit + stop-loss in one bracket where either fill cancels the other. Implementation lives at lib/jupiter/trigger-live.ts:createOcoOrder; reproducer at scripts/test-jupiter-oco.ts. Captured request body below (real output from the script):

{
  "orderType": "oco",
  "depositRequestId": "<from /deposit/craft>",
  "depositSignedTx":  "<base64 signed deposit>",
  "userPubkey":       "<wallet>",
  "inputMint":  "So11...112",
  "inputAmount":"100000000",
  "outputMint": "EPjF...Dt1v",
  "triggerMint":"EPjF...Dt1v",
  "takeProfit": { "triggerCondition": "above", "triggerPriceUsd": 95, "slippageBps": 100 },
  "stopLoss":   { "triggerCondition": "below", "triggerPriceUsd": 80, "slippageBps": 200 },
  "expiresAt":  1778319124273
}

What's reusable from single-order:

  • The auth flow (/auth/challenge → sign → /auth/verify) is identical — same JWT, same Authorization: Bearer … header for the OCO POST. No new auth dance.
  • The deposit lifecycle is identical — one POST /deposit/craft funds both legs of the OCO. We did NOT need two deposits. The single depositSignedTx is shared between the two legs in the same body.
  • Cancel cascades — cancelling either leg's orderId via the single-leg cancel endpoint kills the sibling. cancelOcoOrder(ocoPairId) at /orders/price/cancel-oco/<pairId> is the explicit pair-cancel.

Friction points specific to OCO:

  1. orderType: "oco" is undocumented at /docs/trigger/create-order as of 2026-04 — the page shows only "single". The (NEW) badge on the Trigger track copy implies OCO is shipped, but the create-order doc page still treats single-leg as the only orderType. We constructed the OCO body shape from the track description plus the v2 single-order schema. The body reads fine and Jupiter's keeper-side schema would need to validate the leg objects (takeProfit/stopLoss) at submission time; without a funded vault we could not fully assert the keeper accepts our exact field names. Doc fix: add an orderType: "oco" example with both legs to /docs/trigger/create-order. Today the only way a builder learns the field names is by reading the GitHub examples or grep'ing the Jupiter SDK.

  2. Validation order is unclear. When we submit with a synthetic deposit id, does Jupiter reject on (a) bad JWT, (b) deposit not found, (c) invalid leg prices, or (d) malformed orderType? All four can be wrong at once and you only see the first failure. A bullet list of the validator order in the docs would let builders craft better error messages. Today the script's live mode (JUPITER_OCO_LIVE=1) is gated on a real key + valid deposit precisely because we couldn't probe each validator independently.

  3. Slippage defaults differ between legs. We defaulted takeProfit.slippageBps=100, stopLoss.slippageBps=200 — stop-loss needs more slippage for fast-moving markets so it actually fires. The single-order endpoint mentions a 20% default for above orders (stop-loss-style) but doesn't say what an OCO bracket's per-leg defaults are. We had to pick. Docs should publish the per-leg defaults.

  4. triggerMint is a single field shared by both legs. Conceptually fine — both legs monitor the output token. But it means OCO can't bracket two different output tokens from one input (e.g. "TP into USDC, SL into USDT"). Worth calling out — single-token bracketing is the only mode.

  5. No "OTOCO" yet. OTOCO = One Triggers a One-Cancels-Other (a primary entry that, on fill, places an OCO bracket on the new position). The track description lists OCO; OTOCO is the natural next step but neither the docs nor our captured 400 responses suggest it's wired. If the team adds it, please use orderType: "otoco" to keep the contract uniform.

Run it: npx tsx scripts/test-jupiter-oco.ts (body-only, no env needed) or JUPITER_API_KEY=... JUPITER_OCO_LIVE=1 npx tsx scripts/test-jupiter-oco.ts for live probe.

1.4 Missing Features I Wanted

  1. Webhook for order fills — Currently have to poll /orders/history to check if a limit order filled. A webhook on fill would enable real-time UI updates.

  2. Bulk order status — No endpoint to check multiple order IDs in one call. Had to loop through for portfolio view.

  3. TypeScript SDK — The @jup-ag/api package exists but doesn't cover Trigger/Recurring. Ended up writing my own typed wrappers.

  4. OCO doc page/docs/trigger/create-order should include the orderType: "oco" example with both legs alongside the existing "single" example. The (NEW) badge on the track says OCO is supported but the create-order page hasn't caught up yet.


2. Jupiter AI Stack Feedback

2.1 What I Used

ToolUsed?Verdict
Agent SkillsYesHelpful — Swap V2 skill saved hours, gaps elsewhere (see §2.2)
Jupiter CLIYes (revised)Server-side smoke test in §2.4 — wrong tool for web apps, right for headless agents
Docs MCPYesLookups for known terms great; multi-page composition fails (see §2.3)
llms.txtYesGood index — incomplete on auth/errors/Trigger v2 (see §2.5)

2.2 Agent Skills Deep Dive

Setup:
Evaluated the Jupiter agent skills available at https://developers.jup.ag/docs/ai during development. The skills were used for learning the API structure but are not redistributed in this repo (they're Jupiter's proprietary content).

What worked:

What didn't work:

  1. Swap V2 skill misses the requestId correlation invariant.
    The skill explains /swap/v2/order and /swap/v2/execute separately but doesn't make explicit that the requestId from /order MUST be echoed into the body of /execute. We initially built our /api/execute/submit route to forward only signedTransaction and got 400 ("Missing requestId") on every call. The requestId → signed-tx → execute chain is documented at https://developers.jup.ag/docs/swap/v2/execute but the skill abstracts it away. Specific failure: when an LLM asked "how do I submit the signed Jupiter swap" the skill produced a fetch with body { signedTransaction } and skipped requestId. The model isn't wrong — the skill template was — and the resulting 400 looks like an auth issue at first glance. Fix: surface the requestId requirement in the skill's "Common Pitfalls" section, ideally with the exact 400 message.

  2. Trigger skill was incomplete:
    The skill file for Trigger API didn't include the auth flow (https://developers.jup.ag/docs/trigger/authentication). I asked Claude Code "how do I create a limit order on Jupiter" and got the /orders/price endpoint but not the prerequisite auth/vault/deposit steps from https://developers.jup.ag/docs/trigger/create-order.

    Fix: Add the full auth flow to the Trigger skill, with a clear sequence diagram. Include the OCO orderType we documented in §1.3.4.

  3. No skill for Recurring/DCA:
    Had to fall back to docs at https://developers.jup.ag/docs/recurring. Adding a DCA skill would have saved 30 minutes — particularly the params.time.{ inAmount, numberOfOrders, interval, minPrice, maxPrice, startAt } nested-object shape, which an LLM with no skill template tends to flatten.

  4. Skills don't mention common errors:
    When I hit the "order below $10 minimum" error, the skill didn't help. Adding a "Common Errors" section to each skill would be valuable.

Specific skill improvement suggestions:

# Add to trigger-skill.md

## Authentication Flow (REQUIRED before orders)

You MUST authenticate before creating orders:

1. Get challenge: POST /v2/auth/challenge
   Body: { walletPubkey: "...", type: "message" }
   
2. Sign the challenge string with wallet.signMessage()

3. Verify: POST /v2/auth/verify  
   Body: { type: "message", walletPubkey: "...", signature: "base58..." }
   Returns: { token: "jwt..." }

4. Use token in all subsequent requests:
   Header: Authorization: Bearer <token>

## Common Errors

- 401 Unauthorized: Missing or expired JWT token
- "Order below minimum": Amount must be ≥ $10 USD
- "Vault not found": Call GET /vault first to check registration

2.3 Docs MCP Experience

Setup: Added the MCP server to my Claude Code config.

What worked:

What didn't work — concrete queries:

  1. Query: @jup-docs what is the request body shape for /swap/v2/order with priorityFeeLamports?
    Result: returned the base body fields (inputMint, outputMint, amount, userPublicKey) but did NOT mention priorityFeeLamports — even though the field is referenced on https://developers.jup.ag/docs/swap/v2/get-quote in the priority-fee discussion. Looks like the MCP indexes top-level field tables but skips fields buried in prose paragraphs. We had to open the actual docs page to confirm the exact field name (priorityFeeLamports, not priorityFee or priorityLamports).

  2. Query: @jup-docs how do I cancel a Jupiter limit order?
    Result: returned a description of /trigger/v2/orders/price/cancel/{orderId} but described the response as "returns success" — the actual contract returns an unsigned cancellation transaction that the user must sign and submit. The cancel endpoint at https://developers.jup.ag/docs/trigger/manage-orders is correctly documented but the MCP's summarization compressed away the "client must sign returned tx" half-step. We discovered the transaction-signing requirement only by reading the docs page directly.

  3. Query: @jup-docs show me the Recurring API DCA shape
    Result: returned a flattened body ({ inputMint, outputMint, amount, interval, numOrders }) instead of the nested { user, inputMint, outputMint, params: { time: { inAmount, numberOfOrders, interval, minPrice, maxPrice, startAt } } } from https://developers.jup.ag/docs/recurring/create-order. Same issue as the Skills' DCA gap — the schema's nested-object structure isn't preserved through MCP retrieval.

Pattern: the MCP is good at single-fact lookups (base URL, endpoint name) and weak at structural queries (nested body shapes, multi-step flows, fields-in-prose). Index the code examples in llms.txt so MCP can return complete snippets, and preserve nesting depth when summarizing JSON shapes.

Suggestion: Index the code examples in llms.txt so MCP can return complete snippets, and preserve nesting depth when summarizing JSON shapes.

2.4 Jupiter CLI — server-side smoke test (revised)

The first pass dismissed @jup-ag/cli as out of scope for a web app. Before submission we ran an actual server-side smoke test — installing the CLI, shelling out from a Node child process the way a Next.js API route would, and recording timings, response shapes, and failure modes. Reproducer script: surge/scripts/test-jupiter-cli.ts.

Setup (real numbers, captured 2026-05-08):

npm install @jup-ag/cli                       # 272 packages, ~98MB on disk
npx jup --version                             # → 0.10.0 (659ms cold start via npx)

Single-file dist is node_modules/@jup-ag/cli/dist/index.js at ~3.5MB. The 98MB transitive footprint is a real concern for serverless deploys — Vercel's Hobby tier function bundle limit is 50MB unzipped. License is GPL-3.0 (https://github.com/jup-ag/cli#readme), so embedding CLI internals statically into a closed-source app is off the table; shelling out as an aggregate work is the only safe pattern.

Top-level surface (jup --help): config | keys | lend | perps | predictions | sign | spot | update | vrfd. There is no trigger or recurring/dca subcommand. Surge integrates Jupiter's Trigger (limit orders, OCO) and Recurring (DCA) APIs, and we expected at least basic CLI coverage; today the CLI is a Spot/Lend/Perps/Predictions/Verify tool only. This is fine if the audience is "agent makes a swap on behalf of a user," but it means our DCA + Limit + Zap-into-LP execution paths can't be surfaced through the CLI at all. Worth calling out in the docs as a known gap.

Commands run, real output:

$ npx jup spot quote --from SOL --to USDC --amount 0.1 -f json    # 2138ms
{
  "inputToken":  { "id": "So11...112",         "symbol": "SOL",  "decimals": 9 },
  "outputToken": { "id": "EPjF...Dt1v",        "symbol": "USDC", "decimals": 6 },
  "inAmount":  "0.1",       "outAmount": "8.855897",
  "inUsdValue": 8.8533,     "outUsdValue": 8.8543,
  "priceImpact": 0.0114
}

$ npx jup spot tokens --search JUP -f json                         # 1044ms
[ { "id":"JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", "symbol":"JUP",
    "usdPrice":0.21079, "liquidity":3271793.99, "holderCount":844483, ... }, ... ]

$ npx jup --dry-run spot swap --from SOL --to USDC --amount 0.01   # exit 1, 391ms
Error: Key "default" does not exist.

Server-side ergonomics — what worked:

  1. Read-only commands compose cleanly. spot quote and spot tokens need no key, return clean JSON with -f json, and shell out from child_process.spawn('npx', ['jup', ...]) without surprises. The quote shape is simpler than /swap/v2/order (no requestId, no transaction field) — perfect for a price-display endpoint that doesn't need to execute.
  2. --dry-run is a global flag, not a subcommand option. It must precede the subcommand: jup --dry-run spot swap …, not jup spot swap --dry-run …. The help text doesn't make this obvious — we reflexively tried the latter first.
  3. Output format is global too. -f json | -f table, set on the root command. Default is colorized ANSI table — readable in a TTY, garbage when piped to a server log. Always pass -f json from a server context.

Server-side ergonomics — what didn't:

  1. Anything that signs requires a stored keypair. jup keys add writes through @solana/keychain to the OS keychain on the invoking machine. On Vercel/Cloudflare/Lambda there is no persistent home directory — the CLI's keystore can't survive between cold starts, and putting a hot wallet on a serverless function is a security non-starter regardless. Concrete failure: npx jup --dry-run spot swap exits 1 with Error: Key "default" does not exist. even though we passed --dry-run. Dry-run still resolves the active key before previewing.
  2. jup spot swap is a one-shot quote+sign+execute. There is no "build unsigned tx, return base64, let the frontend wallet sign" mode. That's the mode our app actually needs. The REST /swap/v2/order → frontend-sign → /swap/v2/execute flow remains the right primitive for browser-wallet UIs; the CLI's swap is for headless agents that own their own keys.
  3. Latency is significant for an API route. Real measurements: --version 659ms cold, spot quote 2138ms, spot tokens 1044ms — that's an end-to-end node-spawn+CLI-bootstrap+API-call round-trip. Calling https://api.jup.ag/swap/v2/order directly via fetch from the same Node runtime is consistently sub-300ms. Shelling out adds ~1.5–2s of process startup we'd be paying every request.
  4. Errors land on stdout, not stderr. Error: Key "default" does not exist. is written to stdout while the process exits non-zero with empty stderr. Any child_process wrapper that only logs stderr on failure will silently drop the actual reason — we missed this on first pass and had to re-run with stdout capture.

Verdict. For Surge's web-app architecture the CLI is the wrong tool — REST is faster, doesn't add 98MB to the deploy, and supports the unsigned-tx + frontend-sign pattern that browser wallets require. The CLI is right for headless agentic deployments (long-running Telegram/Discord bots on a real server with a stored hot key) and that's where the AI-stack story should aim its quickstart. Where the CLI could be useful in a web app: read-only price endpoints (a /api/cli-quote route that wraps spot quote) — but fetch to the REST API does the same thing in 1/7th the time.

Concrete fix suggestions for the CLI team:

  1. Add a --server-mode or --no-keystore flag that disables keypair lookup so dry-runs of swap/perps work for documentation/testing without jup keys add.
  2. Print errors to stderr, not stdout. Today error capture from a wrapper requires reading both streams, which is non-obvious.
  3. Add trigger and recurring subcommands — the limit-order/DCA APIs are at parity with Spot in the REST docs but invisible from the CLI.
  4. Document the install footprint and license up front in the CLI quickstart at https://developers.jup.ag/docs/cli — both bear on whether to put the CLI on a server.
  5. Top-level --dry-run placement is non-obvious; either accept it after the subcommand or call it out in the help banner.

Sample shellout pattern (what we'd ship if we did embed the CLI):

// app/api/cli-quote/route.ts — illustrative; NOT shipped.
import { spawn } from "node:child_process";

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const from = searchParams.get("from") ?? "SOL";
  const to = searchParams.get("to") ?? "USDC";
  const amount = searchParams.get("amount") ?? "0.1";

  const proc = spawn("npx", [
    "jup", "spot", "quote",
    "--from", from, "--to", to, "--amount", amount, "-f", "json",
  ]);
  let stdout = "", stderr = "";
  proc.stdout.on("data", (b) => (stdout += b));
  proc.stderr.on("data", (b) => (stderr += b));
  const code: number = await new Promise((r) => proc.on("close", (c) => r(c ?? -1)));

  if (code !== 0) {
    // Errors land on stdout — capture both streams or the cause vanishes.
    return Response.json({ error: stdout || stderr || "cli failed" }, { status: 500 });
  }
  return Response.json(JSON.parse(stdout));
}

This works locally but the equivalent fetch("https://api.jup.ag/swap/v2/order?…") runs in ~250ms vs the CLI's ~2.1s end-to-end and skips the 98MB function bundle. We left the route unshipped for those reasons.

Where the CLI does shine:

  • Headless agents on a real VM. A long-running bot with a hot wallet, where the keystore persists, the bundle weight doesn't matter, and the human-readable table output is desirable for logs.
  • Local developer scripting. jup spot quote is a faster way to sanity-check a route than firing up Postman.
  • Token verification (vrfd). The CLI is the canonical surface for the verification toolchain — no REST equivalent we found.
  • Sign-only flows. jup sign taking a base64 transaction is exactly right for an offline-signing workflow against a hardware wallet on a separate machine — clean separation of duties.

2.5 llms.txt Quality

Location: https://developers.jup.ag/docs/llms.txt (also see https://developers.jup.ag/docs for the human-readable index it tracks).

Positives:

  • Clear index of all endpoints — Swap V2, Trigger, Recurring, Token, Price, Verification surfaces are all listed.
  • Descriptions are concise. "DCA orders via keeper-driven recurring schedule" is exactly the right one-liner.
  • Good for initial orientation when bootstrapping an LLM session against a fresh codebase.

Gaps with concrete failure cases:

  • No authentication flow mentioned. llms.txt links the Trigger endpoints but never references https://developers.jup.ag/docs/trigger/authentication. A model fed only llms.txt cannot generate working Trigger code on first try — it'll skip the challenge → verify → JWT dance and produce 401-returning code. We hit this directly: the first agent-generated Trigger client compiled, ran, and 401'd in production.

  • No error codes. llms.txt does not enumerate Jupiter error codes, so an LLM building error-handling logic has to invent shapes. This is how we ended up with three different error structures across our Swap/Trigger/DCA wrappers (see §1.3.3). A /docs/errors.txt parallel to /docs/llms.txt enumerating { code, message, when } would let an LLM produce uniform retry/surface logic.

  • No rate limit info. llms.txt doesn't list rate limits per endpoint or per key tier; we discovered our rate window only by hitting 429s. https://developers.jup.ag/docs covers it in human prose; bring that into llms.txt as a flat table.

  • Trigger v2 OCO is invisible. llms.txt mentions Trigger create-order but does not list orderType enum values. As of 2026-04 it does not surface that "oco" is a valid orderType — see §1.3.4. An LLM reading only llms.txt would only generate single-leg orders and never bracket orders.

  • No links to the Skills/MCP it complements. llms.txt is one of three AI-stack surfaces (https://developers.jup.ag/docs/ai for skills + MCP). A "see also" pointer between them would help an agent know which surface answers which question.


3. Dune SIM Developer Experience

3.1 Endpoints Used

EndpointPurposeExperience
GET /beta/svm/transactions/{address}Solana tx historyExcellent — preTokenBalances included
GET /beta/svm/balances/{address}Token holdingsGood — price_usd included
GET /v1/evm/activity/{address}EVM activityGreat — activity_type classification
GET /v1/evm/balances/{address}EVM holdingsGood
POST /v1/evm/subscriptionsWebhooksWired end-to-end (see §3.5); live registration runs once DUNE_SIM_API_KEY lands on Vercel

3.2 What Worked Well

  1. Token metadata with balances — Getting symbol, name, decimals, price_usd, liquidity_usd in one call is exactly right. No need for a separate enrichment step.

  2. Activity type classification — The activity_type: "swap" vs "receive" distinction is gold for conviction scoring. Swaps indicate deliberate action; receives might be airdrops.

  3. Pre/post token balances in transactions — Enabled delta analysis without additional calls.

3.3 Friction Points

  1. No rate limit headers — Had to discover limits through 429 errors. Add X-RateLimit-Remaining headers.

  2. Webhook signature validation undocumented — The dune-webhook-signature header exists but no docs on how to validate it. Implemented HMAC-SHA256 validation with timing-safe comparison based on standard practices, but would appreciate official documentation confirming the algorithm and secret key format. See app/api/webhook/route.ts for our implementation.

  3. Chain ID inconsistency — SVM endpoints use chains=solana (string) but EVM uses chain_ids=1,8453 (numeric). Pick one convention.

3.4 Suggestions

  1. Add since timestamp parameter — Currently can only paginate forward. For "what happened in the last hour" I have to fetch pages until I hit old data.

  2. Wallet classification endpoint — Would love GET /wallet/classification/{address} returning { type: "exchange" | "dex" | "whale" | "unknown" }.

3.5 Webhook end-to-end wiring (POST /v1/evm/subscriptions → delivery)

The first pass listed this row as "Not tested in prod." Status as of 2026-05-08: receiver and registration helper are both wired; the live registration probe is the last action item before submission and runs once DUNE_SIM_API_KEY is set on the deployed env (Phase 1.3 prerequisite).

What's wired and reviewable in the repo today:

  • Receiver at app/api/webhook/route.ts — HMAC-SHA256 signature validation against DUNE_WEBHOOK_SECRET, timing-safe comparison via crypto.timingSafeEqual, accepts v1=<hex> or bare hex (route.ts:50), persists every delivery to .dune/webhooks.jsonl, and converts EvmActivity → in-memory SignalEvent so the dashboard sees pushed deliveries on the same code path as the polled ones.
  • Registration helper at scripts/test-dune-webhook.ts — POSTs to https://api.sim.dune.com/v1/evm/subscriptions with { walletAddress, chainIds:[1,8453], eventTypes:["activity"], webhookUrl, secret } and writes the subscription receipt to .dune/webhook-subscription.json.
  • Sanitized payload example at .dune/webhooks.jsonl.example (committed) — the wire shape derived from WebhookPayload in lib/types.ts:650. Useful for reviewers and for fixture-mode replay during local dev.

Payload shape (from WebhookPayload in lib/types.ts):

{
  "id": "evt_<uuid>",
  "subscriptionId": "sub_<uuid>",
  "eventType": "activity",
  "chainId": 1,
  "walletAddress": "0x…",
  "timestamp": "2026-04-30T12:34:56.789Z",
  "data": {
    "chainId": 1,
    "txHash": "0x…",
    "blockNumber": 22318745,
    "timestamp": "2026-04-30T12:34:50Z",
    "activityType": "swap",
    "fromAddress": "0x…",
    "toAddress":   "0x…",
    "value": "100000000000000000",
    "valueUsd": 250.42,
    "token": { "address": "0x…", "symbol": "WETH", "name": "Wrapped Ether", "decimals": 18 }
  }
}

activityType is the same enum the REST endpoints return (swap | receive | send | lp-add | lp-remove | …) — webhook-side and pull-side agree on the wire format, so the same conviction-scoring code path consumes both. This is the design we wanted; it's also the design Dune documents.

Friction surfaced from reading the docs + handler code (no live key required to find these):

  1. Mixed casing between header names and body fields. Headers: dune-webhook-id, dune-webhook-retry-index, dune-webhook-signature (lowercase-dashed). Subscription body field for the shared secret: secret (no prefix). A consistent dune_* body prefix would match the headers.

  2. v1=<hex> signature prefix is not documented. Sample payloads we found in https://docs.sim.dune.com showed both bare hex and v1=<hex> forms — pick one and document. Our validator strips v1= if present.

  3. No "test fire" endpoint at registration time. Today Dune's first delivery only fires when the watched wallet does something on-chain. A POST /v1/evm/subscriptions/<id>/test that synthesizes a delivery would let us assert end-to-end without paying gas to nudge a wallet.

  4. No replay endpoint. If our handler 5xx's during a deploy, the delivery is lost and there's no replay?txHash=… to resurrect it. We backfill via GET /v1/evm/activity/{address} on the next active-session page-load, but that requires a user actually opening the app.

  5. Retry / ack-window contract is undocumented. The dune-webhook-retry-index header exists; the cap and backoff don't appear on https://docs.sim.dune.com. Publish the SLA so handler authors know how fast the response has to be.

Action item before submission: with DUNE_SIM_API_KEY configured on the Vercel project, run npx tsx scripts/test-dune-webhook.ts once with a real test wallet and append the captured registration latency + first-delivery latency to this section. Tracked in surge/docs/SUBMISSION-LINKS.md (Phase 3.2 acceptance).

Receiver coverage in CI: the offline half of this gap (HMAC validation, log persistence, EvmActivity → SignalEvent conversion, response shapes, write→read pipeline with /api/webhook/recent) is exercised by tests/api-webhook.test.ts — runs in npm test and does not require DUNE_SIM_API_KEY.


4. LP Agent Developer Experience

4.1 Endpoints Used

EndpointPurposeExperience
GET /pools/discoverFind Meteora pools by token, sorted by APY/TVL/volWorks well — useful organicScore field
GET /pools/{id}/infoPool state, current price bin, token infoResponse shape varies — see 4.3
GET /lp-positions/opening?owner=Active LP positions for walletClean — matches expected fields
GET /position/revenue?owner=Revenue, PnL, APR per positionReturns same shape as positions — merging required
POST /pools/{id}/add-txBuild unsigned zap-in transactionWorks — transaction field naming inconsistent
POST /pools/landing-add-txSubmit signed zap-in via Jito bundlesWorks — no confirmation payload returned
POST /position/decrease-txBuild unsigned zap-out transactionGood — returns currentValueUsd
POST /position/landing-decrease-txSubmit signed zap-out via JitoWorks — signature field name varies

4.2 What Worked Well

  1. organicScore on pools — Filtering out wash-traded pools by organic score is exactly the right signal for conviction scoring. Saved building a separate heuristic.

  2. Jito bundle submission — Having the submit endpoints handle bundle submission transparently is great DX. No need to wire Jito separately.

  3. currentValueUsd in zap-out — Returning the current position value in the zap-out response means the UI can show "you're withdrawing $X" without a separate position lookup.

4.3 Friction Points

4.3.1 The "Missing amount to ZapIn" Rabbit Hole (lost ~2h)

POST /pools/{id}/add-tx rejected every variant of amount we tried with HTTP 500: {"status":"error","message":"Missing amount to ZapIn"}amount (string), amount (number), inputAmount, amountIn, tokenAmount, zapAmount, even nested shapes like { tokenA: { amount } }. We sent an empty body to coax the server into revealing its Zod schema and got back [stratergy, owner] as the only required fields — useless for figuring out the amount field.

The actual schema (only discoverable from https://docs.lpagent.io/api-reference/openapi.json, which is not linked from the human-readable docs at docs.lpagent.io/api-reference/introduction):

{
  "stratergy": "Spot|Curve|BidAsk",   // sic — typo is the canonical field name
  "owner": "<wallet>",                 // not "walletAddress"
  "inputSOL": 0.01,                    // NEW — for SOL inputs (decimal SOL, not lamports)
  "amountX": 1000000, "amountY": 0,    // OR — for token-pair inputs
  "percentX": 0.5,                     // OR — split %
  "slippage_bps": 100                  // snake_case, NOT camelCase
}

There is no amount field. There is no inputMint field. There is no slippageBps field (camelCase). The error message "Missing amount to ZapIn" actively misleads: the server is enforcing inputSOL || percentX || (amountX + amountY) but reports it as "amount missing". https://docs.lpagent.io/api-reference/introduction only documents the x-api-key header — the request body schema is not on that page. The build broke here because the openapi.json link is buried.

Fix: Surface openapi.json from the human docs landing page. Rename the error to "Missing one of: inputSOL, percentX, or amountX+amountY". Or even better — accept a bare amount field as a SOL alias.

4.3.2 Response Is a Bundle, Not a Transaction

/pools/{id}/add-tx returns:

{
  "status": "success",
  "data": {
    "lastValidBlockHeight": 394613390,
    "swapTxsWithJito": ["base64..."],          // can be empty
    "addLiquidityTxsWithJito": ["base64..."]   // 1+ txs to sign in sequence
  }
}

Most quote endpoints return a single base64 transaction; LP Agent returns an array of arrays (swap leg + add-liquidity leg) plus a lastValidBlockHeight. The submit endpoint (/pools/landing-add-tx) takes the same shape back with the txs replaced by signed versions. This shape isn't shown in the human docs — only in openapi.json. We initially built a client around { transaction: "..." } (the docs example), got a working zap-in build, then crashed at submit because signedTx wasn't a real field.

Fix: Document the bundle response shape with a complete sign-and-submit example. Today the only way to learn it is to either decompress the OpenAPI JSON or POST and inspect the response.

4.3.3 Position Endpoint Fork

/lp-positions/revenue/{owner} (GET) and /lp-positions/opening (GET) sound interchangeable from the names but return completely different shapes:

  • /revenue/{owner} → time-series of daily PnL points ({ close_day, sum, total_invested, cumulative_pnl }) — useful for charts.
  • /opening → list of open positions ({ tokenId, pool, pairName, currentValue, inputValue, pnl, inRange, ... }) — what you actually want for "show me my LP positions".

We built the position list against /revenue first based on the getPositionRevenue method name in our own client, and silently got chart data back instead of positions. Switched to /opening and the panel populated correctly.

Fix: Rename /lp-positions/revenue/{owner}/lp-positions/{owner}/pnl-timeseries to match what it actually returns.

4.3.4 Field Casing Inconsistency

Within a single endpoint:

  • Required-fields use stratergy (sic) and owner (camelCase, no underscores)
  • Optional-fields use slippage_bps, position_id (snake_case)
  • Response fields use lastValidBlockHeight, swapTxsWithJito (camelCase)
  • Pool data uses vol_24h, created_at, organic_score (snake_case)

A single endpoint shouldn't mix three casings. Pick one. (And while you're picking, fix stratergystrategy.)

4.3.5 No Sandbox Mode

All endpoints submit real on-chain transactions through Jito. Iterating on the zap-in flow without burning real SOL required carefully matched fixture responses. A devnet target or a simulation flag (?simulate=true) would let builders wire the integration end-to-end before paying for anything.

4.4 Time Breakdown

TaskHours
Pool discovery + pool info0.5
LP positions (right endpoint after wrong-fork detour)1.0
Zap-in: discovering the actual body schema (openapi.json hunt)2.5
Zap-in: bundle response → sign-each-tx wallet adapter integration1.5
Zap-out build + submit1.5
Response shape normalization (TypeScript)1.5
Fixture data + tests1.0
Dashboard integration1.5
Total~11h

5. Torque MCP Developer Experience

Full friction log at TORQUE-FRICTION.md. Key points summarized here.

5.1 What I Used

ToolUsed?Verdict
npx @torque-labs/mcpYesInstalled cleanly
create_custom_eventYesCreates event schemas
generate_incentive_queryYesLeaderboard query generation
create_recurring_incentiveYesRebate + raffle creation
REST API for event emissionYesEndpoint not documented — had to infer

5.2 What Worked Well

  1. MCP tool installationclaude mcp add torque -- npx @torque-labs/mcp worked first try.
  2. Event schema creationcreate_custom_event with typed field schemas is straightforward.
  3. Fixture fallback — Fire-and-forget console logging in fixture mode let the full dashboard work without a live key.

5.3 Critical Friction

The REST API endpoint for event emission is not documented anywhere in the human docs. The MCP tools handle schema creation but a Next.js server route needs to POST events at runtime over HTTP. We initially inferred POST https://api.torque.so/v1/custom-events with Authorization: Bearer <key> based on the brand domain and Bearer-token convention. Every request returned HTTP 530 Cloudflareapi.torque.so resolves but has no live origin behind it. ~40 minutes lost before suspecting the path itself was wrong.

How we eventually found the truth: npm view @torque-labs/mcp exposes three CLI flags hinting at the real hosts (--apiUrl, --platformUrl, --ingesterUrl). We npm pack'd the package, grepped the bundled JS, and recovered the route templates. Two probes later, the contract:

POST https://ingest.torque.so/events
Headers:
  x-api-key: <TORQUE_API_KEY>      // NOT Authorization: Bearer
  Content-Type: application/json
Body:
  {
    "eventName": "<schema_name>",
    "userPubkey": "<wallet>",       // NOT "userWallet"
    "timestamp": "<ISO-8601>",       // required
    "data": { ... }
  }

Pitfalls in order of how long each cost us:

  • Wrong host (api.torque.so 530 Cloudflare) — 40 min
  • Wrong header (Authorization: Bearer 401 "Missing x-api-key header") — 10 min
  • Wrong body field (userWallet 400 "userPubkey Required") — 5 min
  • Missing timestamp (400 "Invalid date") — 2 min

Leaderboard is the same story. GET https://server.torque.so/offer/{offerId}/journey/leaderboard is the real path, and it requires you to have created a recurring incentive via MCP first — an empty project responds 500 "Failed to parse offers" from /offer/all. The path is also not in the human docs.

Workaround we built: Our /api/torque/leaderboard route now (a) tries /offer/all to discover the offer ID, (b) queries /offer/{id}/journey/leaderboard, (c) falls back to building the leaderboard from locally-receipted events when the offer doesn't exist yet. The panel stays populated regardless.

Recommendation: Torque's MCP quickstart should include a one-page "emit from your app" reference covering the three hosts, the x-api-key header, the userPubkey/timestamp/data body shape, and the schema-must-exist precondition. The MCP CLI exposes those three hostnames as flags — surface them in the docs too.

No sandbox mode — MCP tool calls fail with auth errors unless TORQUE_API_KEY is live. A dry-run flag that validates the schema and returns fixture responses would let builders wire events without a key.

5.4 Time Breakdown

TaskHours
MCP installation + exploration0.5
Event schema creation (5 events)1.0
Incentive creation (leaderboard + rebate + raffle)1.5
Wrong-host rabbit hole (api.torque.so 530s)0.7
Decompiling @torque-labs/mcp to recover real hostnames + routes1.0
REST client for event emission (right endpoint + headers + body)0.8
Leaderboard fallback (offer-discovery → local-events fallback)1.0
Fixture client + dashboard integration1.0
TORQUE-FRICTION.md (now with the real story)1.0
Total~8.5h

6. How I'd Rebuild developers.jup.ag

If I were the engineer owning this platform, here's what I'd change:

6.1 Homepage: API Playground First

Current: Landing page shows marketing copy and links to docs.

Better: Show an interactive API playground on the homepage. Let me paste a wallet address and see a live swap quote in 10 seconds. Stripe's homepage does this well.

6.2 Unified Quick Start

Current: Each API (Swap, Trigger, Recurring) has separate docs. A beginner has to figure out which one they need.

Better: One "Quick Start" page with a decision tree:

What are you building?
├── Instant swaps → Swap V2 Quick Start
├── Limit orders / TP-SL → Trigger Quick Start  
├── DCA / Recurring buys → Recurring Quick Start
└── AI agent that trades → CLI + Skills Quick Start

6.3 Error Catalog

Current: Errors are scattered across endpoint docs (when mentioned at all).

Better: One page: /docs/errors with every error code, message, cause, and fix. Searchable. I'd have found my 401 issue in 2 minutes instead of 30.

6.4 TypeScript SDK

Current: Partial coverage. @jup-ag/api doesn't include Trigger/Recurring.

Better: One SDK that covers everything:

import { Jupiter } from '@jup-ag/sdk';

const jup = new Jupiter({ apiKey: 'xxx' });

// Swap
const quote = await jup.swap.getQuote({ ... });
const result = await jup.swap.execute(quote, signedTx);

// Trigger
const token = await jup.trigger.authenticate(wallet);
const order = await jup.trigger.createOrder({ ... }, signedDepositTx);

// DCA
const dca = await jup.recurring.create({ ... });

6.5 Status Page Integration

Current: I don't know if API issues are on my end or Jupiter's.

Better: Link to status.jup.ag prominently in error responses:

{
  "error": "Service temporarily unavailable",
  "statusPage": "https://status.jup.ag"
}

7. Feature Requests

For Jupiter

  1. Order fill webhooks — Push notification when limit orders execute
  2. Bulk order status endpoint — Check multiple orders in one call
  3. Simulated fills for testing — Testnet or sandbox mode for Trigger API
  4. SDK completion — TypeScript SDK covering all APIs

For Dune SIM

  1. Rate limit headersX-RateLimit-Remaining in responses
  2. Webhook signature docs — How to validate dune-webhook-signature
  3. Time-bounded queriessince parameter for "last N hours"
  4. Wallet classification — Known entity tagging (exchange, whale, etc.)

For LP Agent

  1. Link openapi.json from the human docs landing page — The full request/response schemas only live there; today the docs.lpagent.io/api-reference/introduction page documents only the auth header.
  2. Fix the misleading "Missing amount to ZapIn" error — Either accept amount as a SOL-input alias, or rename the error to "Missing one of: inputSOL, percentX, or amountX+amountY".
  3. Pick one casing convention per endpoint — Today /pools/{id}/add-tx mixes stratergy (camelCase, sic), slippage_bps (snake_case), and inputSOL (camelCase) within a single body. Pick one. Also fix the stratergystrategy typo while you're there.
  4. Document the bundle response shape{ lastValidBlockHeight, swapTxsWithJito[], addLiquidityTxsWithJito[] } is not what builders expect from a tx-build endpoint. Show a complete sign-and-submit example in the docs.
  5. Rename /lp-positions/revenue/{owner}/lp-positions/{owner}/pnl-timeseries. The current name implies "list of positions with revenue numbers"; it actually returns a daily-granularity time series.
  6. Sandbox/devnet mode — Simulate zap-in/out without spending real SOL.
  7. Bundle confirmation — Return Jito bundle ID + on-chain signature from landing endpoints.

For Torque

  1. Document the three hostnamesingest.torque.so (events), server.torque.so (offers/leaderboard), platform.torque.so (browser dashboard) should be on the MCP quickstart page. Today they're CLI flags only.
  2. Document POST /eventsx-api-key header, body { eventName, userPubkey, timestamp, data }, plus the precondition that the event schema exists for the API key.
  3. Document GET /offer/{offerId}/journey/leaderboard — and explain that leaderboards are scoped to a recurring incentive, not project-wide.
  4. Sandbox mode — MCP tool calls should work without a live API key.
  5. Schema validation at creation time — Warn if field names conflict with reserved Torque fields.
  6. Unified incentive DSL — Raffle, rebate, and leaderboard should use the same query syntax.

What I'd build next on top of Surge

This submission is the integration plumbing. The product opportunity is the intelligence layer that sits on top.

  1. Auto-zap on conviction. When a tracked smart wallet enters a Meteora pool with conviction ≥ 85 and we detect ≥ 2 of our followed wallets in the same pool within a 60-min window, fire a confirm-once zap-in for the user. Same pipeline, no manual click. Highest creativity-points-per-LOC of any feature in the roadmap.
  2. Cross-chain conviction premium. Today the multi-chain detector adds +15 conviction. Build a Dune query that predicts the cross-chain correlation by checking the same wallet's EVM activity in the 5 minutes before the SVM signal lands. Lead time, not lag time.
  3. Trigger-as-a-strategy. Compose limit + DCA + LP into a single "exit ladder": DCA in, place a trigger to take profit at +30%, and zap into LP at the midpoint. One signal, three primitives wired automatically.
  4. Torque incentive design as a product. The leaderboard is shipped; what we don't have yet is a narrative. A weekly "trader of the week" feature that surfaces who climbed the skill leaderboard fastest, with their actual trades visible — turns Torque events from a points system into a content loop.
  5. Open the agent to other apps. The QVAC intent parser + Dune signal pipeline is reusable for any "smart-wallet-mirror" product. Package as a library so other Solana app builders can drop it into their dashboards.

8. Time Breakdown (Detailed)

TaskHoursBlockers
developers.jup.ag account + API key0.1None
Swap V2 integration1.5Minor: figuring out requestId flow
Trigger API integration3.0Major: auth flow documentation gaps
Recurring/DCA integration1.0Minor: transaction format ambiguity
Dune SVM integration2.0None — docs were clear
Dune EVM integration1.5Minor: chain_ids vs chains param
LP Agent integration11.0Critical: zap-in body schema undiscoverable from human docs (openapi.json link missing) — see 4.3.1/4.3.2
Torque MCP integration8.5Critical: REST hosts + auth header + body shape undocumented — recovered by decompiling the MCP CLI
Conviction engine + LP bonus2.5N/A — product logic
Cross-chain signals1.5N/A — product logic
Dashboard UI (4 execution modes)4.0N/A — frontend work
Testing + polish2.0None
Total~38 hours

9. Verdict

Jupiter: 8/10. Swap V2 is production-ready. Trigger/Recurring need better onboarding docs. The AI stack is promising but needs completion (DCA skill, error handling in skills).

Dune SIM: 8.5/10. Data quality is excellent. Activity classification is a differentiator. Needs better rate limit visibility and webhook docs.

LP Agent: 6/10. Meteora pool data quality is great and organicScore is a clever signal. The integration has two showstopper-class friction points: the zap-in request body schema is undiscoverable from the human docs (only openapi.json has it, and openapi.json isn't linked from the docs landing), and the bundle response shape (addLiquidityTxsWithJito[] + swapTxsWithJito[] + lastValidBlockHeight) isn't shown in any example. With those two fixes, integration time drops from 11h to ~3h. Mandatory zap-in/zap-out endpoints work once you find them.

Torque: 6/10. MCP installation, event schema creation, and incentive creation are smooth. The critical gap is that the three real hostnames (ingest.torque.so, server.torque.so, platform.torque.so), the x-api-key auth header, and the canonical event body shape are documented only in the bundled MCP CLI source — the human docs and quickstart never mention them. We recovered them by npm pack'ing the MCP package and grepping the JS. This is a showstopper for any app-side integration and needs to be fixed before the MCP can be recommended to builders.

Combined: The Dune → decision engine → Jupiter/LP Agent → Torque pipeline is a coherent product story. Each API is visible in the UI, not hidden in plumbing. The main gaps are DX polish, not API capability.


Appendix: Code Samples

A. Jupiter Swap V2 Integration

// Quote
const order = await fetch(`https://api.jup.ag/swap/v2/order?${params}`, {
  headers: { "x-api-key": API_KEY }
}).then(r => r.json());

// Sign (frontend)
const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, 'base64'));
const signed = await wallet.signTransaction(tx);

// Execute
const result = await fetch("https://api.jup.ag/swap/v2/execute", {
  method: "POST",
  headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    signedTransaction: Buffer.from(signed.serialize()).toString('base64'),
    requestId: order.requestId
  })
}).then(r => r.json());

B. Jupiter Trigger Order Flow

// 1. Authenticate
const { challenge } = await fetch("/trigger/v2/auth/challenge", {
  method: "POST",
  headers: { "x-api-key": API_KEY },
  body: JSON.stringify({ walletPubkey, type: "message" })
}).then(r => r.json());

const signature = await wallet.signMessage(new TextEncoder().encode(challenge));

const { token } = await fetch("/trigger/v2/auth/verify", {
  method: "POST", 
  headers: { "x-api-key": API_KEY },
  body: JSON.stringify({ type: "message", walletPubkey, signature: bs58.encode(signature) })
}).then(r => r.json());

// 2. Craft deposit
const deposit = await fetch("/trigger/v2/deposit/craft", {
  method: "POST",
  headers: { "x-api-key": API_KEY, "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ inputMint, outputMint, userAddress, amount })
}).then(r => r.json());

// 3. Sign deposit and create order
const depositTx = VersionedTransaction.deserialize(Buffer.from(deposit.transaction, 'base64'));
const signedDeposit = await wallet.signTransaction(depositTx);

const order = await fetch("/trigger/v2/orders/price", {
  method: "POST",
  headers: { "x-api-key": API_KEY, "Authorization": `Bearer ${token}` },
  body: JSON.stringify({
    orderType: "single",
    depositRequestId: deposit.requestId,
    depositSignedTx: Buffer.from(signedDeposit.serialize()).toString('base64'),
    userPubkey,
    inputMint,
    inputAmount,
    outputMint,
    triggerMint: outputMint,
    triggerCondition: "below",
    triggerPriceUsd: 145.00,
    expiresAt: Date.now() + 24 * 60 * 60 * 1000
  })
}).then(r => r.json());

C. Dune SIM Signal Detection

const [transactions, balances] = await Promise.all([
  fetch(`https://api.sim.dune.com/beta/svm/transactions/${wallet}?limit=12`, {
    headers: { "X-Sim-Api-Key": DUNE_KEY }
  }).then(r => r.json()),
  fetch(`https://api.sim.dune.com/beta/svm/balances/${wallet}?limit=100`, {
    headers: { "X-Sim-Api-Key": DUNE_KEY }
  }).then(r => r.json())
]);

// Build token price map
const tokenMap = new Map(balances.balances.map(b => [b.address, b]));

// Detect accumulation signals
for (const tx of transactions.transactions) {
  const pre = tx.raw_transaction.meta.preTokenBalances;
  const post = tx.raw_transaction.meta.postTokenBalances;
  
  for (const postBal of post) {
    const preBal = pre.find(p => p.mint === postBal.mint && p.owner === wallet);
    const delta = Number(postBal.uiTokenAmount.amount) - Number(preBal?.uiTokenAmount?.amount ?? 0);
    
    if (delta > 0) {
      // Accumulation detected
      const token = tokenMap.get(postBal.mint);
      signals.push({ token, delta, valueUsd: delta * (token?.price_usd ?? 0) });
    }
  }
}

Known Limitations & Resolutions

See KNOWN-LIMITATIONS.md for the friction we hit during integration and how each item was resolved before submission.

Report submitted for Frontier Hackathon — April 2026 Updated to include LP Agent and Torque MCP integration feedback Known Gaps moved to KNOWN-LIMITATIONS.md and annotated with resolutions on 2026-04-27

THIS REPORT IS THE CANONICAL ARTIFACT · MIRRORED AT /DX-REPORT.MD IN THE REPO