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-devskill 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.mdalongside the API and publish guides. - MCP / CLI: pair it with the
sup-marketplacetools (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:
- It declares a witness type —
public struct MyAdaptor has drop {}, a struct withdropand nothing else. Its fully-qualified name0x<pkg>::<module>::MyAdaptoris the service-type (the listing key, and exactly what an owner authorizes). Only the defining module can constructMyAdaptor {}, which is what makes its intent calls unforgeable. - It moves funds only through
SupWallet::intent— it pulls aCoinout viavalidate_*and pushes results back viaverify_*_and_credit/receive_from_service. It nevertransfers a vault coin itself and never takes a raw vaultCoinas 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_creditre-checks that the returned coin's value equals the reportedamount_outand is>= min_out, and aborts otherwise. Don't hand-roll your own slippage assert — passmin_outintorequest_swapand 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 doing | Mode | Coin direction |
|---|---|---|
| Swap / deposit — coin in, different coin back into the vault | D request_swap → validate_and_swap_out → … → verify_swap_and_credit | CoinIn out, CoinOut back |
| Pay an external recipient or service from the vault | A request_payment → validate_and_pay → … → verify_and_clear | CoinType out to recipient |
| Pull-payment — a service charges a designated payer | B 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 Walletfirst. - Name the input amount
amount_in/amount, and the slippage floormin_out/min_amount_out(these map to theamount/minOutroles). - 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
&Clockfor time — it resolves to0x6. - Order generics
CoinInthenCoinOut: 1 type param ⇒coinIn; 2 ⇒coinIn, coinOut. - Fixed scalars (e.g. a
sqrt_price_limit) are mined aspurefrom 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 droponly (nocopy/store/key). - Every fund movement goes through
intent— no rawtransferof a vault coin. - Each released hot potato is consumed by its
verify_*call (it won't compile otherwise). - Slippage is the
min_outyou pass torequest_swap, enforced insideverify_swap_and_credit. - Leftover/dust coins are credited back with
receive_from_service, not dropped or sent away. - Generics are ordered
CoinIn[, CoinOut];walletis 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.