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 aperp_positionre-read on the venue that executed. Responseverification.confirmedis the field to gate follow-on logic on, not the baretx_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 withperp_positionson 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:
verification.confirmed(Tier A) istrue, orverification.status == "venue_submitted"(Tier B) with a follow-upperp_positionsread.verification.effects/ the re-readperp_positionblock matches what you expected (side, size, leverage) before sizing any follow-on action.- 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_positionsbefore 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.