Guides

Build an adaptor

Write the Move package that wraps an external protocol behind the SupWallet::intent flow. This is the step before publishing: publishing turns a deployed package into a listing; this page is how you write that package so it's safe, compiles, and lists as executable.

Building with an AI agent (Claude Code / Cursor)? Install the sup-adaptor-dev skill below — the same playbook, machine-readable, with the patterns and a compliance checklist your agent follows while it writes the package.

#Use the agent skill

This whole guide is also packaged as an installable skill so your coding agent writes a conforming adaptor for you. It's served straight from this host:

📥 View / download the skill → /llms/adaptor-dev.md  ·  on GitHub: view · raw

# fetch from this host…
mkdir -p .claude/skills/sup-adaptor-dev
curl -sL "$SUP/llms/adaptor-dev.md" -o .claude/skills/sup-adaptor-dev/SKILL.md

# …or straight from GitHub
curl -sL "https://raw.githubusercontent.com/ZzyzxLabs/zzyzx-full-repo/main/Protocols/sup-wallet/skills/sup-adaptor-dev/SKILL.md" \
  -o .claude/skills/sup-adaptor-dev/SKILL.md

The skill lives in the repo at Protocols/sup-wallet/skills/sup-adaptor-dev/SKILL.md (next to sup-marketplace) — the canonical location both this host and GitHub serve from.

  • Claude Code / Cursor: drop it under .claude/skills/<name>/SKILL.md (project) or your user skills dir; the agent loads it automatically and follows the patterns + checklist when you ask it to "build an adaptor for <protocol>".
  • Any agent that fetches URLs: point it at $SUP/llms.txt — the index links this skill at /llms/adaptor-dev.md alongside the API and publish guides.
  • MCP / CLI: pair it with the sup-marketplace tools (introspect, infer, draft) from Build an agent so the agent can draft + publish the package it just wrote.

The rest of this page is that skill in prose — read on, or hand it to your agent.

#The two hard requirements

A package is a Sup adaptor if and only if:

  1. It declares a witness typepublic struct MyAdaptor has drop {}, a struct with drop and nothing else. Its fully-qualified name 0x<pkg>::<module>::MyAdaptor is the service-type (the listing key, and exactly what an owner authorizes). Only the defining module can construct MyAdaptor {}, which is what makes its intent calls unforgeable.
  2. It moves funds only through SupWallet::intent — it pulls a Coin out via validate_* and pushes results back via verify_*_and_credit / receive_from_service. It never transfers a vault coin itself and never takes a raw vault Coin as a parameter.

That's it. Everything below is how to satisfy #2 correctly.

#The hot-potato contract

When intent releases vault funds it hands you a value with no abilities (no drop/store/copy): a WalletWitness (payments) or WalletSwapWitness (swaps). Move won't let the transaction compile or finish until you consume it — by handing back a matching receipt to the verify_* call. Two consequences you should rely on:

  • You can't forget to settle. A leaked potato is a build error, not a runtime bug.
  • Slippage/amount checks live in verify_*. verify_swap_and_credit re-checks that the returned coin's value equals the reported amount_out and is >= min_out, and aborts otherwise. Don't hand-roll your own slippage assert — pass min_out into request_swap and let intent enforce it.

Everything runs in one PTB, so it's atomic: a later failure rolls back the released funds. (This atomicity is Sui-only — it does not extend across chains.)

#Which flow to use

You are doingModeCoin direction
Swap / deposit — coin in, different coin back into the vaultD request_swap → validate_and_swap_out → … → verify_swap_and_creditCoinIn out, CoinOut back
Pay an external recipient or service from the vaultA request_payment → validate_and_pay → … → verify_and_clearCoinType out to recipient
Pull-payment — a service charges a designated payerB request_payment_for_payer → validate_and_pay_for_payer → …debits the payer's allowance
Cap-gated payout, no allowance debit (inheritance, vesting)C request_payment_unmetered → validate_and_pay_unmetered → …service is sole gatekeeper

Lending and staking deposits are modeled as Mode D swaps (Underlying → receiptCoin).

#Pattern — Mode D (swap / deposit)

The only part that changes per protocol is step 2.

module my_adaptor::adaptor;

use SupWallet::intent;
use SupWallet::wallet::{Self, Wallet};
use sui::coin;

public struct MyAdaptor has drop {}

