
Dex Execution
- 190 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
dex-execution is a Claude Code skill for Solana DEX swap execution via the Jupiter aggregator, covering quoting, transaction building, signing, and confirmation.
About
dex-execution is a Claude Code skill for executing Solana token swaps through the Jupiter v6 aggregator, which routes across Raydium, Orca, Meteora, and 20+ venues. It defines a seven-step pipeline covering quoting, user confirmation, transaction building, signing, submission, and confirmation. A developer uses it when building on-chain swap execution into a trading agent. It gives slippage and priority-fee ranges by token type and requires explicit user confirmation before any swap.
- Seven-step Jupiter v6 swap pipeline: quote, display, confirm, build, sign, submit, confirm
- Slippage and priority-fee guidance per token type, from majors to new launches
- Mandates explicit user confirmation before submitting any swap
Dex Execution by the numbers
- 190 all-time installs (skills.sh)
- Ranked #486 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
dex-execution capabilities & compatibility
Needs a funded Solana wallet and RPC URL; Jupiter quote/swap endpoints are free but on-chain gas and priority fees apply.
- Capabilities
- dex execution · swap quoting · transaction signing · slippage control · trade confirmation
- Use cases
- trading · orchestration
- Runs
- Runs locally
- Pricing
- Bring your own API key
What dex-execution says it does
Execute token swaps on Solana through Jupiter, the dominant DEX aggregator routing across Raydium, Orca, Meteora, Phoenix, Lifinity, and 20+ other venues.
**NEVER proceed without explicit "yes" from the user.**
Every swap follows this seven-step pipeline. Never skip steps 2-3 (display and confirm).
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill dex-executionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 190 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Execute Solana token swaps via the Jupiter aggregator with quoting, signing, and confirmation.
Who is it for?
Executing Solana swaps through Jupiter with proper slippage, priority fees, and confirmation flow.
Skip if: Skipping the display-and-confirm steps; the skill mandates explicit user confirmation before any swap.
When should I use this skill?
You need to quote and execute a Solana token swap through Jupiter inside an agent.
What you get
A quoted, user-confirmed swap signed and submitted through Jupiter with confirmation polling.
By the numbers
- Seven-step swap pipeline
- Jupiter routes across 20+ DEX venues
- Slippage ranges 0.5% to 30% by token type
Files
DEX Execution — Solana Swap Execution via Jupiter
Execute token swaps on Solana through Jupiter, the dominant DEX aggregator routing across Raydium, Orca, Meteora, Phoenix, Lifinity, and 20+ other venues.
Overview
Jupiter aggregates liquidity across all major Solana DEXes to find optimal swap routes. A single swap may split across multiple pools and hop through intermediate tokens to minimize price impact. The Jupiter v6 API handles route discovery, transaction building, and fee optimization — your code handles quoting, user confirmation, signing, and submission.
Base URL: https://quote-api.jup.ag/v6
Execution Pipeline
Every swap follows this seven-step pipeline. Never skip steps 2-3 (display and confirm).
Step 1 — Get Quote
import httpx
params = {
"inputMint": "So11111111111111111111111111111111111111112", # SOL
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", # USDC
"amount": 1_000_000_000, # 1 SOL in lamports
"slippageBps": 50, # 0.5%
}
resp = httpx.get("https://quote-api.jup.ag/v6/quote", params=params)
quote = resp.json()Step 2 — Display Quote to User
Always show these fields before proceeding:
| Field | Source |
|---|---|
| Input amount | quote["inAmount"] (in token decimals) |
| Output amount | quote["outAmount"] |
| Minimum received | quote["otherAmountThreshold"] |
| Price impact | quote["priceImpactPct"] |
| Route | quote["routePlan"] — DEXes used |
| Slippage | The slippageBps you requested |
Step 3 — Require User Confirmation
⚠️ SWAP PREVIEW
Selling: 1.000 SOL
Buying: ~142.50 USDC
Min recv: 141.79 USDC (0.5% slippage)
Impact: 0.01%
Route: Raydium V4 → USDC
Proceed? [y/N]NEVER proceed without explicit "yes" from the user.
Step 4 — Build Transaction
swap_body = {
"quoteResponse": quote,
"userPublicKey": "YourPubkeyBase58...",
"wrapAndUnwrapSol": True,
"dynamicComputeUnitLimit": True,
"prioritizationFeeLamports": "auto",
}
resp = httpx.post("https://quote-api.jup.ag/v6/swap", json=swap_body)
swap_data = resp.json()
swap_tx = swap_data["swapTransaction"] # base64-encoded transactionStep 5 — Sign Transaction
import base64
from solders.transaction import VersionedTransaction
from solders.keypair import Keypair
raw_tx = base64.b64decode(swap_tx)
tx = VersionedTransaction.from_bytes(raw_tx)
keypair = Keypair.from_base58_string(os.environ["WALLET_PRIVATE_KEY"])
tx.sign([keypair])Step 6 — Submit Transaction
rpc_url = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
signed_bytes = bytes(tx)
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "sendTransaction",
"params": [
base64.b64encode(signed_bytes).decode(),
{"encoding": "base64", "skipPreflight": False,
"maxRetries": 3, "preflightCommitment": "confirmed"}
],
}
resp = httpx.post(rpc_url, json=payload)
sig = resp.json()["result"]
print(f"Submitted: https://solscan.io/tx/{sig}")Step 7 — Confirm Transaction
import time
for attempt in range(30):
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getSignatureStatuses",
"params": [[sig], {"searchTransactionHistory": False}],
}
resp = httpx.post(rpc_url, json=payload)
status = resp.json()["result"]["value"][0]
if status and status.get("confirmationStatus") in ("confirmed", "finalized"):
print(f"Confirmed at slot {status['slot']}")
break
time.sleep(2)
else:
print("Transaction not confirmed within 60s — check explorer")Jupiter API v6 Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/quote | GET | Get best-price quote with routing |
/swap | POST | Build a swap transaction from a quote |
/swap-instructions | POST | Get individual instructions (advanced) |
/price?ids=token1,token2 | GET | Simple price lookup (v2) |
/tokens | GET | List all supported tokens |
See references/jupiter_api.md for full parameter and response documentation.
Key Parameters
Slippage (slippageBps)
| Token Type | Recommended Range | Notes |
|---|---|---|
| SOL, USDC, major tokens | 50-100 (0.5-1%) | Stable liquidity |
| Mid-cap tokens | 100-300 (1-3%) | Variable liquidity |
| PumpFun / meme tokens | 500-2000 (5-20%) | Thin books, high volatility |
| New launches (<1h old) | 1000-3000 (10-30%) | Extreme volatility |
Dynamic Slippage
Set dynamicSlippage: true in the swap request to let Jupiter auto-adjust slippage based on current market conditions. Preferred for most use cases.
Priority Fees (prioritizationFeeLamports)
Priority fees determine transaction ordering within a block.
| Level | microLamports | When to Use |
|---|---|---|
| Low | 10,000-50,000 | Normal conditions |
| Medium | 50,000-200,000 | Moderate congestion |
| High | 200,000-1,000,000 | High congestion / time-sensitive |
| Urgent | 1,000,000-5,000,000 | Meme coin launches, NFT mints |
| Auto | "auto" | Jupiter estimates for you |
Use "auto" for most cases. For fine-grained control, query Helius getPriorityFeeEstimate.
Other Parameters
- `onlyDirectRoutes`: Skip multi-hop routing. Faster but may get worse price.
- `asLegacyTransaction`: Use legacy format instead of versioned transactions. Required for some older wallets.
- `maxAccounts`: Limit accounts in transaction (default 64). Lower values reduce route options but improve confirmation reliability.
- `platformFeeBps`: Integrator fee in basis points. Taken from output amount.
- `wrapAndUnwrapSol`: Auto wrap/unwrap SOL ↔ wSOL (default true).
- `dynamicComputeUnitLimit`: Auto-set compute budget based on simulation.
Priority Fee Estimation
# Using Helius getPriorityFeeEstimate
helius_url = f"https://mainnet.helius-rpc.com/?api-key={HELIUS_KEY}"
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getPriorityFeeEstimate",
"params": [{"accountKeys": [input_mint, output_mint],
"options": {"recommended": True}}],
}
resp = httpx.post(helius_url, json=payload)
fee = resp.json()["result"]["priorityFeeEstimate"]Transaction Confirmation Strategy
See references/transaction_lifecycle.md for the full Solana transaction lifecycle.
1. Submit with skipPreflight: False to catch obvious errors 2. Poll getSignatureStatuses every 2 seconds for up to 60 seconds 3. If not confirmed: rebuild transaction with fresh blockhash and retry (max 3 attempts) 4. Blockhash expiry: transactions expire ~60 seconds after blockhash was fetched 5. Final check: verify token balance changed as expected
Error Handling
| Error | Cause | Recovery |
|---|---|---|
"Slippage tolerance exceeded" | Price moved beyond slippageBps | Increase slippage or retry |
"Insufficient funds" | Not enough input token or SOL for fees | Check balance before quoting |
"Transaction expired" | Blockhash too old | Rebuild with fresh blockhash |
"Transaction simulation failed" | Various — check logs | Parse simulation logs for root cause |
"Too many accounts" | Route uses too many accounts | Set maxAccounts lower or use onlyDirectRoutes |
| HTTP 429 | Rate limited | Back off and retry with exponential delay |
HTTP 400 "No route found" | No liquidity path exists | Check token mints are correct, try larger slippage |
Safety Requirements
These are non-negotiable requirements for any execution code.
1. ALWAYS show quote details (amounts, price impact, route) before execution 2. ALWAYS require explicit user confirmation — never auto-execute 3. Default to simulation mode — do not sign or submit unless explicitly enabled 4. Never store or log private keys — load from env vars, use immediately, discard 5. Never use 100% slippage — this is a common scam/exploit vector 6. Verify token addresses — confirm mints match expected tokens before swapping 7. Check price impact — warn if >2%, block if >10% unless user overrides 8. Maintain SOL reserve — keep 0.05 SOL minimum for rent and future fees
See references/safety_checklist.md for the complete pre/during/post execution checklist.
Integration with Other Skills
| Skill | Integration |
|---|---|
slippage-modeling | Estimate optimal slippageBps based on token liquidity profile |
liquidity-analysis | Verify pool depth supports trade size before quoting |
position-sizing | Calculate trade amount based on risk parameters |
risk-management | Enforce portfolio-level exposure limits before execution |
jupiter-api | Underlying API documentation for Jupiter endpoints |
helius-api | Priority fee estimation and transaction monitoring |
Files
References
references/jupiter_api.md— Complete Jupiter v6 API parameter and response referencereferences/transaction_lifecycle.md— Solana transaction lifecycle, priority fees, retry strategiesreferences/safety_checklist.md— Pre/during/post execution verification checklist
Scripts
scripts/get_quote.py— Fetch and display Jupiter swap quotes with route analysisscripts/simulate_swap.py— Build and simulate swap transactions without submitting
Jupiter v6 API Reference
Base URL: https://quote-api.jup.ag/v6
No authentication required. Rate limits apply per IP.
Rate Limits
| Endpoint | Limit |
|---|---|
| GET /quote | 600 requests/minute |
| POST /swap | 300 requests/minute |
| POST /swap-instructions | 300 requests/minute |
| GET /price | 600 requests/minute |
| GET /tokens | 60 requests/minute |
GET /quote
Get the best-price quote for a token swap.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
inputMint | string | Yes | — | Input token mint address |
outputMint | string | Yes | — | Output token mint address |
amount | integer | Yes | — | Input amount in smallest unit (lamports for SOL) |
slippageBps | integer | No | 50 | Maximum slippage in basis points |
platformFeeBps | integer | No | 0 | Integrator fee in basis points |
onlyDirectRoutes | boolean | No | false | Skip multi-hop routes |
asLegacyTransaction | boolean | No | false | Return legacy transaction format |
maxAccounts | integer | No | 64 | Maximum accounts in transaction |
excludeDexes | string | No | — | Comma-separated DEX names to exclude |
restrictIntermediateTokens | boolean | No | false | Only use high-liquidity intermediate tokens |
Response
{
"inputMint": "So11111111111111111111111111111111111111112",
"inAmount": "1000000000",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"outAmount": "14250000",
"otherAmountThreshold": "14178750",
"swapMode": "ExactIn",
"slippageBps": 50,
"priceImpactPct": "0.01",
"routePlan": [
{
"swapInfo": {
"ammKey": "...",
"label": "Raydium",
"inputMint": "So111...",
"outputMint": "EPjFW...",
"inAmount": "1000000000",
"outAmount": "14250000",
"feeAmount": "25000",
"feeMint": "So111..."
},
"percent": 100
}
],
"contextSlot": 250000000,
"timeTaken": 0.05
}Key Response Fields
- `outAmount`: Expected output in smallest units
- `otherAmountThreshold`: Minimum output after slippage (for ExactIn mode)
- `priceImpactPct`: Price impact as a percentage string
- `routePlan`: Array of swap steps with DEX labels and amounts
- `timeTaken`: Quote computation time in seconds
Example
curl "https://quote-api.jup.ag/v6/quote?\
inputMint=So11111111111111111111111111111111111111112&\
outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&\
amount=1000000000&\
slippageBps=50"POST /swap
Build a swap transaction from a quote response.
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
quoteResponse | object | Yes | — | Full quote response from GET /quote |
userPublicKey | string | Yes | — | Signer's public key (base58) |
wrapAndUnwrapSol | boolean | No | true | Auto wrap/unwrap SOL |
useSharedAccounts | boolean | No | true | Use shared intermediate token accounts |
dynamicComputeUnitLimit | boolean | No | false | Auto-set compute unit limit from simulation |
skipUserAccountsRpcCalls | boolean | No | false | Skip RPC calls for user account checks |
prioritizationFeeLamports | int/string | No | 0 | Priority fee; use "auto" for Jupiter estimate |
dynamicSlippage | boolean | No | false | Auto-adjust slippage based on conditions |
computeUnitPriceMicroLamports | integer | No | — | Explicit compute unit price (overrides prioritizationFeeLamports) |
Response
{
"swapTransaction": "AQAAAA...base64...",
"lastValidBlockHeight": 250000100,
"prioritizationFeeLamports": 50000,
"computeUnitLimit": 200000,
"dynamicSlippageReport": {
"slippageBps": 75,
"otherAmount": 14143125,
"simulatedIncurredSlippageBps": 12
}
}- `swapTransaction`: Base64-encoded versioned transaction (or legacy if requested)
- `lastValidBlockHeight`: Transaction expires after this block height
- `dynamicSlippageReport`: Present when
dynamicSlippage: true; shows actual slippage used
Example
curl -X POST "https://quote-api.jup.ag/v6/swap" \
-H "Content-Type: application/json" \
-d '{
"quoteResponse": { ... },
"userPublicKey": "YourPubkeyHere",
"wrapAndUnwrapSol": true,
"dynamicComputeUnitLimit": true,
"prioritizationFeeLamports": "auto"
}'POST /swap-instructions
Returns individual instructions instead of a serialized transaction. Use this for advanced cases where you need to add custom instructions.
Request Body
Same as POST /swap.
Response
{
"tokenLedgerInstruction": null,
"computeBudgetInstructions": [...],
"setupInstructions": [...],
"swapInstruction": { ... },
"cleanupInstruction": { ... },
"addressLookupTableAddresses": [...]
}Each instruction contains programId, accounts (array of {pubkey, isSigner, isWritable}), and data (base58).
GET /price
Simple price lookup for one or more tokens (Jupiter Price API v2).
URL: https://price.jup.ag/v2/price
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
ids | string | Yes | Comma-separated token mint addresses |
vsToken | string | No | Quote token (default: USDC) |
Response
{
"data": {
"So11111111111111111111111111111111111111112": {
"id": "So11111111111111111111111111111111111111112",
"mintSymbol": "SOL",
"vsToken": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"vsTokenSymbol": "USDC",
"price": 142.50
}
},
"timeTaken": 0.002
}GET /tokens
Returns all tokens Jupiter supports for swapping.
Response
Array of token objects:
[
{
"address": "So11111111111111111111111111111111111111112",
"chainId": 101,
"decimals": 9,
"name": "Wrapped SOL",
"symbol": "SOL",
"logoURI": "https://...",
"tags": ["old-registry"],
"extensions": {}
}
]Common Token Mints
| Token | Mint Address |
|---|---|
| SOL (wrapped) | So11111111111111111111111111111111111111112 |
| USDC | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| USDT | Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB |
| BONK | DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263 |
| JUP | JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN |
| RAY | 4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R |
| ORCA | orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE |
Error Responses
| Status | Body | Meaning |
|---|---|---|
| 400 | {"error": "No route found"} | No swap path exists for this pair/amount |
| 400 | {"error": "Amount too small"} | Input amount below minimum |
| 429 | — | Rate limit exceeded |
| 500 | — | Internal server error; retry |
DEX Execution Safety Checklist
Pre-Execution Checks
Run all checks before requesting user confirmation. Block execution if any critical check fails.
1. Token Validation (Critical)
def validate_token(mint: str, expected_symbol: str) -> bool:
"""Verify mint address matches expected token."""
resp = httpx.get(f"https://quote-api.jup.ag/v6/tokens")
tokens = {t["address"]: t for t in resp.json()}
if mint not in tokens:
print(f"WARNING: {mint} not in Jupiter token list")
return False
actual = tokens[mint]["symbol"]
if actual != expected_symbol:
print(f"MISMATCH: expected {expected_symbol}, got {actual}")
return False
return True- Verify mint address is correct (copy-paste errors are common)
- Cross-reference symbol with known token list
- Watch for fake tokens that mimic real token names
2. Liquidity Check (Critical)
- Query pool depth before quoting
- Minimum liquidity threshold: trade size should be < 2% of total pool TVL
- For tokens with < $10K liquidity, warn the user explicitly
- See the
liquidity-analysisskill for detailed pool assessment
3. Price Impact Assessment (Critical)
| Impact | Action |
|---|---|
| < 1% | Proceed normally |
| 1-2% | Show warning, proceed if user confirms |
| 2-5% | Strong warning — suggest smaller trade or limit order |
| 5-10% | Block by default — require explicit override |
| > 10% | Block — almost certainly a mistake or scam token |
impact = float(quote["priceImpactPct"])
if impact > 10.0:
print("BLOCKED: Price impact exceeds 10%. This is likely an error.")
return False
elif impact > 5.0:
print(f"WARNING: {impact:.2f}% price impact. Requires explicit override.")
return confirm_with_user("Proceed despite high impact?")
elif impact > 2.0:
print(f"CAUTION: {impact:.2f}% price impact. Consider reducing trade size.")4. Slippage Validation (Critical)
- Never accept slippage >= 50% (5000 bps) — almost always a mistake or exploit
- Warn if slippage > 10% (1000 bps) for non-meme tokens
- For meme tokens, cap at 30% (3000 bps) with explicit user acknowledgment
MAX_SLIPPAGE_BPS = 5000 # absolute max, never exceed
def validate_slippage(slippage_bps: int, is_meme: bool = False) -> bool:
if slippage_bps >= MAX_SLIPPAGE_BPS:
print("BLOCKED: Slippage >= 50% is never acceptable")
return False
if not is_meme and slippage_bps > 1000:
print(f"WARNING: {slippage_bps/100:.1f}% slippage is high for this token")
return True5. Balance Verification (Critical)
def check_balance(rpc_url: str, pubkey: str, input_mint: str,
amount: int, is_sol: bool) -> bool:
"""Verify sufficient balance for trade + fees."""
SOL_RESERVE = 50_000_000 # 0.05 SOL for rent + fees
if is_sol:
# Need amount + reserve for fees
sol_balance = get_sol_balance(rpc_url, pubkey)
required = amount + SOL_RESERVE
if sol_balance < required:
print(f"Insufficient SOL: have {sol_balance}, need {required}")
return False
else:
# Need token amount + SOL for fees
token_balance = get_token_balance(rpc_url, pubkey, input_mint)
sol_balance = get_sol_balance(rpc_url, pubkey)
if token_balance < amount:
print(f"Insufficient token balance: have {token_balance}, need {amount}")
return False
if sol_balance < SOL_RESERVE:
print(f"Insufficient SOL for fees: have {sol_balance}, need {SOL_RESERVE}")
return False
return True6. Quote Freshness
- Quotes are valid for approximately 30 seconds
- If more than 30 seconds have elapsed since the quote, fetch a new one
- In volatile markets, re-quote even more frequently (10-15 seconds)
7. User Confirmation (Critical)
Display all of the following before asking for confirmation:
╔══════════════════════════════════════╗
║ SWAP CONFIRMATION ║
╠══════════════════════════════════════╣
║ Sell: 1.000 SOL ║
║ Buy: ~142.50 USDC ║
║ Min recv: 141.79 USDC ║
║ Impact: 0.01% ║
║ Slippage: 0.5% ║
║ Route: SOL → Raydium → USDC ║
║ Fee: ~0.00005 SOL ║
╠══════════════════════════════════════╣
║ Proceed? Type YES to confirm ║
╚══════════════════════════════════════╝During Execution
8. Simulate Before Sending
- Always call
simulateTransactionbeforesendTransaction - Check
result.value.err— if not null, do not proceed - Parse simulation logs for warnings
- Note compute units consumed for fee estimation
9. Monitor Confirmation
- Poll
getSignatureStatusesevery 2 seconds - Set a timeout of 60 seconds maximum
- If timeout: do NOT assume failure — the transaction may still land
- Check the explorer before retrying to avoid double-execution
10. Handle Timeout Gracefully
def handle_timeout(rpc_url: str, signature: str) -> str:
"""Determine transaction fate after timeout."""
# Check one more time with history search
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getSignatureStatuses",
"params": [[signature], {"searchTransactionHistory": True}],
}
resp = httpx.post(rpc_url, json=payload)
status = resp.json()["result"]["value"][0]
if status is None:
return "dropped" # Transaction was never processed — safe to retry
elif status.get("err"):
return "failed" # Transaction failed on-chain — safe to retry
else:
return "pending" # Still processing — DO NOT retry yetPost-Execution
11. Verify Balance Change
After confirmation, verify the expected token was received:
def verify_execution(rpc_url: str, pubkey: str, output_mint: str,
pre_balance: int, expected_min: int) -> dict:
"""Verify swap executed correctly."""
post_balance = get_token_balance(rpc_url, pubkey, output_mint)
received = post_balance - pre_balance
return {
"received": received,
"expected_min": expected_min,
"within_slippage": received >= expected_min,
"execution_quality": received / expected_min if expected_min > 0 else 0,
}12. Log Transaction
Record for analysis (never log private keys):
execution_log = {
"timestamp": datetime.utcnow().isoformat(),
"signature": sig,
"input_mint": input_mint,
"output_mint": output_mint,
"input_amount": in_amount,
"quoted_output": quoted_output,
"actual_output": actual_output,
"price_impact_pct": price_impact,
"slippage_bps": slippage_bps,
"priority_fee_lamports": priority_fee,
"route": route_labels,
}13. Calculate Execution Quality
quoted_price = float(quoted_output) / float(in_amount)
actual_price = float(actual_output) / float(in_amount)
execution_cost_bps = (1 - actual_price / quoted_price) * 10000
print(f"Execution cost: {execution_cost_bps:.1f} bps vs quoted price")What NOT to Do
1. Never auto-execute — always require explicit user confirmation 2. Never hardcode private keys — environment variables only 3. Never log private keys — not to console, files, or remote services 4. Never ignore simulation errors — if simulation fails, do not send 5. Never set slippage to 100% — this is a common attack vector 6. Never retry without checking — verify the first tx didn't land before retrying 7. Never skip balance checks — insufficient balance errors waste fees 8. Never trust token symbols alone — always verify by mint address 9. Never execute on behalf of user without showing full trade details first 10. Never assume a timeout means failure — always check transaction status
Solana Transaction Lifecycle
Overview
Every Solana swap follows a strict lifecycle: build, simulate, sign, send, confirm. Understanding each step prevents lost funds and failed transactions.
1. Build Transaction
Jupiter's POST /swap returns a base64-encoded versioned transaction. To work with it:
import base64
from solders.transaction import VersionedTransaction
raw_tx = base64.b64decode(swap_response["swapTransaction"])
tx = VersionedTransaction.from_bytes(raw_tx)Versioned vs Legacy Transactions:
- Versioned transactions (v0) support address lookup tables, reducing account size
- Legacy transactions have a 35-account hard limit
- Use
asLegacyTransaction: trueonly if the signing wallet requires it - Jupiter defaults to versioned transactions
2. Simulate Transaction
Always simulate before signing to catch errors without spending fees.
import base64, httpx
rpc_url = "https://api.mainnet-beta.solana.com"
raw_bytes = bytes(tx) # unsigned transaction bytes
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "simulateTransaction",
"params": [
base64.b64encode(raw_bytes).decode(),
{
"encoding": "base64",
"commitment": "confirmed",
"sigVerify": False,
"replaceRecentBlockhash": True,
}
],
}
resp = httpx.post(rpc_url, json=payload)
result = resp.json()["result"]["value"]
if result["err"]:
print(f"Simulation failed: {result['err']}")
print(f"Logs: {result['logs']}")
else:
print(f"Simulation OK — {result['unitsConsumed']} compute units")Key simulation parameters:
- `sigVerify: False`: Skip signature verification (transaction isn't signed yet)
- `replaceRecentBlockhash: True`: Use a fresh blockhash so simulation doesn't fail on expiry
3. Sign Transaction
from solders.keypair import Keypair
keypair = Keypair.from_base58_string(private_key)
tx.sign([keypair])Security rules:
- Load private key from environment variable only
- Never log, print, or persist the private key
- Never sign without user confirmation
- Clear the keypair from memory after signing (Python GC handles this)
4. Send Transaction
signed_bytes = bytes(tx)
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "sendTransaction",
"params": [
base64.b64encode(signed_bytes).decode(),
{
"encoding": "base64",
"skipPreflight": False,
"preflightCommitment": "confirmed",
"maxRetries": 3,
}
],
}
resp = httpx.post(rpc_url, json=payload)
sig = resp.json()["result"]`skipPreflight` options:
False(default): RPC simulates before forwarding. Catches errors early.True: Skip simulation, send directly. Use only when you already simulated and need speed.
5. Confirm Transaction
import time
def confirm_transaction(rpc_url: str, signature: str,
timeout: int = 60, interval: int = 2) -> dict:
"""Poll for transaction confirmation."""
start = time.time()
while time.time() - start < timeout:
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getSignatureStatuses",
"params": [[signature], {"searchTransactionHistory": False}],
}
resp = httpx.post(rpc_url, json=payload)
statuses = resp.json()["result"]["value"]
if statuses[0]:
status = statuses[0]
if status.get("err"):
return {"confirmed": False, "error": status["err"]}
if status["confirmationStatus"] in ("confirmed", "finalized"):
return {"confirmed": True, "slot": status["slot"],
"status": status["confirmationStatus"]}
time.sleep(interval)
return {"confirmed": False, "error": "timeout"}Commitment levels:
- `processed`: Seen by the leader, not yet voted on (~400ms)
- `confirmed`: Voted on by supermajority (~5-10s) — sufficient for most swaps
- `finalized`: Rooted, irreversible (~15-30s) — use for high-value transactions
Priority Fees
Solana uses a fee market based on compute unit price. Higher fees = higher priority in block scheduling.
How Priority Fees Work
Total fee = base_fee (5000 lamports) + compute_units * compute_unit_price
# compute_unit_price is in microLamports (1 lamport = 1_000_000 microLamports)
# Example: 100,000 microLamports * 200,000 CU = 20,000 lamports = 0.00002 SOLEstimating Priority Fees
Option A — Jupiter auto: Set prioritizationFeeLamports: "auto" in the /swap request. Jupiter estimates based on recent blocks.
Option B — Helius API:
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getPriorityFeeEstimate",
"params": [{
"accountKeys": [input_mint, output_mint],
"options": {"recommended": True}
}],
}
resp = httpx.post(helius_rpc_url, json=payload)
estimate = resp.json()["result"]
# Returns: {"priorityFeeEstimate": 50000}Option C — Fixed tiers:
| Tier | microLamports | Typical Cost (200K CU) |
|---|---|---|
| Economy | 10,000 | 0.000002 SOL |
| Standard | 50,000 | 0.00001 SOL |
| Fast | 200,000 | 0.00004 SOL |
| Turbo | 1,000,000 | 0.0002 SOL |
| Emergency | 5,000,000 | 0.001 SOL |
When to Increase Priority Fees
- Token launches (first minutes): Turbo/Emergency
- High market volatility: Fast/Turbo
- Normal trading: Standard
- Non-urgent rebalancing: Economy
Blockhash Expiry and Retries
Transactions include a recentBlockhash that expires after ~60 seconds (~150 slots).
Retry Strategy
1. Get quote → Build tx → Simulate → Sign → Send
2. Poll for 30 seconds
3. If not confirmed:
a. Get fresh quote (prices may have changed)
b. Build new tx with new blockhash
c. Simulate → Sign → Send
4. Repeat up to 3 total attempts
5. If still not confirmed, abort and alert userNever resubmit a signed transaction with an expired blockhash — it will be rejected.
Common Failure Modes
| Failure | Cause | Solution |
|---|---|---|
BlockhashNotFound | Blockhash expired | Rebuild transaction with fresh blockhash |
InsufficientFundsForRent | Account needs rent-exempt minimum | Ensure 0.05 SOL reserve |
SlippageToleranceExceeded | Price moved beyond limit | Increase slippage or retry quickly |
AccountNotFound | Token account doesn't exist | Enable wrapAndUnwrapSol, check ATAs |
ProgramError | AMM-specific error | Check simulation logs for details |
| Transaction dropped | Leader didn't include it | Resubmit with higher priority fee |
Verifying Execution
After confirmation, verify the swap executed correctly:
# Check token balance changed
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getTokenAccountsByOwner",
"params": [
user_pubkey,
{"mint": output_mint},
{"encoding": "jsonParsed"}
],
}Compare post-swap balance with pre-swap balance to calculate actual execution price.
#!/usr/bin/env python3
"""Fetch and display Jupiter swap quotes with route analysis.
Queries the Jupiter v6 API for the best swap route between two tokens,
displays a detailed breakdown of the quote including amounts, price impact,
route path, and compares with direct price to show effective cost.
Usage:
python scripts/get_quote.py
python scripts/get_quote.py --demo
INPUT_MINT=So111... OUTPUT_MINT=EPjF... AMOUNT_LAMPORTS=1000000000 python scripts/get_quote.py
Dependencies:
uv pip install httpx
Environment Variables:
INPUT_MINT: Input token mint address (default: SOL)
OUTPUT_MINT: Output token mint address (default: USDC)
AMOUNT_LAMPORTS: Input amount in smallest units (default: 1000000000 = 1 SOL)
SLIPPAGE_BPS: Maximum slippage in basis points (default: 50)
HELIUS_API_KEY: Optional — for priority fee estimation
"""
import os
import sys
import time
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
JUPITER_BASE_URL = "https://quote-api.jup.ag/v6"
JUPITER_PRICE_URL = "https://price.jup.ag/v2/price"
# Well-known token mints
SOL_MINT = "So11111111111111111111111111111111111111112"
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
USDT_MINT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
# Token decimals for display
KNOWN_DECIMALS: dict[str, int] = {
SOL_MINT: 9,
USDC_MINT: 6,
USDT_MINT: 6,
}
INPUT_MINT = os.getenv("INPUT_MINT", SOL_MINT)
OUTPUT_MINT = os.getenv("OUTPUT_MINT", USDC_MINT)
AMOUNT_LAMPORTS = int(os.getenv("AMOUNT_LAMPORTS", "1000000000")) # 1 SOL
SLIPPAGE_BPS = int(os.getenv("SLIPPAGE_BPS", "50"))
HELIUS_API_KEY = os.getenv("HELIUS_API_KEY", "")
# ── Token Metadata ──────────────────────────────────────────────────
def get_token_info(client: httpx.Client, mint: str) -> dict:
"""Look up token symbol and decimals from Jupiter token list.
Args:
client: HTTP client instance.
mint: Token mint address.
Returns:
Dict with 'symbol' and 'decimals' keys.
"""
if not hasattr(get_token_info, "_cache"):
get_token_info._cache = {}
if mint in get_token_info._cache:
return get_token_info._cache[mint]
# Use known decimals if available
if mint in KNOWN_DECIMALS:
info = {"symbol": _mint_to_symbol(mint), "decimals": KNOWN_DECIMALS[mint]}
get_token_info._cache[mint] = info
return info
# Fetch from Jupiter token list
try:
resp = client.get(f"{JUPITER_BASE_URL}/tokens", timeout=10.0)
resp.raise_for_status()
for token in resp.json():
if token["address"] == mint:
info = {"symbol": token.get("symbol", "UNKNOWN"),
"decimals": token.get("decimals", 9)}
get_token_info._cache[mint] = info
return info
except httpx.HTTPError:
pass
info = {"symbol": mint[:8] + "...", "decimals": 9}
get_token_info._cache[mint] = info
return info
def _mint_to_symbol(mint: str) -> str:
"""Map well-known mints to symbols."""
symbols = {SOL_MINT: "SOL", USDC_MINT: "USDC", USDT_MINT: "USDT"}
return symbols.get(mint, mint[:8] + "...")
# ── Jupiter Quote ───────────────────────────────────────────────────
def fetch_quote(
client: httpx.Client,
input_mint: str,
output_mint: str,
amount: int,
slippage_bps: int = 50,
only_direct_routes: bool = False,
) -> Optional[dict]:
"""Fetch a swap quote from Jupiter v6 API.
Args:
client: HTTP client instance.
input_mint: Input token mint address.
output_mint: Output token mint address.
amount: Input amount in smallest units (e.g., lamports for SOL).
slippage_bps: Maximum slippage in basis points.
only_direct_routes: If True, skip multi-hop routes.
Returns:
Quote response dict, or None on failure.
"""
params = {
"inputMint": input_mint,
"outputMint": output_mint,
"amount": amount,
"slippageBps": slippage_bps,
}
if only_direct_routes:
params["onlyDirectRoutes"] = "true"
try:
resp = client.get(
f"{JUPITER_BASE_URL}/quote",
params=params,
timeout=15.0,
)
resp.raise_for_status()
return resp.json()
except httpx.HTTPStatusError as e:
print(f"Quote API error {e.response.status_code}: {e.response.text}")
return None
except httpx.HTTPError as e:
print(f"HTTP error fetching quote: {e}")
return None
# ── Jupiter Price ───────────────────────────────────────────────────
def fetch_direct_price(
client: httpx.Client,
input_mint: str,
output_mint: str,
) -> Optional[float]:
"""Fetch direct price from Jupiter Price API v2.
Args:
client: HTTP client instance.
input_mint: Input token mint.
output_mint: Output (quote) token mint.
Returns:
Price as float, or None on failure.
"""
try:
resp = client.get(
JUPITER_PRICE_URL,
params={"ids": input_mint, "vsToken": output_mint},
timeout=10.0,
)
resp.raise_for_status()
data = resp.json().get("data", {})
token_data = data.get(input_mint)
if token_data:
return float(token_data["price"])
except (httpx.HTTPError, KeyError, ValueError) as e:
print(f"Price lookup failed: {e}")
return None
# ── Priority Fee Estimation ─────────────────────────────────────────
def estimate_priority_fee(
client: httpx.Client,
input_mint: str,
output_mint: str,
) -> Optional[int]:
"""Estimate priority fee using Helius API.
Args:
client: HTTP client instance.
input_mint: Input token mint.
output_mint: Output token mint.
Returns:
Recommended priority fee in microLamports, or None if unavailable.
"""
if not HELIUS_API_KEY:
return None
helius_url = f"https://mainnet.helius-rpc.com/?api-key={HELIUS_API_KEY}"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getPriorityFeeEstimate",
"params": [{
"accountKeys": [input_mint, output_mint],
"options": {"recommended": True},
}],
}
try:
resp = client.post(helius_url, json=payload, timeout=10.0)
resp.raise_for_status()
result = resp.json().get("result", {})
return int(result.get("priorityFeeEstimate", 0))
except (httpx.HTTPError, KeyError, ValueError):
return None
# ── Display ─────────────────────────────────────────────────────────
def format_amount(raw_amount: str, decimals: int) -> str:
"""Format a raw token amount with proper decimal places.
Args:
raw_amount: Amount as string in smallest units.
decimals: Token decimal places.
Returns:
Formatted amount string.
"""
value = int(raw_amount) / (10 ** decimals)
if decimals <= 2:
return f"{value:,.2f}"
elif value >= 1.0:
return f"{value:,.4f}"
else:
return f"{value:,.{decimals}f}"
def display_quote(
quote: dict,
input_info: dict,
output_info: dict,
direct_price: Optional[float],
priority_fee: Optional[int],
) -> None:
"""Display a formatted quote summary.
Args:
quote: Jupiter quote response.
input_info: Input token info (symbol, decimals).
output_info: Output token info (symbol, decimals).
direct_price: Direct market price for comparison, or None.
priority_fee: Estimated priority fee in microLamports, or None.
"""
in_amount = format_amount(quote["inAmount"], input_info["decimals"])
out_amount = format_amount(quote["outAmount"], output_info["decimals"])
min_received = format_amount(quote["otherAmountThreshold"], output_info["decimals"])
price_impact = float(quote.get("priceImpactPct", "0"))
slippage = quote.get("slippageBps", 0)
# Calculate effective price
in_value = int(quote["inAmount"]) / (10 ** input_info["decimals"])
out_value = int(quote["outAmount"]) / (10 ** output_info["decimals"])
effective_price = out_value / in_value if in_value > 0 else 0
# Route description
route_parts = []
for step in quote.get("routePlan", []):
swap = step.get("swapInfo", {})
label = swap.get("label", "Unknown")
pct = step.get("percent", 100)
if pct < 100:
route_parts.append(f"{label} ({pct}%)")
else:
route_parts.append(label)
route_str = " → ".join(route_parts) if route_parts else "Unknown"
# Print formatted output
print()
print("=" * 60)
print(" JUPITER SWAP QUOTE")
print("=" * 60)
print()
print(f" Sell: {in_amount} {input_info['symbol']}")
print(f" Buy: ~{out_amount} {output_info['symbol']}")
print(f" Min received: {min_received} {output_info['symbol']}")
print(f" Slippage: {slippage / 100:.2f}%")
print(f" Price impact: {price_impact:.4f}%")
print(f" Eff. price: 1 {input_info['symbol']} = {effective_price:,.6f} {output_info['symbol']}")
print(f" Route: {route_str}")
# Compare with direct price
if direct_price is not None and effective_price > 0:
price_diff_bps = abs(effective_price - direct_price) / direct_price * 10000
print()
print(f" Market price: 1 {input_info['symbol']} = {direct_price:,.6f} {output_info['symbol']}")
print(f" Diff vs market: {price_diff_bps:,.1f} bps")
# Priority fee estimate
if priority_fee is not None:
fee_sol = priority_fee * 200_000 / 1_000_000_000_000 # assume 200K CU
print()
print(f" Priority fee: {priority_fee:,} microLamports/CU (~{fee_sol:.6f} SOL)")
else:
print()
print(" Priority fee: Use 'auto' or set HELIUS_API_KEY for estimate")
# Warnings
if price_impact > 2.0:
print()
print(f" *** HIGH PRICE IMPACT: {price_impact:.2f}% ***")
print(" *** Consider reducing trade size ***")
elif price_impact > 5.0:
print()
print(f" *** EXTREME PRICE IMPACT: {price_impact:.2f}% ***")
print(" *** Trade execution NOT recommended ***")
print()
print("=" * 60)
print(" This is a quote only. No transaction has been executed.")
print("=" * 60)
print()
# ── Main ────────────────────────────────────────────────────────────
def run_demo(client: httpx.Client) -> None:
"""Run a demo quote for SOL -> USDC.
Args:
client: HTTP client instance.
"""
print("Running demo: 1 SOL → USDC quote")
print()
quote = fetch_quote(
client,
input_mint=SOL_MINT,
output_mint=USDC_MINT,
amount=1_000_000_000,
slippage_bps=50,
)
if not quote:
print("Failed to fetch demo quote.")
return
input_info = {"symbol": "SOL", "decimals": 9}
output_info = {"symbol": "USDC", "decimals": 6}
direct_price = fetch_direct_price(client, SOL_MINT, USDC_MINT)
priority_fee = estimate_priority_fee(client, SOL_MINT, USDC_MINT)
display_quote(quote, input_info, output_info, direct_price, priority_fee)
# Also show a direct-route-only comparison
print("Comparing with direct-route-only quote...")
direct_quote = fetch_quote(
client,
input_mint=SOL_MINT,
output_mint=USDC_MINT,
amount=1_000_000_000,
slippage_bps=50,
only_direct_routes=True,
)
if direct_quote:
direct_out = int(direct_quote["outAmount"]) / 1e6
multi_out = int(quote["outAmount"]) / 1e6
diff = multi_out - direct_out
print(f" Multi-hop output: {multi_out:,.4f} USDC")
print(f" Direct-only output: {direct_out:,.4f} USDC")
print(f" Multi-hop advantage: {diff:,.4f} USDC ({diff / multi_out * 100:.3f}%)")
print()
def main() -> None:
"""Main entry point."""
demo_mode = "--demo" in sys.argv
with httpx.Client() as client:
if demo_mode:
run_demo(client)
return
input_mint = INPUT_MINT
output_mint = OUTPUT_MINT
amount = AMOUNT_LAMPORTS
slippage_bps = SLIPPAGE_BPS
print(f"Fetching quote: {input_mint[:8]}... → {output_mint[:8]}...")
print(f"Amount: {amount} (smallest units), Slippage: {slippage_bps} bps")
print()
# Fetch token metadata
input_info = get_token_info(client, input_mint)
output_info = get_token_info(client, output_mint)
# Fetch quote
quote = fetch_quote(client, input_mint, output_mint, amount, slippage_bps)
if not quote:
print("Failed to fetch quote. Check token mints and try again.")
sys.exit(1)
# Fetch comparison price
direct_price = fetch_direct_price(client, input_mint, output_mint)
# Estimate priority fee
priority_fee = estimate_priority_fee(client, input_mint, output_mint)
# Display results
display_quote(quote, input_info, output_info, direct_price, priority_fee)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Build and simulate a Jupiter swap transaction without submitting.
Fetches a quote, builds the swap transaction via Jupiter v6 API, then
simulates it against the Solana RPC to verify it would succeed. Reports
compute units, logs, and any errors. NEVER signs or submits a real
transaction.
⚠️ SIMULATION ONLY — This script does NOT execute real swaps.
⚠️ No private keys are required or used.
Usage:
python scripts/simulate_swap.py
python scripts/simulate_swap.py --demo
Dependencies:
uv pip install httpx
Environment Variables:
INPUT_MINT: Input token mint address (default: SOL)
OUTPUT_MINT: Output token mint address (default: USDC)
AMOUNT_LAMPORTS: Input amount in smallest units (default: 1000000000 = 1 SOL)
SLIPPAGE_BPS: Maximum slippage in basis points (default: 50)
USER_PUBKEY: Your wallet public key (required for non-demo mode)
SOLANA_RPC_URL: Solana RPC endpoint (default: public mainnet)
"""
import base64
import os
import sys
import time
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Safety Banner ───────────────────────────────────────────────────
SAFETY_BANNER = """
╔══════════════════════════════════════════════════════════════╗
║ ⚠️ SIMULATION MODE — NO REAL TRANSACTIONS WILL EXECUTE ║
║ This script builds and simulates swap transactions only. ║
║ No private keys are required, loaded, or used. ║
╚══════════════════════════════════════════════════════════════╝
"""
# ── Configuration ───────────────────────────────────────────────────
JUPITER_BASE_URL = "https://quote-api.jup.ag/v6"
DEFAULT_RPC_URL = "https://api.mainnet-beta.solana.com"
SOL_MINT = "So11111111111111111111111111111111111111112"
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
# Demo public key (a known Solana system account — not a real user wallet)
DEMO_PUBKEY = "11111111111111111111111111111111"
KNOWN_DECIMALS: dict[str, int] = {
SOL_MINT: 9,
USDC_MINT: 6,
}
INPUT_MINT = os.getenv("INPUT_MINT", SOL_MINT)
OUTPUT_MINT = os.getenv("OUTPUT_MINT", USDC_MINT)
AMOUNT_LAMPORTS = int(os.getenv("AMOUNT_LAMPORTS", "1000000000"))
SLIPPAGE_BPS = int(os.getenv("SLIPPAGE_BPS", "50"))
USER_PUBKEY = os.getenv("USER_PUBKEY", "")
SOLANA_RPC_URL = os.getenv("SOLANA_RPC_URL", DEFAULT_RPC_URL)
# ── Quote ───────────────────────────────────────────────────────────
def fetch_quote(
client: httpx.Client,
input_mint: str,
output_mint: str,
amount: int,
slippage_bps: int = 50,
) -> Optional[dict]:
"""Fetch a swap quote from Jupiter v6 API.
Args:
client: HTTP client instance.
input_mint: Input token mint address.
output_mint: Output token mint address.
amount: Input amount in smallest units.
slippage_bps: Maximum slippage in basis points.
Returns:
Quote response dict, or None on failure.
"""
params = {
"inputMint": input_mint,
"outputMint": output_mint,
"amount": amount,
"slippageBps": slippage_bps,
}
try:
resp = client.get(
f"{JUPITER_BASE_URL}/quote",
params=params,
timeout=15.0,
)
resp.raise_for_status()
return resp.json()
except httpx.HTTPStatusError as e:
print(f"Quote API error {e.response.status_code}: {e.response.text}")
return None
except httpx.HTTPError as e:
print(f"HTTP error fetching quote: {e}")
return None
# ── Build Swap Transaction ──────────────────────────────────────────
def build_swap_transaction(
client: httpx.Client,
quote: dict,
user_pubkey: str,
priority_fee: str = "auto",
) -> Optional[dict]:
"""Build a swap transaction from a Jupiter quote.
Args:
client: HTTP client instance.
quote: Quote response from Jupiter.
user_pubkey: User's wallet public key (base58).
priority_fee: Priority fee setting — "auto" or integer microLamports.
Returns:
Swap response dict containing swapTransaction, or None on failure.
"""
body = {
"quoteResponse": quote,
"userPublicKey": user_pubkey,
"wrapAndUnwrapSol": True,
"dynamicComputeUnitLimit": True,
"prioritizationFeeLamports": priority_fee,
}
try:
resp = client.post(
f"{JUPITER_BASE_URL}/swap",
json=body,
timeout=30.0,
)
resp.raise_for_status()
return resp.json()
except httpx.HTTPStatusError as e:
print(f"Swap API error {e.response.status_code}: {e.response.text}")
return None
except httpx.HTTPError as e:
print(f"HTTP error building swap: {e}")
return None
# ── Simulate Transaction ────────────────────────────────────────────
def simulate_transaction(
client: httpx.Client,
rpc_url: str,
swap_transaction_b64: str,
) -> Optional[dict]:
"""Simulate a transaction via Solana RPC without submitting.
Args:
client: HTTP client instance.
rpc_url: Solana RPC endpoint URL.
swap_transaction_b64: Base64-encoded transaction from Jupiter.
Returns:
Simulation result dict, or None on RPC failure.
"""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": [
swap_transaction_b64,
{
"encoding": "base64",
"commitment": "confirmed",
"sigVerify": False,
"replaceRecentBlockhash": True,
},
],
}
try:
resp = client.post(rpc_url, json=payload, timeout=30.0)
resp.raise_for_status()
result = resp.json()
if "error" in result:
print(f"RPC error: {result['error']}")
return None
return result.get("result", {}).get("value")
except httpx.HTTPStatusError as e:
print(f"RPC HTTP error {e.response.status_code}: {e.response.text}")
return None
except httpx.HTTPError as e:
print(f"HTTP error during simulation: {e}")
return None
# ── Display ─────────────────────────────────────────────────────────
def format_amount(raw: str, decimals: int) -> str:
"""Format raw token amount for display.
Args:
raw: Amount string in smallest units.
decimals: Token decimal places.
Returns:
Formatted amount string.
"""
value = int(raw) / (10 ** decimals)
if value >= 1.0:
return f"{value:,.4f}"
return f"{value:,.{decimals}f}"
def display_quote_summary(quote: dict) -> None:
"""Print a concise quote summary.
Args:
quote: Jupiter quote response.
"""
in_dec = KNOWN_DECIMALS.get(quote["inputMint"], 9)
out_dec = KNOWN_DECIMALS.get(quote["outputMint"], 6)
in_sym = "SOL" if quote["inputMint"] == SOL_MINT else quote["inputMint"][:8] + "..."
out_sym = "USDC" if quote["outputMint"] == USDC_MINT else quote["outputMint"][:8] + "..."
in_amt = format_amount(quote["inAmount"], in_dec)
out_amt = format_amount(quote["outAmount"], out_dec)
min_amt = format_amount(quote["otherAmountThreshold"], out_dec)
impact = float(quote.get("priceImpactPct", "0"))
route_parts = []
for step in quote.get("routePlan", []):
label = step.get("swapInfo", {}).get("label", "?")
route_parts.append(label)
route_str = " → ".join(route_parts) if route_parts else "Unknown"
print("─── Quote Summary ────────────────────────────────────────")
print(f" Sell: {in_amt} {in_sym}")
print(f" Buy: ~{out_amt} {out_sym}")
print(f" Min recv: {min_amt} {out_sym}")
print(f" Impact: {impact:.4f}%")
print(f" Slippage: {quote.get('slippageBps', 0) / 100:.2f}%")
print(f" Route: {route_str}")
print()
def display_swap_details(swap_data: dict) -> None:
"""Print swap transaction build details.
Args:
swap_data: Jupiter swap response.
"""
tx_b64 = swap_data.get("swapTransaction", "")
tx_bytes = base64.b64decode(tx_b64) if tx_b64 else b""
last_block = swap_data.get("lastValidBlockHeight", "N/A")
priority = swap_data.get("prioritizationFeeLamports", "N/A")
cu_limit = swap_data.get("computeUnitLimit", "N/A")
dynamic = swap_data.get("dynamicSlippageReport")
print("─── Swap Transaction Details ─────────────────────────────")
print(f" Transaction size: {len(tx_bytes)} bytes")
print(f" Last valid block: {last_block}")
print(f" Priority fee: {priority} lamports")
print(f" Compute unit limit: {cu_limit}")
if dynamic:
print(f" Dynamic slippage: {dynamic.get('slippageBps', 'N/A')} bps")
sim_slip = dynamic.get("simulatedIncurredSlippageBps", "N/A")
print(f" Simulated slippage: {sim_slip} bps")
print()
def display_simulation_result(sim_result: dict) -> None:
"""Print simulation results.
Args:
sim_result: Simulation value from RPC response.
"""
err = sim_result.get("err")
units = sim_result.get("unitsConsumed", 0)
logs = sim_result.get("logs", [])
print("─── Simulation Result ────────────────────────────────────")
if err is None:
print(" Status: SUCCESS")
print(f" Compute: {units:,} units consumed")
else:
print(" Status: FAILED")
print(f" Error: {err}")
print(f" Compute: {units:,} units consumed")
# Show last N log lines (most informative)
if logs:
print()
print(" Logs (last 15 lines):")
for line in logs[-15:]:
# Truncate long log lines
if len(line) > 100:
line = line[:97] + "..."
print(f" {line}")
print()
if err is None:
print(" ✓ Transaction would succeed if signed and submitted.")
else:
print(" ✗ Transaction would FAIL. Check error and logs above.")
print()
print("─── REMINDER: This was a SIMULATION only. ────────────────")
print(" No transaction was signed, submitted, or executed.")
print("─────────────────────────────────────────────────────────")
print()
# ── Pipeline ────────────────────────────────────────────────────────
def run_simulation(
client: httpx.Client,
input_mint: str,
output_mint: str,
amount: int,
slippage_bps: int,
user_pubkey: str,
rpc_url: str,
) -> bool:
"""Run the full quote → build → simulate pipeline.
Args:
client: HTTP client instance.
input_mint: Input token mint.
output_mint: Output token mint.
amount: Input amount in smallest units.
slippage_bps: Slippage tolerance in basis points.
user_pubkey: Wallet public key.
rpc_url: Solana RPC URL.
Returns:
True if simulation succeeded, False otherwise.
"""
# Step 1: Get quote
print("Step 1/3: Fetching Jupiter quote...")
quote = fetch_quote(client, input_mint, output_mint, amount, slippage_bps)
if not quote:
print("Failed to fetch quote. Aborting.")
return False
display_quote_summary(quote)
# Safety check: price impact
impact = float(quote.get("priceImpactPct", "0"))
if impact > 10.0:
print(f"BLOCKED: Price impact {impact:.2f}% exceeds 10% safety limit.")
print("This trade would likely result in significant loss.")
return False
elif impact > 5.0:
print(f"WARNING: High price impact ({impact:.2f}%). In production,")
print("this would require explicit user override.")
# Step 2: Build swap transaction
print("Step 2/3: Building swap transaction...")
swap_data = build_swap_transaction(client, quote, user_pubkey)
if not swap_data:
print("Failed to build swap transaction. Aborting.")
return False
swap_tx_b64 = swap_data.get("swapTransaction", "")
if not swap_tx_b64:
print("No transaction returned from Jupiter. Aborting.")
return False
display_swap_details(swap_data)
# Step 3: Simulate
print("Step 3/3: Simulating transaction via RPC...")
sim_result = simulate_transaction(client, rpc_url, swap_tx_b64)
if sim_result is None:
print("RPC simulation call failed. This may be a network issue.")
print("The transaction itself may still be valid.")
return False
display_simulation_result(sim_result)
return sim_result.get("err") is None
# ── Main ────────────────────────────────────────────────────────────
def run_demo(client: httpx.Client) -> None:
"""Run demo simulation with SOL → USDC.
Note: Demo uses a system account as pubkey, so the swap build
may fail (account doesn't hold SOL). This demonstrates the
pipeline and error handling.
Args:
client: HTTP client instance.
"""
print("Running demo: Simulate 1 SOL → USDC swap")
print(f"Using demo pubkey: {DEMO_PUBKEY[:16]}...")
print("(Demo account may not have balance — build/simulation may fail)")
print()
run_simulation(
client=client,
input_mint=SOL_MINT,
output_mint=USDC_MINT,
amount=1_000_000_000,
slippage_bps=50,
user_pubkey=DEMO_PUBKEY,
rpc_url=SOLANA_RPC_URL,
)
def main() -> None:
"""Main entry point."""
print(SAFETY_BANNER)
demo_mode = "--demo" in sys.argv
with httpx.Client() as client:
if demo_mode:
run_demo(client)
return
if not USER_PUBKEY:
print("ERROR: USER_PUBKEY environment variable is required.")
print("Set it to your wallet's public key (base58 format).")
print()
print("Example:")
print(' USER_PUBKEY="YourPubkeyHere" python scripts/simulate_swap.py')
print()
print("Or run in demo mode:")
print(" python scripts/simulate_swap.py --demo")
sys.exit(1)
print(f"Configuration:")
print(f" Input mint: {INPUT_MINT}")
print(f" Output mint: {OUTPUT_MINT}")
print(f" Amount: {AMOUNT_LAMPORTS} (smallest units)")
print(f" Slippage: {SLIPPAGE_BPS} bps")
print(f" Wallet: {USER_PUBKEY[:16]}...")
print(f" RPC: {SOLANA_RPC_URL[:40]}...")
print()
success = run_simulation(
client=client,
input_mint=INPUT_MINT,
output_mint=OUTPUT_MINT,
amount=AMOUNT_LAMPORTS,
slippage_bps=SLIPPAGE_BPS,
user_pubkey=USER_PUBKEY,
rpc_url=SOLANA_RPC_URL,
)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
Related skills
FAQ
Which venues does Jupiter route across?
Raydium, Orca, Meteora, Phoenix, Lifinity, and 20+ other Solana DEXes, sometimes splitting a swap across pools.
What slippage is recommended?
0.5-1% for majors, 1-3% for mid-caps, 5-20% for PumpFun/meme tokens, and 10-30% for launches under an hour old.