- name
- launkr
- description
- Launch and trade restricted SIP-010 tokens on Launkr — a protected token launcher and XYK AMM on Stacks. Deploy a token, open a bonding or direct pool, and trade STX for tokens via the singleton contract. Works on both mainnet and testnet.
- metadata
- {"author":"rather-labs","author-agent":"Launkr by Rather Labs","user-invocable":"false","arguments":"launch | create-pool | get-pool | quote-buy | quote-sell | swap-buy | swap-sell | swap-and-burn | quote-swap-and-burn | is-paused | set-fee-receiver | accept-fee-receiver","entry":"launkr/launkr.ts","mcp-tools":"deploy_contract, call_contract, call_read_only_function","requires":"wallet","tags":"l2, defi, write, requires-funds"}
# Launkr Skill
Launch and trade restricted SIP-010 tokens on Launkr — a protected token
launcher and AMM on the Stacks blockchain. Contracts are public and
permissionless — any agent with a Stacks wallet can call them directly. No
intermediary, no custody, no fee to any third party beyond Launkr's own
protocol fee (see below).
**What Launkr is:** A singleton XYK AMM that hosts N pools. Each pool trades
STX against a *restricted* SIP-010 token. "Restricted" means composability,
not custody: transfers to an ordinary wallet always succeed; only sending
to a *contract* is gated (allowlisted principals/code-hashes, initially
just the singleton). This guarantees fee capture on every *swap* — trades
must go through the singleton — without freezing the token in your wallet.
See `AGENT.md`'s "What NOT to do" for the precise on-chain logic and why
an earlier draft of these docs described this restriction incorrectly.
**Two pool modes:**
- **Bonding** (`create-pool-bonding`) — Starts with virtual reserves. No STX
seed required. Fee: 1%. Automatically graduates to direct mode when real
STX collected crosses the graduation threshold.
- **Direct** (`create-pool-direct`) — Starts with a real STX seed (≥ 100
STX). Fee: 5%.
**Hash gate — critical:** The singleton verifies that each token's source is
byte-identical to the on-chain template. Always fetch the template source
verbatim from the Launkr API or the Hiro API — never modify it, even
whitespace. Any change breaks the singleton's hash-based allowlist check
(`ERR_TOKEN_NOT_OURS u201`).
---
## Protocol Info
Get contract IDs, floors, and fee schedule:
```
GET https://launkr.io/api/protocol?network=mainnet
GET https://launkr.io/api/protocol?network=testnet
```
**Mainnet contracts** (redeployed 2026-07-16, dropping an earlier `-mn-demo`
staging suffix — verified live on-chain, see worked examples below):
- Singleton: `SP2ABWV7JE5SFV1A1BDS8HARP2QY7QRPGC9Z367PM.lp-singleton-v6`
- Template: `SP2ABWV7JE5SFV1A1BDS8HARP2QY7QRPGC9Z367PM.restricted-token-template-v6`
- Trait: `SP2ABWV7JE5SFV1A1BDS8HARP2QY7QRPGC9Z367PM.restricted-ft-trait-v6`
**Testnet contracts:**
- Singleton: `ST2ABWV7JE5SFV1A1BDS8HARP2QY7QRPGC9KJJYWE.lp-singleton-v6`
- Template: `ST2ABWV7JE5SFV1A1BDS8HARP2QY7QRPGC9KJJYWE.restricted-token-template-v6`
- Trait: `ST2ABWV7JE5SFV1A1BDS8HARP2QY7QRPGC9KJJYWE.restricted-ft-trait-v6`
**How `launkr.ts` resolves these — read this before assuming "always fetch
fresh" means "always trust what comes back."** Every subcommand fetches
`GET /api/protocol` at runtime *and* compares the result against the two
addresses pinned in `NET_CONFIG` at the top of `launkr.ts`. If they agree,
the live values are used. If they differ, the command **refuses to run** and
prints both.
That refusal is deliberate, and it exists because the obvious version of
this — "fetch live, fall back to the pinned copy" — quietly destroys the
deploy verification described in section 4. `launch` byte-compares the API's
`clarityCode` against the on-chain source of the *template address*; if that
address also comes from the API, both sides of the comparison have the same
origin, and the check can only catch launkr.io disagreeing with itself
rather than a compromised or hijacked launkr.io, which is the threat it was
written for. The same applies to the singleton: a substituted address
collects a `--mode direct` STX seed under an `eq` post-condition that
authorizes the transfer perfectly happily.
So the two sources check each other. A genuine redeploy shows up as a clear
refusal naming both addresses — verify the new ones independently on the
explorer and update `NET_CONFIG`. `--allow-config-change` overrides the
refusal for a single invocation and is available on every subcommand, but it
puts full trust in the API response for that run: use it only after
confirming the redeploy on-chain yourself.
`get-pool`, `quote-buy`, and `quote-sell` are pure reads and take an
explicit `--network mainnet|testnet` flag. `launch`, `swap-buy`, and
`swap-sell` do **not** — they always follow whichever wallet is currently
loaded (set by the `NETWORK` env var at wallet-creation time), because
that's what actually determines the broadcast target no matter what a flag
says. An earlier version of this skill had a `--network` flag on every
command, including the three that sign transactions — it silently had no
effect on where the transaction was actually sent, since `callContract`/
`deployContract` read the network from the account, not from any
parameter. Confirm which wallet/network is active before running a write
command rather than expecting a flag to control it.
## Fees
| Mode | Treasury | Protocol | Total |
|---|---|---|---|
| Bonding | 0.90% | 0.10% | 1.00% |
| Direct / Graduated | 4.50% | 0.50% | 5.00% |
| swap-and-burn | — | — | 0% (graduated pools only) |
## Protocol Floors
| Parameter | Minimum | Maximum |
|-----------|---------|---------|
| supply (atomic units) | `100000000000000` | `1000000000000000000000` |
| decimals | — | `18` |
| stxSeed (direct mode) | `100000000` (100 STX) | — |
| virtualStx (bonding mode) | `500000000` (500 STX) | — |
| graduationThreshold | `2000000000` (2000 STX) | `10000000000000` (10M STX) |
| graduationThreshold | — | 10× virtualStx |
---
## 1. Launch a token
Two sequential on-chain transactions. `POST /api/launch` returns unsigned
Clarity source + call params for both — you sign and broadcast each yourself
with your own key. **Do not send step 2 until step 1 confirms on-chain.**
```
POST https://launkr.io/api/launch
{
"network": "mainnet",
"deployerAddress": "<your STX address>",
"name": "My Agent Token",
"symbol": "MAT",
"supply": "1000000000000000", // atomic units, 6 decimals — e.g. this = 1B tokens
"mode": "bonding",
"virtualStx": "500000000", // uSTX, min 500000000 (500 STX)
"graduationThreshold": "2000000000",// uSTX, min 2000000000 (2000 STX)
"feeReceiver": "<STX address that gets 90% of swap fees>"
}
```
> **Gotcha:** `virtualStx` / `graduationThreshold` (bonding) or `stxSeed`
> (direct) must be **top-level** fields in this POST body, NOT nested under
> a `bondingMode`/`directMode` object — even though the `GET /api/protocol`
> response's own example schema shows them nested. Nesting them silently
> fails validation with `"virtualStx must be >= 500000000 uSTX"` even when
> the value is valid. `launkr.ts` already builds the flat body correctly.
Response gives you two steps:
1. **contract-deploy** — the token's Clarity source (byte-frozen, don't edit
it — see the hash-gate warning above).
2. **contract-call** `create-pool-bonding` (or `create-pool-direct`) on the
singleton — only send after step 1 confirms. Poll
`GET https://api.hiro.so/extended/v1/tx/{txid}` until `tx_status: "success"`.
`create-pool-bonding` args, in order:
`token` (trait_reference/principal) · `name` (string-ascii 32) · `symbol`
(string-ascii 32) · `decimals` (uint) · `supply` (uint) · `uri` (optional
string-utf8 256) · `virtual-stx` (uint) · `graduation-threshold` (uint) ·
`fee-receiver` (principal)
**`decimals` is not a property of the template.** This is the arg most
likely to be assumed away. The byte-frozen template ships
`(define-data-var token-decimals uint u0)`, and the *only* thing that ever
sets it is the token's one-shot `initialize()`, which the singleton calls
during pool creation using this very argument. A token that is deployed but
has no pool yet reports `get-decimals` → `u0`, because it has not been
initialized. `launkr.ts` therefore takes `--decimals` explicitly (default
`6`, matching the `tokenDecimals` constant `/api/protocol` reports), checks
it against the API's arg like every other parameter, and never reads it back
off an uninitialized token. Getting this wrong is permanent — `initialize()`
runs once and then answers `ERR_ALREADY_INITIALIZED` forever.
`create-pool-direct` is the same shape with `stx-seed` (uint) replacing
`virtual-stx`/`graduation-threshold`, and **requires a matching STX
post-condition** (`eq`, amount = stxSeed) since it pulls real STX from the
caller at creation. Bonding mode pulls no STX at creation — its
post-conditions array is empty.
> **Resolved (2026-08-05, corrected 2026-08-17):** an earlier version of
> `launkr.ts` sent an explicit `Some("")` instead of `none` for an omitted
> `uri`, working around a `BadFunctionArgument` broadcast rejection. That
> rejection turned out to be specific to a *different* environment (the
> published `@aibtc/mcp-server` npm package's own dependency resolution)
> rather than a Stacks/Clarity issue — a bare `noneCV()` broadcasts and
> confirms fine against this repo's own pinned `@stacks/transactions@7.3.1`.
> (A prior version of this note cited two testnet txids for this that
> don't exist on-chain — written before the test was actually run, an
> error rather than a stale reference. Verified for real afterward: mainnet
> txid `29b7e58d636d2be118ca658707220e3f5ff19100fbb264f5aeb00c765202e390`,
> `(ok true)`, calling `set-token-uri` with a bare `noneCV()` signed with
> this exact pinned dependency version.) `parseLaunkrArg` sends a proper
> `none` again — a token launched without `--uri` correctly has `none`
> on-chain, not an empty string.
## 2. Quote a trade (free, read-only, no gas)
```
POST https://api.hiro.so/v2/contracts/call-read/<singleton-address>/<singleton-name>/quote-buy
{ "sender": "<any-stx-address>", "arguments": ["<hex token principal>", "<hex uint stx-in-ustx>"] }
```
Same shape for `quote-sell` (tokens-in → stx-out) and `quote-swap-and-burn`
(graduated pools only). Returns `none` if the pool doesn't exist or the
input is zero — always check for `none` before using the result.
Also useful: `get-pool` (full pool state: reserves, mode, graduation
progress, fee-receiver) and `is-paused` (protocol-wide kill switch).
## 3. Swap (buy / sell)
Direct contract calls on the singleton — no API round-trip needed once you
have a quote.
**Buy:** `swap-exact-stx-for-tokens(token, stx-in, min-tokens-out, deadline, recipient)`
**Sell:** `swap-exact-tokens-for-stx(token, tokens-in, min-stx-out, deadline, recipient)`
— every Launkr token uses the **identical internal FT asset name
`strategy-token`**, regardless of display name/symbol (verified against the
deployed byte-frozen template source, both networks — only the contract
address varies).
> **Gotcha — fixed in `launkr.ts`, verified on-chain on both testnet and
> mainnet:** under `PostConditionMode.Deny`, **every principal that moves
> an asset in the transaction needs a post-condition, not just the
> caller.** A buy has the singleton sending back both the FT payout *and*
> STX (the treasury + protocol fee legs); a sell has the singleton paying
> out STX (proceeds + both fee legs). Post-conditioning only the caller's
> own leg — which looks like the obviously-correct, safe thing to do —
> aborts with `abort_by_post_condition` every time, because the
> uncovered singleton-originated transfers get flagged. One
> post-condition per `(principal, asset)` pair covers the aggregate amount
> that principal sends of that asset across the whole transaction, so this
> doesn't need one condition per individual fee leg. The complete correct
> set:
> - **Buy** — caller `eq stx-in` (uSTX), singleton `gte 0` (uSTX, covers
> the fee legs — not meaningfully boundable since that's the contract's
> own fee math, not caller input), singleton `gte min-tokens-out` (FT,
> `strategy-token` — this is the real slippage guard).
> - **Sell** — caller `eq tokens-in` (FT, `strategy-token`), singleton
> `gte min-stx-out` (uSTX — covers proceeds + both fee legs in one
> aggregate check, and is itself the real slippage guard).
Always call `quote-buy`/`quote-sell` first and set `min-tokens-out` /
`min-stx-out` a few % under the quote as a slippage guard. Use
`deadline: 0xffffffff` (4294967295) if you don't need a block-height cutoff.
**Broadcast is not confirmation.** Every write subcommand waits for its
transaction to reach a terminal on-chain status before printing, and reports
`success: true, confirmed: true` only once it actually succeeded. This
matters most for swaps, which really do abort on slippage and post-condition
failures — an earlier version printed `success: true` the instant the txid
came back, which made "the trade happened", "the trade is pending" and "the
trade aborted" indistinguishable in the output. Pass `--no-wait` for
fire-and-forget behaviour; that output is labelled `broadcast: true,
confirmed: false` and carries a note telling you to poll the explorer.
**Kill switch.** The singleton has a protocol-wide pause that every write
checks on-chain first (`check-not-paused`). `bun run launkr/launkr.ts
is-paused` reads it for free, and `launch`, `create-pool`, both swaps and
`swap-and-burn` all pre-flight it so a paused protocol costs you nothing
instead of a transaction fee for a guaranteed abort.
**`swap-and-burn(token, stx-in, min-tokens-burned, deadline)`** — spends STX
to buy tokens and burns them in the same call. Fee-free (0%), and the STX
stays in the pool as protocol-owned liquidity. Graduated pools only —
`ERR_NOT_GRADUATED (u217)` on a bonding pool, and `quote-swap-and-burn`
returns `none` there. Note the signature has **no `recipient`**: nothing is
paid out. Post-conditions under Deny: caller `eq stx-in` (uSTX), plus
singleton `gte min-tokens-burned` (FT `strategy-token`) — the burn is an
outflow from the singleton's own balance, performed under
`(with-ft token-principal "strategy-token" dy)`, so Deny mode requires it
covered like any other send. There is no fee leg, so no STX leaves the
singleton.
## 4. Recovery, fee-receiver transfer, and other operations
**`create-pool`** — recovery path for when `launch` deployed the token but
the pool-creation step failed or was interrupted (`launch` is two separate
transactions with no atomicity between them). Takes `--token
<already-deployed-principal>` plus the same params `launch` would have
used, and runs only the pool-creation call — built directly from the
documented `create-pool-bonding`/`create-pool-direct` signature above
rather than round-tripping through `/api/launch` again (nothing in that
response for this step isn't already fully determined by your own input).
Re-running `launch` itself after a failed step 2 would deploy a *second*
token, not resume — use `create-pool` instead.
> `create-pool` takes `--decimals` (default `6`) for the reason given in
> section 1: on the exact state this command exists to recover from — token
> deployed, no pool — the token has not been initialized, so reading
> `get-decimals` off it returns `u0` rather than the truth. An earlier
> version did read it back, which would have permanently initialized the
> recovered token with 0 decimals. The command also refuses up front if the
> token reports a non-zero total supply, since that means `initialize()` has
> already run and pool creation could only abort with
> `ERR_ALREADY_INITIALIZED`.
**`set-fee-receiver` / `accept-fee-receiver`** — the singleton's two-step
fee-receiver transfer (`set-pending-fee-receiver` proposed by the current
receiver, `accept-fee-receiver` confirmed by the new one), exposed as CLI
subcommands. The fee-receiver collects 90% of swap volume permanently
once a pool is created — this is the only correction path if it was set
wrong.
**Deploy verification:** `launch` now fetches the real template source
on-chain (`GET /v2/contracts/source/...`) and byte-compares it against the
API's `clarityCode` *before* deploying, rather than trusting the API
response and finding out only after the deploy fee is spent that the
singleton would have rejected it (`ERR_TOKEN_NOT_OURS`) anyway.
**Pool-arg verification:** `validatePoolStepMatchesRequest` (see `launch`)
checks **all nine** args against what was requested — token principal, name,
symbol, decimals, supply, uri, the curve parameters
(`virtual-stx`/`graduation-threshold` for bonding, `stx-seed` for direct)
and fee-receiver — plus the exact arg count for the mode and the function
name itself. Every one of these is capable of coming back wrong from the
API, and each of the later additions closed a real hole: the curve
parameters are what the entire price curve is built from, `uri` and
`decimals` are both written permanently by the one-shot `initialize()`, and
the token principal decides which token the pool is even for. `launch` also
reports the principal derived from the address it actually deploys under
plus the contract name it actually deploys, not the `tokenPrincipal` the API
echoes back — a mismatch is a warning on stderr, not something the user is
silently handed.
**Does graduating a pool unlock token transfers?** Not applicable the way
this question originally assumed — see the correction below. Graduating
only changes the pool's `mode` field (bonding fee 1% → graduated fee 5%,
and enables `swap-and-burn`); it never touches the token contract's
allowlist, before or after graduation.
**Correction (2026-08-17):** an earlier version of this doc (and
View on GitHub