Skip to main content

Canister Integration

For canisters (Motoko or Rust) that need to swap tokens through PartyDEX. Your canister calls pass_through_trade — wallet-to-wallet, no trading balance, no polling loop.

This is exactly how PartyDEX's own treasury canister executes buybacks — production code, not a demo.


The Flow

One-time: ICRC-2 approve the spot canister to pull your input tokens.

Per swap: quote the route, then execute.

// 1. One-time: approve spot canister to spend your input token
await input_ledger.icrc2_approve({
spender = { owner = spot_canister_id; subaccount = null };
amount = 340_282_366_920_938_463_463_374_607_431_768_211_455; // max Nat
expected_allowance = null;
expires_at = null;
fee = null; memo = null; from_subaccount = null; created_at_time = null;
});

// 2. Quote — get smart-routed breakdown (read-only)
let #ok(quote) = await spot.quote_order(#buy, amount, ?887272, ?50)
else return #err("No route");

// 3. Execute — wallet-to-wallet
let #ok(trade) = await spot.pass_through_trade({
book_order = quote.book_order;
pool_swaps = quote.pool_swaps;
recipient = null; // tokens go to caller (set a principal to redirect)
}) else return #err("Trade rejected");

// 4. Check both transfer legs
switch (trade.output.error) {
case (?err) { /* output stuck in trading balance — recover via withdraw */ };
case null { /* trade.output.amount is yours */ };
};
switch (trade.refund.error) {
case (?err) { /* unused input stuck in trading balance — recover via withdraw */ };
case null { /* trade.refund.amount returned to wallet (or 0 if fully consumed) */ };
};

quote_order(side, input_amount, ?limit_tick, ?slippage_bps)input_amount is quote for buys, base for sells. Use ?887272 as limit_tick for "any price", or null for no limit. pass_through_trade takes book_order and pool_swaps directly from the quote result; set recipient to null to receive tokens at the caller.


Actor Interface

Define only what you need. This covers the full pass-through flow:

module SpotInterface {
public type Side = { #buy; #sell };
public type Tick = Int32;

public type BookOrderSpec = {
side : Side;
input_amount : Nat;
limit_tick : Tick;
immediate_or_cancel : Bool;
};

public type PoolSwapSpec = {
side : Side;
input_amount : Nat;
limit_tick : Tick;
fee_pips : Nat32;
};

public type ApiError = {
category : { #validation; #authorization; #state; #resource; #rate_limit; #external; #admin; #other };
code : Text;
message : Text;
metadata : ?[(Text, Text)];
};

public type TransferLeg = {
amount : Nat;
block_index : ?Nat;
error : ?Text;
};

public type QuoteResult = {
input_amount : Nat;
output_amount : Nat;
pool_swaps : [PoolSwapSpec];
book_order : ?BookOrderSpec;
total_fees : Nat;
effective_tick : Tick;
reference_tick : Tick;
venue_breakdown : [{ venue_id : { #book; #pool : Nat32 }; input_amount : Nat; output_amount : Nat; fee_amount : Nat }];
};

public type PassThroughTradeSuccess = {
versions : { platform : Nat; orderbook : Nat; candle : Nat; user : Nat };
order_results : [{ index : Nat32; result : { #ok : { order_id : Nat }; #err : ApiError } }];
swap_results : [{ index : Nat32; result : { #ok : { input_amount : Nat; output_amount : Nat; fee : Nat }; #err : ApiError } }];
output : TransferLeg;
refund : TransferLeg;
};

public type TokenMetadata = {
ledger : Principal;
decimals : Nat8;
fee : Nat;
};

public type RoutingState = {
system_state : { #normal; #degraded; #halted };
base : TokenMetadata;
quote : TokenMetadata;
reference_tick : ?Tick;
};

public type WithdrawSuccess = {
versions : { platform : Nat; orderbook : Nat; candle : Nat; user : Nat };
withdrawn : Nat;
remaining : Nat;
block_index : ?Nat;
};

public type SpotDEX = actor {
get_routing_state : shared query () -> async RoutingState;
quote_order : shared query (Side, Nat, ?Tick, ?Nat32) -> async { #ok : QuoteResult; #err : ApiError };
pass_through_trade : shared ({ book_order : ?BookOrderSpec; pool_swaps : [PoolSwapSpec]; recipient : ?Principal }) -> async { #ok : PassThroughTradeSuccess; #err : ApiError };
withdraw : shared ({ #base; #quote }, Nat) -> async { #ok : WithdrawSuccess; #err : ApiError };
};
};

Market Discovery

Find the spot canister ID for a trading pair:

let indexer = actor("gx3we-baaaa-aaaab-afaia-cai") : actor {
get_market_by_symbols : shared query (Text, Text) -> async ?Principal;
};

let ?spot_id = await indexer.get_market_by_symbols("ckBTC", "ckUSDT")
else return #err("Market not found");

let spot : SpotInterface.SpotDEX = actor(Principal.toText(spot_id));

Token metadata (ledger principal, decimals, fee) is available from get_routing_state() on any spot canister — no hardcoded lookup needed:

let state = await spot.get_routing_state();
let input_ledger : ICRC.Ledger = actor(Principal.toText(state.quote.ledger)); // quote for buys

Error Handling

pass_through_trade can fail at two levels:

  • Top-level #err — validation, rate limit, insufficient allowance. No tokens moved.
  • Transfer leg errors — the swap executed but the outbound ICRC transfer failed (trade.output.error or trade.refund.error). Tokens are safe in the spot canister's trading balance — recover with withdraw:
// Recover tokens stuck in trading balance after a failed outbound transfer
let #ok(w) = await spot.withdraw(#quote, amount) else return #err("Withdraw failed");
// w.withdrawn = net amount returned to your canister's wallet

Retry Strategy

on #err:
match category:
validation → fix input (amount, tick, allowance), retry immediately
state → check (await spot.get_routing_state()).system_state, retry once
external → retry with exponential backoff (500ms base, 30s cap)
rate_limit → see Rate Limits

For rate limit tiers, cooldowns, and recovery, see Rate Limits.

All mutations can return errors — read Rate Limits before writing retry logic. Repeated err responses increment violations regardless of the call path.