public fun swap<CoinIn, CoinOut>(
    wallet: &mut Wallet,
    pool: &mut some_dex::Pool<CoinIn, CoinOut>,   // protocol object(s)
    amount_in: u64,
    min_out: u64,
    ctx: &mut TxContext,
) {
    assert!(amount_in > 0, 0);

    // 1 · release CoinIn (allowance-checked) + receive the hot potato
    let sig = intent::request_swap<MyAdaptor, CoinIn, CoinOut>(MyAdaptor {}, amount_in, min_out);
    let (coin_in, ww) = intent::validate_and_swap_out<MyAdaptor, CoinIn, CoinOut>(wallet, sig, ctx);

    // 2 · YOUR protocol logic: turn coin_in into coin_out
    let coin_out = some_dex::swap<CoinIn, CoinOut>(pool, coin_in, ctx);
    let amount_out = coin::value(&coin_out);

    // 3 · settle: re-checks amount_out >= min_out, credits coin_out back, clears both potatoes
    let receipt = intent::create_swap_receipt<MyAdaptor, CoinIn, CoinOut>(MyAdaptor {}, amount_in, amount_out);
    intent::verify_swap_and_credit<MyAdaptor, CoinIn, CoinOut>(wallet, ww, receipt, coin_out);
}

If your protocol leaves a remainder (e.g. a flash-swap pays back less than you pulled), credit the leftover back into the vault — never drop or transfer it away:

wallet::receive_from_service<MyAdaptor, CoinIn>(wallet, leftover_coin, MyAdaptor {});

#Pattern — Mode A (pay)

public fun pay<CoinType>(wallet: &mut Wallet, amount: u64, recipient: address, ctx: &mut TxContext) {
    let sig = intent::request_payment<MyAdaptor, CoinType>(MyAdaptor {}, amount, recipient);
    let (coin, ww) = intent::validate_and_pay<MyAdaptor, CoinType>(wallet, sig, ctx);

    transfer::public_transfer(coin, recipient);   // or hand the coin to the external service

    let receipt = intent::create_receipt_sig<MyAdaptor, CoinType>(MyAdaptor {}, amount, recipient);
    intent::verify_and_clear<MyAdaptor, CoinType>(ww, receipt);
}

#Write signatures the miner understands

The publish tooling builds your op's call spec automatically — it introspects the ABI and mines the package's past on-chain calls to fill the fixed object ids and scalars. That only works if your signature follows the roles it classifies. Do this and you get an executable listing with zero hand-typing:

  • Put wallet: &mut Wallet first.
  • Name the input amount amount_in / amount, and the slippage floor min_out / min_amount_out (these map to the amount / minOut roles).
  • Take protocol objects (pool, config, market, treasury) by reference. A config that is identical across every past call is a singleton and auto-fills; pools that vary by pair come back as candidates to pick from.
  • Use &Clock for time — it resolves to 0x6.
  • Order generics CoinIn then CoinOut: 1 type param ⇒ coinIn; 2 ⇒ coinIn, coinOut.
  • Fixed scalars (e.g. a sqrt_price_limit) are mined as pure from past calls.

Cold start: a brand-new package has no call history, so mining returns nothing and the op drafts as non-executable. Either make one real call first to seed the history, or pass the object ids explicitly when you draft. See Manifest & call spec for the exact shape the draft produces.

#Compliance checklist

  • The witness struct is has drop only (no copy/store/key).
  • Every fund movement goes through intent — no raw transfer of a vault coin.
  • Each released hot potato is consumed by its verify_* call (it won't compile otherwise).
  • Slippage is the min_out you pass to request_swap, enforced inside verify_swap_and_credit.
  • Leftover/dust coins are credited back with receive_from_service, not dropped or sent away.
  • Generics are ordered CoinIn[, CoinOut]; wallet is the first parameter.
  • Consider freezing the package's UpgradeCap — consumers trust immutable adaptors more.

#Learn from real adaptors

  • Cetus — DEX swap (Mode D) with a flash-swap middle and a leftover credit.
  • Scallop — lending deposit/withdraw modeled as Mode D Underlying ↔ SCoin.
  • Mock swap — minimal and dependency-free; the best starting template.

Find them under the official adaptors in the contracts repo (ZZYZX-Contract/.../official/adaptor_*).

#Next

Once it compiles, you have a deployable package. Deploy it, then Publish an adaptor drafts the manifest (mining your object ids), pins it to Walrus, and gives you the unsigned register() to sign. Check that executableOps matches the ops you intended — if one came back non-executable, revisit the signature conventions above.