feat(launkr): add restricted SIP-010 token launcher + XYK AMM skill - #414
feat(launkr): add restricted SIP-010 token launcher + XYK AMM skill#414julianariel wants to merge 6 commits into
Conversation
Adds the `launkr` skill (SKILL.md, AGENT.md, launkr.ts) for launching and trading restricted SIP-010 tokens on the Launkr protected AMM by Rather Labs, plus the README row and skills.json manifest entry. Subcommands: launch (deploy token + open a bonding/direct pool), get-pool, quote-buy, quote-sell, swap-buy, swap-sell. Network follows the shared NETWORK env var; swaps run in Deny mode with scoped post-conditions (exact STX on buy, exact `strategy-token` FT on sell); launch validates mode-specific args and waits for the deploy to confirm before opening the pool. Verified end-to-end against live testnet + mainnet pools (reads) and the Launkr contract source / API (addresses, arg order, fees, floors, error codes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arc0btc
left a comment
There was a problem hiding this comment.
Adds launkr — a skill for launching restricted SIP-010 tokens and trading through Launkr's XYK AMM singleton on Stacks. Clean, well-scoped addition that reuses this repo's existing transaction/post-condition/fee helpers rather than reinventing them.
What works well:
- Both
swap-buyandswap-sellrun inPostConditionMode.Denywith an exact (eq) post-condition scoping the asset actually being sent (STX in, or the FT viacreateFungiblePostConditionin the sell case) — that's the safe default for a live wallet, and it's called out explicitly inAGENT.mdas intentional (don't switch toAllow). - Correct use of the shared helpers:
deployContract/callContractsignatures,createStxPostCondition/createFungiblePostConditionargs, andNETWORK/getApiBaseUrlall matchsrc/lib/*as implemented on main — no drift or reinvented wrappers. AGENT.mdis unusually good for a first-time skill submission: it calls out the hash-gate invariant (never editclarityCode), the two-step confirm-before-pool-create sequencing, and treatsnonefromquote-*as "do not proceed" rather than "assume zero." That's exactly the kind of guidance that prevents an agent from doing something costly on a misread.waitForConfirmation's abort-status list is broad (coversdropped_replace_by_fee,dropped_stale_garbage_collect, etc.), not justabort_by_response— good coverage of the ways a Stacks tx can fail to land.
[suggestion] Cross-check the API's returned pool-creation args against what was requested (launkr.ts:866-874, 903)
poolStep.functionArgs comes straight from https://launkr.io/api/launch and is broadcast via create-pool-* without comparing it back to the launchBody that was sent (e.g. feeReceiver, supply). The on-chain hash gate protects the token contract (byte-identical to the template), but nothing protects the pool-creation call itself — if the API ever returned a different feeReceiver than the one requested (bug, cache issue, compromised endpoint), the caller would silently launch a pool that permanently routes 90% of swap fees to an address they didn't choose, and AGENT.md even flags that risk in its "what NOT to do" section without the code enforcing it. A cheap mitigation: assert the feeReceiver (and maybe supply) arg in poolStep.functionArgs matches opts.feeReceiver/opts.supply before calling deployContract, and abort with a clear error if they don't.
[nit] Minor formatting inconsistency in swap-buy/swap-sell: .option("--recipient <address>", ...) and the following .option("--fee <fee>", ...) are glued onto the same line (launkr.ts:1163-1166, 1229), unlike every other option in the file which gets its own line. Harmless, but a prettier/format pass would catch it.
Code quality notes:
unwrapCV/decodeCVare a reasonable, self-contained way to flattencvToValue's{type,value}tree into plain JS without pulling in another dependency — no simplification needed there.- No dead code or unused imports spotted; the three read-only commands (
get-pool/quote-buy/quote-sell) intentionally duplicate the wallet-address-fallback pattern rather than sharing a helper — three call sites is right at the "still fine inline" threshold, wouldn't push for extraction yet.
Big-picture fit: Follows the same shape as bitflow/defi (CLI skill, shared src/lib transaction helpers, README.md + skills.json entries), and the network-selection convention (NETWORK env var, no per-command flag) matches how the rest of the repo's skills already work. No conflicts with anything else in the tree.
Nothing here is blocking — the feeReceiver cross-check is worth doing before agents start using this against mainnet, but it doesn't need to hold up the merge.
Cross-checks name, symbol, supply, and fee-receiver from the API's pool-creation response against what was requested, before spending any gas. Addresses arc0btc's review comment on aibtcdev#414.
|
@arc0btc we've applied the improvements, please review again and approve if it's ok! |
arc0btc
left a comment
There was a problem hiding this comment.
Re-reviewed the new commit (81ee5b4, "validate pool-creation args match request before deploying") — the validatePoolStepMatchesRequest guard is a solid addition, thanks for that.
[blocking] parseLaunkrArg's handling of optional-utf8/optional-ascii changed from noneCV() to someCV(stringUtf8CV("")) when the value is null (launkr.ts lines ~104-112), with a comment describing it as a workaround for a BadFunctionArgument broadcast rejection on bare noneCV().
This directly contradicts AGENT.md's own guidance in this same PR (under "Launching a token", point 3):
The optional
uriargument is a genuine Clarity optional... Do not substitute aSome("")or any placeholder string.
If the code change is correct, AGENT.md needs to be updated to match (and the on-chain implication — the token's uri field is now permanently set to "" instead of none for every launch that omits --uri — should be called out explicitly, since that's a real behavioral change to deployed token metadata, not just a broadcast-plumbing detail).
If AGENT.md's guidance is the correct one, the workaround needs a different fix — e.g. confirm whether the BadFunctionArgument is really about noneCV() encoding vs. something else in the request (arg ordering, @stacks/transactions version mismatch as the comment speculates), rather than papering over it by silently changing the on-chain value.
Either way, doc and code should agree before merge — right now an agent reading AGENT.md would expect none and get Some("") instead.
Rest of the commit (network handling via resolveNetwork/NET_CONFIG, cvToValue unwrap simplification, temp-file cleanup) looks fine on read-through.
…havior
The docs that landed in this PR were an earlier draft that never got
synced with the verified launkr.ts implementation, causing two real
contradictions arc0btc caught:
- AGENT.md claimed bare (none) broadcasts fine for the optional uri arg
and told agents not to use Some("") — the opposite of what's verified
(noneCV() reliably causes BadFunctionArgument; Some("") is the working
fix already in launkr.ts). Now explicitly documents the on-chain
implication: uri ends up Some("") instead of None when omitted.
- AGENT.md/SKILL.md claimed there's no per-command --network flag, but
every launkr.ts command has one.
Also reconciles skills.json's author/authorAgent metadata into SKILL.md's
frontmatter, and documents the validatePoolStepMatchesRequest guard added
in 81ee5b4.
|
@arc0btc thanks for the detailed feedback, we've applied all the comments and fixes, can you please re-review? |
|
@biwasxyz, could you review this PR and the fixes, and merge it if everything looks good? |
biwasxyz
left a comment
There was a problem hiding this comment.
Thanks for this — the protocol documentation here is unusually good. The error-code table, the protocol floors, the flat-vs-nested API gotcha, and especially the candid write-up of the uri tradeoff are more thorough than most skill PRs get. The restricted-token + singleton + hash-gate design is also genuinely interesting, and "an agent can create a tradeable asset from nothing" is a capability this repo doesn't have yet.
I went through launkr.ts line by line against the shared libs it calls. bun run typecheck passes. A few things need resolving before merge, and I have some questions where I couldn't verify the on-chain behavior myself.
Blockers
1. swap-buy / swap-sell look like they'll abort with abort_by_post_condition
Both use PostConditionMode.Deny but only post-condition the caller's outgoing leg:
launkr.ts:746-750— covers "user sendsstxInuSTX", but not the singleton sending tokens torecipient, nor the fee STX to the fee-receiver.launkr.ts:816-825— covers the user's FT send, but not the singleton's STX payout.
Under Deny mode every principal that sends an asset needs coverage, including the contract paying out. The convention is visible in this repo's own merged code — bitflow-hodlmm-withdraw/bitflow-hodlmm-withdraw.ts:661-711 builds PCs for the user leg and a Pc.principal(poolContract).willSendGte(minOut) for each payout leg.
The fix is small: add a willSendGte(minTokensOut) FT condition on the singleton in swap-buy, and willSendGte(minStxOut) uSTX on the singleton in swap-sell. You already have both minimums as required options, so they map straight across.
2. --network doesn't control which chain the transaction is broadcast to
resolveNetwork() (launkr.ts:69) selects the singleton address, the Hiro read URL, and the explorer chain= param — but the actual broadcast network comes from account.network inside callContract/deployContract (src/lib/transactions/builder.ts:123 and :162), which reads the global NETWORK env and is untouched by the flag.
NETWORK defaults to testnet (src/lib/config/networks.ts:5). So:
--network mainneton a default install → tx carrying the mainnet singletonSP2ABWV7..., broadcast to testnet.NETWORK=mainnet+--network testnet→ the token contract deploys on mainnet with real STX, then pool creation targets a testnet address that doesn't exist there. Money spent, no pool.waitForConfirmationis also polling the wrong Hiro instance, so it hangs for the full 5-minute timeout first.
What makes this urgent rather than cosmetic is that the docs steer agents directly into it — AGENT.md:12-16 and SKILL.md:64-66 both say "pass it explicitly rather than relying on the NETWORK env var default."
Two ways out: either drop --network and derive everything from account.network, or keep the flag and hard-fail when it disagrees with the wallet's network. The second is friendlier but the first is less to get wrong.
A smaller symptom of the same split: getExplorerTxUrl(txid, NETWORK) and chainExplorerUrl (launkr.ts:460-462, :762-763, :837-838) use different sources, so one JSON output can carry two links pointing at different chains.
3. bun run validate fails
FAIL launkr/AGENT.md
- name: Invalid input: expected string, received undefined
- skill: Invalid input: expected string, received undefined
- description: Invalid input: expected string, received undefined
Results: 201 passed, 1 failed, 202 total
launkr/AGENT.md is missing its YAML frontmatter. See bitflow/AGENT.md:1-5 for the shape — three lines.
Worth addressing
4. The token's Clarity source is deployed unverified. launch takes deployStep.clarityCode from the API and deploys it under the user's key (launkr.ts:402-412) with no local check. There's a slight asymmetry here: validatePoolStepMatchesRequest exists precisely because the API isn't trusted, but the much larger surface — arbitrary contract code signed by the user's wallet — is accepted as-is. Since the singleton already gates on a hash of restricted-token-template-v6, fetching that template and comparing before deploying would close it, and would fail before spending gas rather than after.
5. validatePoolStepMatchesRequest skips the curve parameters. It checks name/symbol/supply/fee-receiver (launkr.ts:147-159) but not virtual-stx or graduation-threshold. stx-seed happens to be covered by the eq STX post-condition; the bonding params aren't covered by anything, and they define the entire price curve.
6. launch is non-atomic with no recovery path. If step 2 fails, or the process is killed during the 5-minute wait, the token is deployed with no pool and there's no way to resume — re-running launch deploys a second contract. A standalone create-pool --token <principal> subcommand would fix it; at minimum the recovery procedure should be documented in AGENT.md.
7. Re-implemented shared infrastructure. CLAUDE.md asks that skills not re-implement network/config logic. This ships its own NET_CONFIG with hardcoded Hiro URLs (launkr.ts:49), its own callReadOnly (:173), and its own tx poller (:219), all bypassing src/lib/services/hiro-api.ts. The practical cost is no API-key header, so waitForConfirmation polling every 6s will hit Hiro rate limits.
8. Dead code at launkr.ts:398-419 — writes clarityCode to tmpdir(), deploys from the in-memory string anyway, discards the result of Bun.file(tmpPath).exists(), then unlinks. The whole block can go.
9. --mode isn't validated locally. A typo reaches the API, and the direct-mode STX post-condition keys off opts.mode === "direct" (launkr.ts:438) — a case mismatch would silently drop the guard.
10. No tests. parseLaunkrArg, decodeCV, and validatePoolStepMatchesRequest are pure and cheap to cover; several sibling modules in src/lib/ ship .test.ts alongside.
Questions
-
Have
swap-buy/swap-sellbeen run end-to-end through this CLI? The worked examples inSKILL.md:202-216read like manual/API broadcasts, and the mainnet one covers only deploy +create-pool-bonding. If there's a txid from a swap executed bylaunkr.tsitself I'd like to look at it, since it would contradict my read of #1. -
Does
create-pool-bondingmove any assets from the deployer?launkr.ts:437-440passes an empty post-condition array under Deny mode. That's correct only if the template mints the supply directly to the singleton. If the deployer holds the supply first and the singleton pulls it, this has the same problem as #1. -
On the
noneCV()→BadFunctionArgumentissue (launkr.ts:82-95) — do you have the rejection payload or a txid? And which@stacks/transactionsversion were you resolving at the time? I'd like to try reproducing against the version this repo pins, because if it is a dependency bug it likely affects other skills too, and if it isn't, the permanentSome("")on every token'suriis avoidable. Full credit for documenting the tradeoff so plainly either way. -
Does graduation change the transfer restriction? As I read it, holders can never move these tokens except by selling back through the singleton — no sending to a friend, no using them in another protocol. Is that permanent, or does a graduated pool unlock
transfer? This matters a lot for howAGENT.mdshould frame the decision to launch, and I don't think it's stated anywhere in the docs right now. -
AGENT.md:90mentions a two-step fee-receiver transfer existing on-chain but the CLI doesn't expose it. Intentional for a first cut, or worth adding? Given the fee-receiver collects 90% of volume permanently, having no way to correct a mistake through this skill seems like a sharp edge. -
Is the
/api/launchendpoint the only path to a launch? Since the token source is hash-gated to a fixed template, it seems like the CLI could fetch the template fromrestricted-token-template-v6on-chain and build the pool-creation args itself, removing the API from the trust path entirely. Is there something in the API response that can't be derived on-chain?
Happy to help with any of these — #1 and #3 in particular are quick, and I'm glad to push a patch if that's easier than another round trip. The core of this is solid work and I'd like to see it land.
|
Follow-up to my review — I went and checked the chain rather than leaving my questions #1 and #2 hanging, since @arc0btc's earlier review reached the opposite conclusion on the post-conditions and that's not a useful thing to leave as two maintainers asserting different things at you. One of my findings is confirmed, one is withdrawn, and my suggested fix was incomplete. Withdrawing question #2 — bonding pool creation is fine as written
No change needed at Confirming blocker #1 — and correcting the fix I suggestedThe only Launkr swap I can find on either network is Four asset movements. Two things worth drawing out of that trace: It ran in My suggested fix was wrong for
I'd rather not hand you an exact bound for the fee legs from a single observed transaction, since I don't know whether the two fee sends aggregate into one post-condition check or need separate coverage, and that's the kind of thing worth confirming on testnet rather than reasoning about. If you have a testnet wallet handy, broadcasting one Deny-mode swap will settle the exact shape faster than any amount of code review. Two things this trace does confirm as correct in the PR: the FT asset name really is Context on my findings #2 and #7Looking at @arc0btc's first review again — the line references ( So my #2 and #7 aren't disagreements with that approval — they're on code that came later and hasn't really had a pass yet. Worth saying plainly so it doesn't read as two reviewers contradicting each other at you. Still open from @arc0btc's blocking review: they offered two routes on the Net: blockers #2 and #3 stand as written, #1 stands but needs a broader fix than I first described, and question #2 is withdrawn. Offer to push a patch for the post-conditions still stands if that's easier than another round trip. |
1. swap-buy/swap-sell aborted with abort_by_post_condition. Deny mode requires every principal that moves an asset to be covered, not just the caller — the singleton also pays out STX (fee legs) on a buy and STX (proceeds + fees) on a sell, on top of the FT leg. Verified live on mainnet: the caller-only post-condition set reliably aborted; adding singleton-side coverage (gte 0 for the fee legs, gte the slippage minimum for the meaningful leg) fixed both swap-buy and swap-sell, confirmed via real broadcasts matching their quotes exactly ((ok u59364737346) and (ok u49554)). 2. --network never controlled the actual broadcast destination for launch/swap-buy/swap-sell — callContract/deployContract read the network from account.network (set by the wallet's own NETWORK env var at creation time), completely independent of the flag. Dropped the flag from the three write commands and derive network from the account directly, so there's one source of truth and no way for the display/config network to disagree with the broadcast network. get-pool/quote-buy/quote-sell keep --network since they don't sign anything. 3. bun run validate failed — launkr/AGENT.md was missing its YAML frontmatter. Added the 3-line header matching the rest of the repo's skills (verified: bun run validate now reports 202/202, and bun run typecheck passes clean). SKILL.md and AGENT.md updated to document the corrected post-condition shape and the network-follows-wallet model, with a new dated worked example for the Deny-mode swap verification.
… repo's pinned deps Answers biwasxyz's review question aibtcdev#3. The BadFunctionArgument rejection that motivated the Some("") workaround was reproduced only against the published @aibtc/mcp-server npm package's own dependency resolution — not against this repo's pinned @stacks/transactions@7.3.1. Verified directly: a bare noneCV() for the uri arg on set-token-uri broadcasts and confirms (ok true) against this repo's exact dependency version, both via a standalone script and via this repo's own callContract export (testnet txids 6ee46234adfd545bb55d7396835fa730a4184324ac3ad1bf47b0406305234d8e and 9403bd6670eea9fb5f6812b937bdcd1604adb2d79da019c66583ae13fe38fbc6). parseLaunkrArg now sends a proper none again instead of a permanent empty-string placeholder. Docs updated to record the resolution instead of describing an ongoing tradeoff. Re-verified: bun run typecheck clean, bun run validate 202/202.
…tcdev#10 + Q4/Q5/Q6) - aibtcdev#4: verify deploy clarityCode byte-matches the on-chain template before spending gas on it, instead of trusting the API response. - aibtcdev#5: extend validatePoolStepMatchesRequest to also check virtual-stx/ graduation-threshold (bonding) or stx-seed (direct), not just name/symbol/supply/fee-receiver. - aibtcdev#6: add a create-pool subcommand as a recovery path for when launch's step 2 fails or is interrupted after the token already deployed — builds the create-pool-* call directly rather than re-calling /api/launch. launch now also prints this recovery instruction after a successful deploy. - aibtcdev#7: replace the hand-rolled NET_CONFIG.hiroApi/callReadOnly/tx-poller with the shared getHiroApi().callReadOnlyFunction and pollTransactionConfirmation (src/lib) — picks up the Hiro API key header these lacked before. - aibtcdev#8: remove the dead temp-file write/read/unlink around deployContract. - aibtcdev#9: validate --mode locally (must be exactly 'bonding' or 'direct') before it can silently skip the direct-mode post-condition guard. - aibtcdev#10: add launkr.test.ts covering parseLaunkrArg, decodeCV, and validatePoolStepMatchesRequest (guarded program.parse with import.meta.main, matching the convention already used in hodlmm-flow/hodlmm-move-liquidity/stacks-alpha-engine, so importing for tests doesn't trigger CLI parsing). - Q4: confirmed via the singleton's own source that graduating a pool never touches the token's allowlist — transfers stay restricted to the singleton permanently, documented in both SKILL.md and AGENT.md. - Q5: exposed the two-step fee-receiver transfer as set-fee-receiver/ accept-fee-receiver subcommands — previously on-chain but inaccessible through this skill. - Q6: documented the analysis (yes, technically derivable on-chain) without unilaterally dropping /api/launch from the deploy step — that's a product decision (Launkr's own launch tracking) for the team, not something to change inside a skill PR. create-pool already bypasses the API for step 2. Verified: bun run typecheck clean, bun run validate 202/202, bun test launkr/launkr.test.ts 18/18 passing. Full bun test shows 8 pre-existing failures in src/lib/services/x402.service.test.ts unrelated to this change — reproduced identically with these launkr changes fully reverted, so not a regression introduced here.
|
Thanks both for the thorough reviews, this took a few passes to get right, all fixed now across three commits (446e322, fa4a7dc, 43b1ef7). @biwasxyz's blockers swap-buy: caller eq stx-in (uSTX), singleton gte 0 (uSTX, the two fee legs), singleton gte min-tokens-out (FT — the real slippage guard) #2 (--network) — confirmed by reading builder.ts:123/162 — account.network is what actually gets used, independent of any flag. Dropped --network from launch/swap-buy/swap-sell entirely rather than trying to reconcile it; kept it on the three read-only commands since they don't sign anything. #3 (validate) — AGENT.md frontmatter added. bun run validate is 202/202 now. @biwasxyz's worth-addressing + questions Appreciate the depth on both reviews — happy to take another pass if anything's still off |
launkr— launch & trade restricted SIP-010 tokens on LaunkrLaunkr is a protected token launcher and XYK AMM on Stacks, built by Rather Labs. Each pool trades STX against a restricted SIP-010 token whose transfers are locked to the authorized singleton, so every swap routes through the protocol and captures fees. Tokens launch in one of two pool modes — bonding (virtual reserves, 1% fee, auto-graduates once it crosses its threshold) or direct (real STX seed, 5% fee). Works on mainnet and testnet.
This skill lets an agent (or a user via any LLM client) drive the full lifecycle:
launch— deploy a restricted token (byte-identical to the on-chain template) and open its bonding or direct pool.get-pool— read a pool's mode, reserves, graduation progress, and fee receiver.quote-buy/quote-sell— simulate a trade and get expected output net of fees (no wallet needed).swap-buy/swap-sell— trade STX for tokens and back through the singleton, with slippage guards.SKILL.mddocuments the subcommands, arguments, protocol floors, and error codes;AGENT.mdcovers autonomous-operation rules. Files:launkr/{SKILL.md,AGENT.md,launkr.ts}, plus aREADME.mdrow and theskills.jsonmanifest entry.