Skip to content

Perps playbook

Status: Live (jurisdiction-gated)

This is the full open -> monitor -> funding -> close lifecycle for perpetual futures on Crank, grounded directly in the venue-agnostic perps surface (crank_mcp/tools/perps_tools.py) -- every tool routes through the multi-venue venue adapter (hard rule 2), Jupiter Perps primary, on-chain and non-custodial. venue is optional everywhere; omit it to use the configured primary.

Every write returns an unsigned transaction (Tier A venues) or a signing payload (Tier B, venue-custodied) -- Crank never signs. This playbook covers the two-phase execution pattern once and every step below follows it.

0. One-time jurisdiction attestation

Perps writes are jurisdiction-gated (CFTC posture, hard rule 2). Declare once, standalone of any trade:

result = await client.call_tool("declare_jurisdiction", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "jurisdiction": "SG",   # ISO-3166-1 alpha-2; self-declared, never inferred
})

The attestation persists for 90 days. Without one, every gated write is DENIED fail-safe (unknown jurisdiction is not permission) -- you can also pass jurisdiction inline on place_perp_order itself instead of a separate call.

1. Check venue status and markets first

status = await client.call_tool("get_venue_status", {})
# -> {"primary_venue": "jupiter_perps", "fallback_venue": ..., "routable_venues": [...],
#     "venues": [...], "capabilities": {...}}

markets = await client.call_tool("perp_markets", {})
# -> {"venues": [...], "markets": [...], "count": N}

get_venue_status tells you the configured primary/fallback and every venue's custody_tier. Only trade a market that appears in perp_markets for the venue you intend to route to -- an unmapped asset simply cannot open.

Tier B acknowledgement: if custody_tier for a venue is B (venue-custodied, e.g. Pacifica per hard rule 2), every write to it is refused until you review custody_disclosure and re-call with acknowledge_tier_b=true. Tier A venues (Jupiter Perps: on-chain, non-custodial) need no acknowledgement.

2. Open a position (build leg)

build = await client.call_tool("place_perp_order", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "market": "SOL-PERP",
    "side": "long",
    "size_usd": 200,
    "leverage": 3.0,
    "order_type": "market",
    "take_profit_price": 210.0,
    "stop_loss_price": 165.0,
    # "venue": "jupiter_perps",        # optional -- defaults to primary
    # "acknowledge_tier_b": True,      # only if routed to a Tier B venue
})

Without signed_transaction, this is the build leg: it returns an unsigned transaction (Tier A) or a signing payload (Tier B), plus verification: {"status": "pending_broadcast", "instructions": ...} and a best-effort tracking_id. Nothing has executed yet.

Param Type Default Meaning
wallet_address string required Agent's managed wallet address.
market string required Perp market symbol (see perp_markets).
side string required "long" or "short".
size_usd float required Position notional in USD.
leverage float required Leverage multiplier.
order_type string "market" "market" or a limit variant.
limit_price float none Required for a limit order.
take_profit_price float none Optional TP level.
stop_loss_price float none Optional SL level.
venue string none Defaults to the configured primary (Jupiter Perps).
acknowledge_tier_b bool false Required true on a Tier B venue write.
signed_transaction string none Set on the completion leg (step 3).
verify bool true Skip on-chain re-confirmation on the completion leg if false.
jurisdiction string none Inline attestation instead of a prior declare_jurisdiction call.

3. Sign and complete (two-phase execution)

Your agent's own wallet signs the unsigned transaction / signing payload from step 2 -- Crank never holds or sees your key. Then re-call the same tool with signed_transaction set:

completed = await client.call_tool("place_perp_order", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "market": "SOL-PERP",           # unchanged from the build call
    "side": "long",
    "size_usd": 200,
    "leverage": 3.0,
    "signed_transaction": "<BASE64_SIGNED_TX>",
    "verify": True,
})

This does not re-build the order (that would duplicate it) -- it relays your signed bytes through venue_router.complete_perp_write, dispatched by custody tier:

  • Tier A (Jupiter Perps): broadcasts to Solana RPC, then runs verify_execution -- a real on-chain confirmation plus a perp_position re-read on the venue that executed. Response verification.confirmed is the field to gate follow-on logic on, not the bare tx_signature.
  • Tier B (venue-custodied): submits to the venue's own endpoint and returns a venue order ID (not a Solana signature), reported as verification.status = "venue_submitted" -- there is no on-chain confirmation to run. Confirm the fill with perp_positions on that venue instead.

If you broadcast the signed transaction yourself instead of relaying it through Crank, call the standalone verify_transaction(tx_signature=..., wallet_address=...) to get the same on-chain confirmation.

4. Monitor: positions and funding

positions = await client.call_tool("perp_positions", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    # "venue": "jupiter_perps",   # omit to aggregate across every routable venue
})

funding = await client.call_tool("perp_funding_rates", {
    "market": "SOL-PERP",
    # "venue": "jupiter_perps",   # omit to get rates_by_venue across all venues
})

perp_positions without venue fans out across every routable venue your wallet's venue_allowlist permits and aggregates the results (a single venue's failure never sinks the read). perp_funding_rates is the same shape: pass venue for one venue's rates, omit it for rates_by_venue across all. Poll funding periodically -- a persistently unfavourable funding rate against your side is the standard signal to reduce or close.

5. Close a position (same two-phase pattern)

build = await client.call_tool("close_perp_position", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "position_id": "SOL-PERP",   # venue-native; the market symbol on position-per-market venues
    "close_pct": 100.0,
})
# sign build's unsigned tx / signing payload with your own wallet, then:
completed = await client.call_tool("close_perp_position", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "position_id": "SOL-PERP",
    "close_pct": 100.0,
    "signed_transaction": "<BASE64_SIGNED_TX>",
})

close_pct supports a partial close (e.g. 50.0 to halve the position). Verification follows the exact same tier-aware rule as opening: Tier A gets a real on-chain re-read scoped to position_id as the market key; Tier B gets venue_submitted.

6. Cancel a resting order

build = await client.call_tool("cancel_perp_order", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "order_id": "<ORDER_ID_FROM_PLACE>",
})
# sign, then re-call with signed_transaction=... to complete

A cancel removes a resting order rather than moving a position, so on the completion leg confirming the signature itself IS the verification -- there is no position re-read.

Post-trade verification checklist

Whichever write you just completed, do not treat tx_signature alone as "done." Check:

  1. verification.confirmed (Tier A) is true, or verification.status == "venue_submitted" (Tier B) with a follow-up perp_positions read.
  2. verification.effects / the re-read perp_position block matches what you expected (side, size, leverage) before sizing any follow-on action.
  3. On a Tier A venue, an unconfirmed or failed verification means the trade may not have landed -- do not assume it executed; re-check with perp_positions before retrying (retrying a possibly-landed order can double it).

Strategy automation

Instead of driving this lifecycle by hand, a perp grid or basis trade strategy automates the open/monitor/close loop on a schedule through the same venue adapter and the same two-phase-execution contract: see the Perp grid and Basis trade strategy guides.

See also