Skill: agentswapx/atxswap (version 0.0.31)

--- name: atxswap description: >- Manage ATX on BSC with wallet creation, price and balance queries, PancakeSwap V3 swaps, liquidity operations, LP positions and holdings, and BNB/ERC20 transfers. Use when the user mentions ATX, BSC, PancakeSwap V3, wallet creation, price checks, buying, selling, liquidity, fees, holdings, LP positions, or token transfers. version: "0.0.31" compatibility: Requires Node.js 18+ and npm. Network access to BSC RPC required. inject: - bash: echo "${CLAUDE_SKILL_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" as: SKILL_DIR metadata: author: agentswapx openclaw: requires: bins: - node - npm homepage: https://github.com/agentswapx/skills/tree/main/atxswap os: - linux - macos --- # ATXSwap Skill Execute ATX trading and wallet workflows on BSC. This skill is designed for agents that need safe, repeatable commands for wallet management, ATX/USDT quotes, swaps, V3 liquidity actions, and transfers. - **SDK**: [`atxswap-sdk`](https://www.npmjs.com/package/atxswap-sdk) on npm ([source](https://github.com/agentswapx/atxswap-sdk)) - **Docs (team / project)**: [Team introduction (EN)](https://docs.atxswap.com/guide/team) · [Team introduction (ZH)](https://docs.atxswap.com/zh/guide/team) - **Keystore dir**: `~/.config/atxswap/keystore` (fixed, not configurable) - **Secrets dir**: `~/.config/atxswap/` (master.key + secrets.json) ## Use This Skill For - Create the single wallet used by this skill instance (importing an existing private key is not supported) - Query ATX price, balances, LP positions (see **Required agent reply for holdings** under `query.js`), quotes, and arbitrary ERC20 token info - Buy or sell ATX against USDT on PancakeSwap V3 - Add liquidity (full range or a custom **price range in USDT per ATX** or **tick** bounds), remove liquidity, collect fees, or burn empty LP NFTs - Transfer BNB, ATX, USDT, or arbitrary ERC20 tokens ## Before First Use This skill ships its own Node scripts and depends on `atxswap-sdk`. 1. Open the skill directory where this `SKILL.md` is installed. 2. Run `npm install` there before using any script. 3. If `npm install` fails, stop and report the dependency error instead of guessing. If the skill is installed via ClawHub or OpenClaw CLI, the install location is typically `~/.clawhub/skills/atxswap/` (or the equivalent client-managed path). If you cloned this repository directly, the location is `skills/atxswap/`. ## Script Location Use the skill directory path to locate scripts. If `${SKILL_DIR}` is available (injected by skills.sh-compatible runtimes), use it; otherwise use the absolute path to this skill's installed directory. Example: ```bash cd skills/atxswap && npm install cd "${SKILL_DIR}" && node scripts/wallet.js list ``` All examples below use `cd "${SKILL_DIR}" &&` for clarity. If your runtime does not inject `${SKILL_DIR}`, replace it with the absolute path of the installed skill directory. ## Runtime Notes - `BSC_RPC_URL` is optional and supports comma-separated values for fallback, e.g. `BSC_RPC_URL="https://primary,https://backup1,https://backup2"`. When unset, scripts use a built-in fallback list of 6 BSC public RPC endpoints and viem will retry them in order. - Wallet files live under `~/.config/atxswap/keystore`. - Secure secrets live under `~/.config/atxswap/` (master.key + secrets.json). - Only **one wallet** is allowed per skill installation. If a wallet already exists, `wallet.js create` fails. - Use `wallet.js list` before creating a wallet. - Importing an existing private key via this skill is **not supported**. If the user asks to import a private key, refuse and tell them to use a dedicated wallet tool of their choice. - Scripts write JSON output. `wallet.js export` prints the address's encrypted **MetaMask-compatible keystore V3 JSON** to stdout (or writes it to a file via `--out <file>`); it never prints the raw private key. - `query.js quote` can return a JSON error if the configured Quoter or RPC rejects the simulation. Surface the error and do not proceed to a write. - For balance, holdings, LP position, or pending-fee questions, always fetch fresh on-chain data at request time. Do **NOT** answer from prior chat output, memory, cached numbers, or earlier command results unless you rerun the relevant query first. - For custom-range liquidity, do **not** guess the second token amount from chat. First run `liquidity.js quote-add` or use `liquidity.js add --base-token ... --amount ...` so the script computes the counter-asset from the live pool price and range. ## Password Rules When the user asks to **create** a wallet: 1. Ask the user for a password first (do NOT generate one). 2. Pass it via `--password <pwd>` to the script when running non-interactively. 3. The password is auto-saved to secure storage after creation. 4. Never print the password back to the chat. 5. After the wallet is created, export and send the encrypted keystore backup to the user who requested the wallet. 6. Clearly label it as encrypted keystore backup material, not the raw private key. 7. Do not upload it to any website or send it to any third party. For **swap**, **transfer**, and **liquidity** operations, rely on auto-unlock first. Only ask for the password if auto-unlock fails. If the user says they forgot the wallet password or asks to recover it, first explain that saved wallet passwords are encrypted at rest in the local SecretStore (for example Keychain, Secret Service, or the file backend under `~/.config/atxswap/`) and are not stored by the agent in chat memory. Even if the user confirms, do **not** print the password in chat; guide them to use a trusted local workflow instead. ## Hard Safety Rules 1. Treat all BSC writes as real-asset operations. 2. **NEVER** output private keys or passwords in chat. 3. **ALWAYS** run a preview before write actions: query price, quote, balance, or positions as appropriate. 4. **ALWAYS** show the preview to the user and wait for explicit confirmation before swap, transfer, or liquidity writes. 5. **NEVER** execute large trades without the user saying "yes" or "confirm". 6. `wallet.js export` only emits the **encrypted MetaMask-compatible keystore JSON**, never the raw private key. There is no command that prints the unencrypted private key, and the agent must not attempt to derive or display one. 7. Prefer `wallet.js export <address> --out <file>` and tell the user the file path. Avoid pasting the keystore JSON itself into chat unless the user explicitly asks for it. 8. Before deleting a wallet, keystore file, or any private-key-bearing wallet material, **ALWAYS** remind the user to export and back up the encrypted keystore first. Do not delete anything until the user explicitly confirms that the keystore backup has been completed. 9. Wallet deletion requires a second explicit confirmation: after backup is confirmed, require the user to send the exact phrase `force delete wallet` before running any delete command. 10. If the user asks to delete a wallet, do **NOT** send the keystore immediately. First ask whether they want to receive the encrypted keystore backup. Only after the user agrees may you export and send the keystore to the user. 11. If the user explicitly asks to back up or export the wallet, export and send the encrypted keystore backup to the user who requested the wallet, and clearly label it as keystore backup material. 12. The encrypted keystore may only be sent to the user who owns the current session request. **NEVER** send the keystore through any channel that is not under that user's own control. It may only be sent to the user personally, and must not be pasted into any external form or sent to any other person, group, agent, or service. 13. After `wallet.js create` succeeds, export and send the encrypted keystore to the user who requested the wallet. Treat this as part of the wallet creation handoff, but only to that user. 14. If the user asks to recover or reveal a saved wallet password, remind them that the password is encrypted in local secure storage and must not be disclosed in chat. Do not attempt to print, derive, or expose the password even after user confirmation. 15. If the user asks to recover, reveal, print, or paste the wallet private key, refuse. Offer `wallet.js export <address> --out <file>` as the only supported backup path, because it exports an encrypted keystore instead of exposing the raw private key. 16. For every transfer, treat the tuple `(asset, from, to, amount)` as a single transfer intent. Repeat that exact tuple back to the user before execution. 17. After a transfer command returns a `txHash`, consider that transfer intent **already sent**. Do **NOT** send the same transfer again unless the user explicitly asks to send it again. 18. If a transfer write ends in an ambiguous state (for example RPC timeout, dropped connection, or partial output after signing/submission), do **NOT** retry blindly. First check chain state or wallet state, summarize what is known, and ask the user whether to retry. 19. If the same transfer tuple appears again in the same conversation after a successful or ambiguous prior attempt, pause and ask whether the user means a new transfer or is referring to the earlier one. 20. For any user question about **current** balances, holdings, LP positions, pending fees, or wallet assets, rerun the matching read command against the chain first. Never rely on previously displayed numbers as if they were still current. ## Required Preview Flow Before every write action: 1. Query the price, quote, balance, or positions that match the requested action. 2. Summarize the preview in plain language. 3. Ask the user to confirm. 4. Execute the write command only after confirmation. 5. Return the transaction hash and the key result fields. For transfers, the summary in step 2 must explicitly include: - asset - source wallet - destination address - amount After step 5, if a `txHash` is available, treat the transfer as executed and do not issue the same write again unless the user clearly requests a second send. For read-only asset questions: 1. Run the relevant live query first (`query.js balance`, `query.js positions`, `query.js price`, or `query.js quote` as appropriate). 2. Answer from that fresh output only. 3. If the RPC or query fails, say the data could not be refreshed instead of reusing older numbers. ## High-Value Workflows ### Check market state ```bash cd "${SKILL_DIR}" && node scripts/query.js price cd "${SKILL_DIR}" && node scripts/query.js balance <address> cd "${SKILL_DIR}" && node scripts/query.js positions <address> cd "${SKILL_DIR}" && node scripts/query.js positions <address> <tokenId> ``` ### Preview before swap ```bash cd "${SKILL_DIR}" && node scripts/query.js quote <buy|sell> <amount> ``` ### Execute after confirmation ```bash cd "${SKILL_DIR}" && node scripts/swap.js buy <usdtAmount> [--from address] [--slippage bps] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/liquidity.js add <atxAmount> <usdtAmount> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/liquidity.js add --base-token <atx|usdt> --amount <n> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/transfer.js atx <to> <amount> [--from address] [--password <pwd>] ``` ## Command Reference ### `wallet.js` ```bash cd "${SKILL_DIR}" && node scripts/wallet.js create [name] --password <pwd> cd "${SKILL_DIR}" && node scripts/wallet.js list cd "${SKILL_DIR}" && node scripts/wallet.js export <address> [--out <file>] cd "${SKILL_DIR}" && node scripts/wallet.js has-password <address> cd "${SKILL_DIR}" && node scripts/wallet.js forget-password <address> cd "${SKILL_DIR}" && node scripts/wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet" ``` After `wallet.js create`: 1. Return the created wallet address. 2. Run `wallet.js export <address> [--out <file>]`. 3. Send the encrypted keystore only to the user who requested the wallet. 4. Do not upload it to any website or send it to any third party. 5. Explain that it is encrypted keystore backup material, not the raw private key. Before `wallet.js delete`: 1. Ask whether the user wants the encrypted keystore sent to them for backup. 2. After the user agrees, export and send the encrypted keystore to the user only. 3. Do not upload it to any website or send it to any third party. 4. Require the user to explicitly confirm that the backup is complete. 5. Require the user to send the exact phrase `force delete wallet`. 6. Only then run `wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet"`. If the user asks to back up the wallet: 1. Run `wallet.js export <address> [--out <file>]`. 2. Send the encrypted keystore only to the user. 3. Do not upload it to any website or send it to any third party. 4. Explain that this is encrypted keystore backup material, not the raw private key. ### `query.js` ```bash cd "${SKILL_DIR}" && node scripts/query.js price cd "${SKILL_DIR}" && node scripts/query.js balance <address> cd "${SKILL_DIR}" && node scripts/query.js quote <buy|sell> <amount> cd "${SKILL_DIR}" && node scripts/query.js positions <address> cd "${SKILL_DIR}" && node scripts/query.js positions <address> <tokenId> cd "${SKILL_DIR}" && node scripts/query.js token-info <tokenAddress> ``` `query.js positions` includes **principal token amounts** in the position (`principalAtx`, `principalUsdt`, `principal0`, `principal1`) computed from V3 `liquidity` (L), ticks (internal), and the pool’s current `sqrtPriceX96` (same `getAmountsForLiquidity` math as the web app). It emits human **USDT-per-ATX** bounds as **`priceRangeUsdtPerAtx.min` / `.max`**, **`currentPriceUsdtPerAtx`**, and **`currentPriceInRange`** (whether the pool tick lies inside that position). It includes **`pendingFees.atx`** and **`pendingFees.usdt`** as the simulated collect notionals (`collectable*`), plus raw `tokensOwed0`/`1` and `collectable0`/`1` for debugging. Use `collectable*` / `pendingFees` to decide whether a fee harvest is worth executing. Each object also includes **`feePercent`** (e.g. `"0.25%"`) for the pool’s swap-fee tier; raw **`fee`** stays the on-chain code (`100` / `500` / `2500` / `10000`). **Tick indices are not included** in the JSON — quote USDT/ATX prices and in-range state instead. When the user asks "how much do I have now", "what is my current balance", "what positions are left", "how much ATX is still in the LP", or similar present-tense questions, rerun `query.js balance` and/or `query.js positions` immediately. Do not answer from previously captured JSON. **Required agent reply for holdings** when the user asks about their positions, LP NFTs, or liquidity holdings (per position): Run `query.js positions <address>` (omit `tokenId` to list all ATX/USDT V3 NFTs). The CLI prints **one JSON object per NFT**; include **every** position. For each position, the answer **must** address the topics below (label them in the user’s language when replying). **Do not** show raw V3 tick numbers to the user — use **USDT per 1 ATX** from the JSON below. | Topic | What to include | CLI JSON fields | |-------|-----------------|-----------------| | **Tokens in the position** | **In-range liquidity** as ATX and USDT notionals — always cite **`principalAtx`** and **`principalUsdt`** (and optionally `principal0` / `principal1` in pool token0/token1 order). These are computed at the **current pool price**. Mention `liquidity` only as the raw **L** scalar if explaining detail. Do **not** treat **`query.js balance`** as LP “position tokens”: wallet ATX/USDT/BNB balances are unrelated to NFT principal — if shown, label them distinctly (e.g. “Wallet balances, separate from this LP NFT”). | | **NFT token ID** | The V3 LP NFT id | `tokenId` | | **Pool swap fee tier** | The pool’s trading fee as a **percentage** for end users | Prefer **`feePercent`** (e.g. `"0.25%"`). If you show the raw tier code, pair it with **`feePercent`** (e.g. `2500` + `0.25%`). | | **Price range & spot** | Configured **min/max USDT per 1 ATX** for the position, and current pool price in the same unit | **`priceRangeUsdtPerAtx.min`**, **`priceRangeUsdtPerAtx.max`**, **`currentPriceUsdtPerAtx`**, **`currentPriceInRange`** (confirm “in range” / “out of range” in natural language). | | **Pending fees** | Uncollected fees (both tokens) | Always show **`pendingFees.atx`** and **`pendingFees.usdt`** explicitly. Prefer these over quoting only one asset. Optionally reference `collectableAtx`/`collectableUsdt` synonyms. State fees stay **pending** until `liquidity.js collect`. | Do not answer with only raw pool indices (ticks). If there are no positions, relay `No ATX/USDT positions found.` exactly. Minor mismatch vs on-chain bookkeeping can occur because `principal*` uses the same float-based tick→√P path as other tooling (~wei-level); values are intended for humans and routing, not audits. ### `swap.js` ```bash cd "${SKILL_DIR}" && node scripts/swap.js buy <usdtAmount> [--from address] [--slippage bps] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/swap.js sell <atxAmount> [--from address] [--slippage bps] [--password <pwd>] ``` ### `liquidity.js` **`add` — price / tick range** (same USDT/ATX semantics as the web app; default full range = full-width liquidity): - Default: no extra flags means **full range** (same as before). - `--full-range`: explicit full range (do not combine with the two groups below). - `--min-price` / `--max-price`: band in **USDT per 1 ATX**; the script reads pool `token0` and maps to `tickLower` / `tickUpper` like the app (`token1/token0` + `tickSpacing`). **Both** prices are required. - `--range-percent`: band around **current ATX price** as a percentage; e.g. `20` means about `-20%` to `+20%` of the current price. - `--tick-lower` / `--tick-upper`: raw V3 ticks (**both** required; script uses the smaller as lower, larger as upper, clamped to valid V3 bounds). - `quote-add <atx|usdt> <amount>`: given live price and range, estimate the other leg — use before a write. - `add --base-token <atx|usdt> --amount <n>`: single-sided notional; script computes the other leg and executes the add. Optional: `--slippage-bps` (0–10000; default from SDK). ```bash cd "${SKILL_DIR}" && node scripts/liquidity.js quote-add <atx|usdt> <amount> [range opts] cd "${SKILL_DIR}" && node scripts/liquidity.js add <atxAmount> <usdtAmount> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/liquidity.js add --base-token <atx|usdt> --amount <n> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/liquidity.js remove <tokenId> <percent> [--from address] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/liquidity.js collect <tokenId> [--from address] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/liquidity.js burn <tokenId> [--from address] [--password <pwd>] ``` `liquidity.js remove` already uses a single `multicall`: `decreaseLiquidity` -> `collect` -> and when `percent = 100`, `burn`. That means a full removal automatically collects withdrawable funds and destroys the LP NFT in the same transaction, so running `collect` again for the same `tokenId` is expected to fail because the position no longer exists. Before `collect`, preview the target position with: ```bash cd "${SKILL_DIR}" && node scripts/query.js positions <address> <tokenId> ``` Prefer `pendingFees` / `collectableAtx` / `collectableUsdt` over relying on `tokensOwed0/1` alone when deciding whether fees are available, because the raw `tokensOwed` fields may stay at zero while `collect()` can still succeed. Example (not full-range; align the range with the user using `query.js price` before writes): ```bash cd "${SKILL_DIR}" && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20 cd "${SKILL_DIR}" && node scripts/liquidity.js add --base-token usdt --amount 0.1 --range-percent 20 --from <address> cd "${SKILL_DIR}" && node scripts/liquidity.js add 10 1 --min-price 0.05 --max-price 0.15 cd "${SKILL_DIR}" && node scripts/liquidity.js add 10 1 --tick-lower -20000 --tick-upper 1000 ``` Mapping user phrasing to commands: - If the user asks to add **0.1 USDT** of liquidity with a **±20%** range around spot, first run `quote-add usdt 0.1 --range-percent 20`. - Show the returned `estimatedAmounts` (or equivalent summary) and wait for confirmation. - After confirmation, run `add --base-token usdt --amount 0.1 --range-percent 20 --from <address>`. ### `transfer.js` ```bash cd "${SKILL_DIR}" && node scripts/transfer.js bnb <to> <amount> [--from address] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/transfer.js atx <to> <amount> [--from address] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/transfer.js usdt <to> <amount> [--from address] [--password <pwd>] cd "${SKILL_DIR}" && node scripts/transfer.js token <tokenAddress> <to> <amount> [--from address] [--password <pwd>] ``` Transfer anti-duplication checklist: 1. Preview balances first. 2. Restate the exact transfer tuple `(asset, from, to, amount)` to the user. 3. Wait for explicit confirmation for that exact tuple. 4. Run `transfer.js` once. 5. Return the `txHash`. 6. If the user repeats the same request, do not run the transfer again until you clarify whether they want a second transfer. 7. If the command may have submitted but the final result is unclear, do not rerun it automatically; inspect chain state or ask the user how to proceed. ## When To Refuse Or Pause - Missing wallet but the user requests a write action - Missing confirmation for swap, transfer, or liquidity writes - User asks to delete a wallet, keystore file, or private-key-bearing wallet material before confirming that the encrypted keystore has been backed up - User asks to delete a wallet but has not agreed to receive the keystore backup first - User asks to delete a wallet but has not explicitly sent `force delete wallet` - User asks to send or upload a keystore through a channel not under their own control, or to anyone other than the user - User asks to recover or reveal a saved wallet password in chat - User asks to recover, reveal, print, or paste a wallet private key in chat - `npm install` has not been run successfully in the skill directory - RPC, dependency, or wallet-unlock errors that make the state unclear ## Standard Workflow For any write action: 1. Query current price, quote, balance, or positions as needed. 2. Summarize the preview for the user. 3. Wait for explicit confirmation. 4. Execute the write command. 5. Report the transaction hash and result.

path: CHANGELOG.md
content: # Changelog

## 0.0.31

- ClawHub republish; registry tarball unchanged (`atxswap-sdk` **`^0.0.15`**). `SKILL.md` / `README*` — require fresh on-chain reads for balances/positions; transfer intent recap, no duplicate sends after `txHash`, no blind retries on ambiguous RPC/signing.; `frontend`/`docs-site` `llms.txt` reference **`0.0.31`**.

## 0.0.30

- ClawHub republish; registry tarball unchanged (`atxswap-sdk` **`^0.0.15`**). Docs: `llms.txt` updated to reference latest **`0.0.30`**.

## 0.0.29

- ClawHub publish: registry tarball bundles `atxswap-sdk` **`^0.0.15`** (npm **`0.0.15`**).
- `query.js positions`: emits **`priceRangeUsdtPerAtx`**, **`currentPriceUsdtPerAtx`**, **`currentPriceInRange`**, **`pendingFees`** `{ atx, usdt }`; **`tickLower`/`tickUpper`** removed from printed JSON. `SKILL.md` / `README*` updated.

## 0.0.28

- ClawHub publish: registry tarball bundles `atxswap-sdk` **`^0.0.15`** (npm **`0.0.15`**).
- `query.js positions`: add **`feePercent`** (human-readable pool swap fee, e.g. `"0.25%"`; raw **`fee`** remains the on-chain tier code such as `2500`). `SKILL.md` instructs agents to prefer **`feePercent`** in user-facing summaries.

## 0.0.27

- ClawHub publish: registry tarball bundles `atxswap-sdk` **`^0.0.15`** (npm **`0.0.15`**); includes **`principalAtx`/`principalUsdt`** in `query.js positions`, `_v3math.js`, and updated `SKILL.md`/`README*` for LP-vs-wallet semantics.

## 0.0.26

- `query.js positions` outputs **`principalAtx`**, **`principalUsdt`**, `principal0`/`principal1`: in-range token amounts computed from `liquidity` + ticks + spot `sqrtPriceX96` (`getAmountsForLiquidity`), using SDK-provided `principal*` when present (future `atxswap-sdk` versions) to avoid an extra `getPrice()` call. `SKILL.md` updated: distinguish LP principal from **`query.js balance`** (wallet); warns agents not to substitute wallet balances for position tokens; table row fully English. `README.md` / `README.zh.md` fee section documents `principal*` vs `collectable*`.

## 0.0.25

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14). `SKILL.md` is English-only; documents required agent reply for LP holdings (tokens, NFT id, price range, pending fees); `liquidity.js` narrative translated to English.

## 0.0.24

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14); skill scripts use shared `jsonStringify` for BigInt-safe JSON output.

## 0.0.23

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14); `SKILL.md` hard-safety keystore channel wording (English).

## 0.0.22

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14).

## 0.0.21

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14).

## 0.0.20

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14).

## 0.0.19

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14).

## 0.0.18

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.13` (npm 0.0.13).

## 0.0.17

- ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.12` (npm 0.0.12).

## 0.0.16

- ClawHub registry publish (no functional change from 0.0.15 bundle).

## 0.0.14

- Bumped bundled `atxswap-sdk` to `^0.0.11` (BSC `DEFAULT_RPC_URLS`: 6 endpoints, `bsc-dataseed.bnbchain.org` first).

## 0.0.13

- Bumped bundled `atxswap-sdk` to `^0.0.10` (default slippage when omitted is now **1%** / `100` bps, was 3%).

## 0.0.12

- Bumped bundled `atxswap-sdk` to `^0.0.9` (npm README / docs links).
- `README` / `README.zh` / `SKILL.md`: link to ATXSwap documentation [team introduction](https://docs.atxswap.com/guide/team) pages.

## 0.0.11

- Bumped bundled `atxswap-sdk` to `^0.0.8` (npm maintenance release).

## 0.0.9

- Bumped `atxswap-sdk` to `^0.0.7` so `wallet.js export` emits
  MetaMask-compatible encrypted keystore V3 JSON. SDK 0.0.7 uses the standard
  Web3 Secret Storage MAC (`keccak256`) and can re-export legacy SDK keystores
  without exposing raw private keys.

## 0.0.8

- Tightened `SKILL.md` safety guidance: before deleting a wallet/keystore, remind
  the user to back up the encrypted keystore and wait for explicit confirmation
  of backup. On forgotten-password and recovery requests, explain local
  encrypted storage and **never** print passwords in chat. Refuse to reveal
  or paste private keys; point users to `wallet.js export` (encrypted
  keystore) only.

## 0.0.7

- Bumped the bundled `atxswap-sdk` dependency to `^0.0.6` so the skill uses the
  cron/headless SecretStore fixes from SDK 0.0.6. Cron, SSH, and other
  non-desktop Linux environments now fall back to the encrypted file backend
  instead of failing on `secret-tool store`.
- `wallet.js create` now includes `passwordSaved` and optional
  `passwordSaveError` in its JSON output, making password persistence failures
  visible without treating them as wallet-creation failures.

## 0.0.6

- Bumped the bundled `atxswap-sdk` dependency to `^0.0.5` so the skill picks up
  the corrected `DEFAULT_CONTRACTS` (production ATX token + ATX/USDT pool
  addresses on BSC mainnet). Required because npm semver treats `^0.0.x` as
  pinned to that exact patch, so older dependency ranges would never have
  resolved to `0.0.5`.
- Version `0.0.5` was intentionally skipped to keep the skill release line
  distinct from the SDK release line going forward.

## 0.0.4

- Bumped the bundled `atxswap-sdk` dependency to `^0.0.3` (required for the new
  `WalletManager.exportKeystore()` API used by `wallet.js export`).
- Removed `wallet.js import <privateKey>` and the public
  `WalletManager.importPrivateKey()` SDK method. Importing an existing private
  key is no longer supported through this skill or the underlying SDK; the only
  way to provision a wallet for this skill instance is `wallet.js create`.
- Replaced `wallet.js export <address>` raw private-key output with **keystore
  V3 JSON** export. The `WalletManager.exportPrivateKey()` SDK method has been
  removed and superseded by `WalletManager.exportKeystore(address)`, which
  returns the on-disk encrypted keystore. `wallet.js export` now also supports
  `--out <file>` to write the keystore to disk instead of printing to stdout.
  The skill no longer has any path that exposes the unencrypted private key.

## 0.0.1

- Initial OpenClaw and ClawHub skill bundle for ATX trading on BSC
- Added self-contained scripts for wallet, query, swap, liquidity, and transfer flows
- Added OpenClaw-oriented `SKILL.md`, publish notes, and localized README files
- Normalized runtime failures to compact JSON errors for cleaner agent output


path: PUBLISH.md
content: # Publish Notes

This directory is published to **ClawHub** as a single skill bundle that also
works as a standalone `skills.sh`-compatible package (Claude / Cursor / Codex
CLI). The same `SKILL.md` frontmatter declares both conventions, so a single
source of truth covers all clients. After publish, the skill is installable via
both `clawhub install atxswap` and `openclaw skills install atxswap` (OpenClaw
pulls from the same ClawHub registry).

> Heads up: the OpenClaw CLI itself does **not** have `skills publish` or
> `skills validate` subcommands. All publishing flows through the dedicated
> `clawhub` CLI (`npm install -g clawhub`).

## Pre-publish checklist

1. Bump versions consistently:
   - `SKILL.md` frontmatter `version`
   - `package.json` `version`
   - These two MUST match — `clawhub publish --version` overrides them at
     upload time but mismatched local values confuse `skills.sh` consumers.
2. Install dependencies inside this directory (`npm install`) so the SDK
   builds cleanly.
3. Run the local read-only checks:
   - `node scripts/wallet.js list`
   - `node scripts/query.js price`
   - `node scripts/query.js quote buy 1`
4. Confirm the folder does not include secrets, keystore files, or
   `node_modules/`. (`.clawhubignore` already excludes `node_modules/`,
   `.clawhub/`, `.clawdhub/`, `.DS_Store`, `*.log`.)
5. Make sure the parent submodule (`agentswapx/skills`) is committed and
   pushed — the published `homepage` URL points at GitHub.

## Authenticate with ClawHub

```bash
# Browser flow (opens a token-grant page)
clawhub login

# Or token flow (no browser)
clawhub login --token <YOUR_TOKEN> --no-browser

# Verify
clawhub whoami
```

## Publish

```bash
clawhub publish ./skills/atxswap \
  --slug atxswap \
  --name "ATXSwap" \
  --version 0.0.1 \
  --tags latest,atxswap,atx,bsc,trading
```

After upload there is a brief security-scan window during which `clawhub
inspect atxswap` returns "Skill is hidden while security scan is pending".

Once the scan completes, ClawHub will flag this skill as "suspicious" because
VirusTotal Code Insight detects crypto-key / external-API patterns — that is
expected for any wallet SDK and not a real warning. Users must pass `--force`
in non-interactive contexts:

```bash
clawhub install atxswap --force
```

## Verify the published skill

```bash
# Registry round-trip
clawhub inspect atxswap

# End-user simulation in a clean directory
TEST=$(mktemp -d)
cd "$TEST" && clawhub install atxswap --force
cd skills/atxswap && npm install   # pulls atxswap-sdk from npm (~15s)
node scripts/query.js              # should print usage
```

## Suggested changelog

```text
Initial ClawHub release for ATX wallet, query, swap, liquidity, and transfer
workflows on BSC. Compatible with both ClawHub/OpenClaw clients and the
standalone skills.sh runtime.
```


path: README.md
content: # ATXSwap Skill

A skill bundle for the ATXSwap decentralized agent exchange protocol on BSC. A single `SKILL.md`
works across clients represented by **Claude Code** and **OpenClaw**, so you do
not need separate directories for different clients.

[**中文文档**](./README.zh.md)

- **GitHub**: https://github.com/agentswapx/skills
- **SDK on npm**: [`atxswap-sdk`](https://www.npmjs.com/package/atxswap-sdk)
- **SDK source / docs**: [agentswapx/atxswap-sdk](https://github.com/agentswapx/atxswap-sdk)

For project background and a short [team introduction](https://docs.atxswap.com/guide/team) ([中文](https://docs.atxswap.com/zh/guide/team)), see the ATXSwap documentation site. This README describes the skill’s scope and scripts.

## What This Skill Covers

- Create the single wallet used by the skill (importing an existing private key is not supported)
- Query ATX price, balances, LP positions, and ERC20 token info
- Buy or sell ATX against USDT on PancakeSwap V3
- Preview custom-range liquidity, add liquidity, remove liquidity, collect fees, and burn empty LP NFTs
- Transfer BNB, ATX, USDT, or arbitrary ERC20 tokens

## Directory Layout

```text
atxswap/
├── SKILL.md
├── README.md
├── README.zh.md
├── PUBLISH.md
├── CHANGELOG.md
├── .clawhubignore
├── .gitignore
├── package.json
└── scripts/
    ├── _helpers.js
    ├── wallet.js
    ├── query.js
    ├── swap.js
    ├── liquidity.js
    └── transfer.js
```

## Install

### OpenClaw Install

```bash
openclaw skills install atxswap
```

### Claude Code Install

```bash
git clone https://github.com/agentswapx/skills.git
cd skills/atxswap && npm install
```

By default the skill uses a built-in fallback list of 6 BSC public RPC
endpoints. To override, set `BSC_RPC_URL` to a single URL or to a
comma-separated list (priority left to right):

```bash
export BSC_RPC_URL="https://my-private-rpc.example.com,https://bsc-dataseed.bnbchain.org"
```

## Common Commands

```bash
cd skills/atxswap && node scripts/wallet.js list
cd skills/atxswap && node scripts/query.js price
cd skills/atxswap && node scripts/query.js quote buy 1
cd skills/atxswap && node scripts/query.js positions <address> <tokenId>
cd skills/atxswap && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20
```

When invoked through a `${SKILL_DIR}`-aware runtime, `cd "${SKILL_DIR}"` is
preferred so the skill works regardless of where the client installed it.

## Liquidity Preview

For custom-range liquidity, do not guess the second token amount from chat.
Preview first, then write:

```bash
cd "${SKILL_DIR}" && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20
cd "${SKILL_DIR}" && node scripts/liquidity.js add --base-token usdt --amount 0.1 --range-percent 20 --from <address>
```

Supported custom range modes:

- `--range-percent <n>`: expands around the current ATX price, e.g. `20` means `-20% ~ +20%`
- `--min-price <p> --max-price <p>`: explicit `USDT per 1 ATX`
- `--tick-lower <n> --tick-upper <n>`: raw V3 ticks

Recommended flow:

1. Run `query.js price` or `liquidity.js quote-add`
2. Show the returned `estimatedAmounts` to the user
3. Wait for confirmation
4. Execute `liquidity.js add`

## Fee Harvest Preview

Before collecting fees, preview the position first:

```bash
cd "${SKILL_DIR}" && node scripts/query.js positions <address> <tokenId>
cd "${SKILL_DIR}" && node scripts/liquidity.js collect <tokenId> --from <address>
```

`query.js positions` returns **principal** notionals (**`principalAtx`**, **`principalUsdt`**, `principal0`/`principal1`) from liquidity L and spot (`getAmountsForLiquidity`); human **USDT-per-ATX** band **`priceRangeUsdtPerAtx`**, spot **`currentPriceUsdtPerAtx`**, **`currentPriceInRange`**, **`pendingFees`** `{ atx, usdt }`; plus raw `tokensOwed*` / `collectable*` for debugging. Prefer **`principal*`** when explaining tokens in-range; **`pendingFees`** / **`collectable*`** before fee harvest — do **not** surface raw tick indices to users.

`liquidity.js remove <tokenId> <percent>` already performs a single on-chain `multicall`:
`decreaseLiquidity` -> `collect` -> and when `percent = 100`, `burn`.
So a full removal already collects withdrawable funds before burning the NFT. After a successful `remove ... 100`,
running `collect` again for the same `tokenId` is expected to fail because the position NFT no longer exists.

## Security Rules

1. Never expose private keys or passwords in chat output.
2. Always preview price, quote, balance, or positions before write actions.
3. Always wait for explicit user confirmation before swap, transfer, or liquidity writes.
4. Treat all write actions as mainnet asset operations.
5. Before deleting a wallet, require the user to export and back up the encrypted keystore first.
6. Wallet deletion requires a second confirmation: the user must explicitly send `force delete wallet`.
7. For transfers, always treat `(asset, from, to, amount)` as one unique transfer intent and restate it before execution.
8. Once a transfer command returns a `txHash`, treat that transfer as already sent; do not automatically send the same transfer again unless the user explicitly asks for another send.
9. If transfer submission status is ambiguous due to timeout, RPC failure, or interrupted output, do not retry blindly; first inspect chain state or wallet state, then ask the user whether to retry.
10. For present-tense questions about balances, holdings, LP positions, pending fees, or remaining assets, always refresh from live on-chain queries first; never answer from earlier chat output, memory, or cached values alone.

## Duplicate-Transfer Guard

Use this minimum flow to avoid duplicate transfers:

1. Preview balances or wallet state first
2. Restate `(asset, from, to, amount)`
3. Wait for explicit confirmation for that exact tuple
4. Run the transfer command once
5. Return the `txHash`
6. If the same transfer request appears again, clarify whether the user wants a second transfer or is referring to the earlier one

## Live Query Requirement

For these questions, rerun a live query before answering:

- wallet balances
- ATX / USDT holdings
- LP NFT positions
- `principalAtx` / `principalUsdt`
- pending fees
- current pool price or quotes

Recommended mapping:

1. Use `query.js balance` for balances
2. Use `query.js positions` for LP holdings and principals
3. Use `query.js price` for current pool price
4. Use `query.js quote` for swap quotes
5. If the refresh fails, say the latest on-chain data could not be refreshed instead of reusing older values

## Wallet Deletion

Delete a wallet only after both confirmations are complete:

1. The user confirms the encrypted keystore backup is done
2. The user explicitly sends `force delete wallet`

Then run:

```bash
cd "${SKILL_DIR}" && node scripts/wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet"
```


path: README.zh.md
content: # ATXSwap 技能

BSC 上 **ATXSwap** 智能体去中心化交换协议的技能包。同一份 `SKILL.md` 同时兼容以
**Claude Code** 和 **OpenClaw** 为代表的客户端，无需为不同客户端维护多份目录。

[**English**](./README.md)

- **GitHub**: https://github.com/agentswapx/skills
- **SDK (npm)**: [`atxswap-sdk`](https://www.npmjs.com/package/atxswap-sdk)
- **SDK 源码 / 文档**: [agentswapx/atxswap-sdk](https://github.com/agentswapx/atxswap-sdk)

项目背景与简要[团队介绍](https://docs.atxswap.com/zh/guide/team)见 ATXSwap 文档站（[English](https://docs.atxswap.com/guide/team)）。本文档说明技能能力与脚本用法。

## 能力范围

- 为当前技能实例创建单个钱包（**不支持导入已有私钥**）
- 查询 ATX 价格、余额、LP 仓位和 ERC20 代币信息
- 在 PancakeSwap V3 上买卖 ATX/USDT
- 预估自定义区间流动性、添加流动性、减仓、收手续费、销毁空仓位 NFT
- 转账 BNB、ATX、USDT 或任意 ERC20 代币

## 目录结构

```text
atxswap/
├── SKILL.md
├── README.md
├── README.zh.md
├── PUBLISH.md
├── CHANGELOG.md
├── .clawhubignore
├── .gitignore
├── package.json
└── scripts/
    ├── _helpers.js
    ├── wallet.js
    ├── query.js
    ├── swap.js
    ├── liquidity.js
    └── transfer.js
```

## 安装

### OpenClaw 安装

```bash
openclaw skills install atxswap
```

### Claude Code 安装

```bash
git clone https://github.com/agentswapx/skills.git
cd skills/atxswap && npm install
```

默认会使用内置的 6 个 BSC 公共 RPC 端点做 fallback。如需覆盖，可将
`BSC_RPC_URL` 设为单个地址或逗号分隔的多个地址（按从左到右的优先级回退）：

```bash
export BSC_RPC_URL="https://my-private-rpc.example.com,https://bsc-dataseed.bnbchain.org"
```

## 常用命令

```bash
cd skills/atxswap && node scripts/wallet.js list
cd skills/atxswap && node scripts/query.js price
cd skills/atxswap && node scripts/query.js quote buy 1
cd skills/atxswap && node scripts/query.js positions <address> <tokenId>
cd skills/atxswap && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20
```

在支持 `${SKILL_DIR}` 注入的运行时中，建议使用 `cd "${SKILL_DIR}"`，以便技能
能在客户端管理的任意安装目录下正常运行。

## 流动性预估

做自定义区间流动性时，不要在对话里直接猜另一边代币数量。建议先预估，再执行写入：

```bash
cd "${SKILL_DIR}" && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20
cd "${SKILL_DIR}" && node scripts/liquidity.js add --base-token usdt --amount 0.1 --range-percent 20 --from <address>
```

支持的区间模式：

- `--range-percent <n>`：以当前 ATX 价格为中心展开，例如 `20` 表示 `-20% ~ +20%`
- `--min-price <p> --max-price <p>`：显式指定 `1 ATX = 多少 USDT`
- `--tick-lower <n> --tick-upper <n>`：直接指定 V3 tick

推荐流程：

1. 先执行 `query.js price` 或 `liquidity.js quote-add`
2. 把返回的 `estimatedAmounts` 展示给用户
3. 等用户确认
4. 再执行 `liquidity.js add`

## 手续费预览

收手续费前，先预览目标仓位：

```bash
cd "${SKILL_DIR}" && node scripts/query.js positions <address> <tokenId>
cd "${SKILL_DIR}" && node scripts/liquidity.js collect <tokenId> --from <address>
```

`query.js positions` 会根据流动性 L、价格区间与当前池价计算 **仓内代币约数**（**`principalAtx`** / **`principalUsdt`**、`principal0`/`principal1`，与前端 `getAmountsForLiquidity` 同源）；并给出 **`priceRangeUsdtPerAtx`**（区间内 USDT/ATX）、**`currentPriceUsdtPerAtx`**、**`currentPriceInRange`**；待收 **`pendingFees`**（`atx`、`usdt`），以及链上 `tokensOwed*` 与 `collectable*`。说明「头寸里有多少币」时请引用 **`principal*`**；说明区间与现价用价格字段而非 tick；判断是否值得收割时优先看 **`pendingFees`** / **`collectable*`**。

`liquidity.js remove <tokenId> <percent>` 现在本身就会发起一笔链上 `multicall`：
`decreaseLiquidity` -> `collect` -> 且当 `percent = 100` 时再 `burn`。
也就是说，100% 移除会在销毁 NFT 前自动收走当前可提取资金。若 `remove ... 100` 已成功，再对同一个
`tokenId` 执行 `collect` 报错是预期行为，因为该头寸 NFT 已不存在。

## 安全规则

1. 不要在聊天输出中暴露私钥或密码。
2. 所有写操作前，必须先预览价格、报价、余额或仓位。
3. 交换、转账、流动性操作前，必须等待用户明确确认。
4. 所有写操作都按主网真实资产处理。
5. 删除钱包前，必须先要求用户导出并备份加密 keystore。
6. 删除钱包还需要第二次确认：用户必须明确发送 `force delete wallet`。
7. 转账时，必须把 `(资产, from, to, amount)` 作为一笔唯一转账意图来处理，并在执行前完整复述给用户确认。
8. 一旦转账命令已经返回 `txHash`，就视为这笔转账已经发出；除非用户明确要求“再转一次”，否则不能对同一笔转账自动重复发送。
9. 如果转账执行处于不确定状态，例如 RPC 超时、网络中断、命令输出不完整但可能已经签名/广播，不能盲目重试；必须先查链上状态或余额变化，再让用户确认是否重试。
10. 用户询问“当前余额”“现在还有多少币”“当前 LP 持仓”“待收手续费”“还剩多少 ATX”这类现时状态时，必须先重新查询链上最新数据，不能直接复用聊天里之前的结果、缓存值或记忆中的数字。

## 防重复转账

为避免重复转账，建议固定执行下面的最小流程：

1. 先查余额或钱包状态
2. 明确展示 `(资产, from, to, amount)`
3. 等用户对这组参数明确确认
4. 只执行一次转账命令
5. 返回 `txHash`
6. 如果用户再次发出相同请求，先确认他们是否真的要再转一笔，而不是引用上一次结果

## 实时查询约束

涉及下面这些问题时，必须先查链上再回答：

- 钱包余额
- ATX / USDT 持仓
- LP NFT 仓位
- 仓位里的 `principalAtx` / `principalUsdt`
- 待收手续费
- 当前池价、报价

推荐约束：

1. 余额问题先跑 `query.js balance`
2. LP / 持仓问题先跑 `query.js positions`
3. 价格问题先跑 `query.js price`
4. 报价问题先跑 `query.js quote`
5. 如果查询失败，要明确说明“本次未能刷新链上最新数据”，不能拿旧结果顶替

## 删除钱包

只有在以下两个条件都满足后，才能删除钱包：

1. 用户已明确确认加密 keystore 备份完成
2. 用户已明确发送 `force delete wallet`

然后再执行：

```bash
cd "${SKILL_DIR}" && node scripts/wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet"
```


path: package-lock.json
content: {
  "name": "atxswap",
  "version": "0.0.31",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "atxswap",
      "version": "0.0.31",
      "dependencies": {
        "atxswap-sdk": "^0.0.15"
      }
    },
    "node_modules/@adraffy/ens-normalize": {
      "version": "1.11.1",
      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@noble/ciphers": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/curves": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@noble/hashes": "1.8.0"
      },
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/hashes": {
      "version": "1.8.0",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/base": {
      "version": "1.2.6",
      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip32": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@noble/curves": "~1.9.0",
        "@noble/hashes": "~1.8.0",
        "@scure/base": "~1.2.5"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip39": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@noble/hashes": "~1.8.0",
        "@scure/base": "~1.2.5"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/abitype": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/wevm"
      },
      "peerDependencies": {
        "typescript": ">=5.0.4",
        "zod": "^3.22.0 || ^4.0.0"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        },
        "zod": {
          "optional": true
        }
      }
    },
    "node_modules/atxswap-sdk": {
      "version": "0.0.15",
      "resolved": "https://registry.npmjs.org/atxswap-sdk/-/atxswap-sdk-0.0.15.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "viem": "^2.31.3"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/eventemitter3": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isows": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/wevm"
        }
      ],
      "license": "MIT",
      "peerDependencies": {
        "ws": "*"
      }
    },
    "node_modules/ox": {
      "version": "0.14.20",
      "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/wevm"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "@adraffy/ens-normalize": "^1.11.0",
        "@noble/ciphers": "^1.3.0",
        "@noble/curves": "1.9.1",
        "@noble/hashes": "^1.8.0",
        "@scure/bip32": "^1.7.0",
        "@scure/bip39": "^1.6.0",
        "abitype": "^1.2.3",
        "eventemitter3": "5.0.1"
      },
      "peerDependencies": {
        "typescript": ">=5.4.0"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        }
      }
    },
    "node_modules/viem": {
      "version": "2.48.4",
      "resolved": "https://registry.npmjs.org/viem/-/viem-2.48.4.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/wevm"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "@noble/curves": "1.9.1",
        "@noble/hashes": "1.8.0",
        "@scure/bip32": "1.7.0",
        "@scure/bip39": "1.6.0",
        "abitype": "1.2.3",
        "isows": "1.0.7",
        "ox": "0.14.20",
        "ws": "8.18.3"
      },
      "peerDependencies": {
        "typescript": ">=5.0.4"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        }
      }
    },
    "node_modules/ws": {
      "version": "8.18.3",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: package.json
content: {
  "name": "atxswap",
  "private": true,
  "type": "module",
  "version": "0.0.31",
  "description": "Standalone skills.sh-oriented ATX trading skill for BSC wallet, query, swap, liquidity, and transfer workflows",
  "scripts": {
    "bootstrap": "npm install",
    "check:price": "node scripts/query.js price",
    "check:wallets": "node scripts/wallet.js list"
  },
  "dependencies": {
    "atxswap-sdk": "^0.0.15"
  }
}


path: scripts/_helpers.js
content: import { AtxClient } from "atxswap-sdk";
import { homedir } from "node:os";
import { join } from "node:path";
import readline from "node:readline";

const DEFAULT_KEYSTORE_PATH = join(homedir(), ".config", "atxswap", "keystore");

function parseRpcUrls(raw) {
  if (!raw) return undefined;
  const urls = raw
    .split(",")
    .map((url) => url.trim())
    .filter((url) => url.length > 0);
  return urls.length > 0 ? urls : undefined;
}

export async function createClient() {
  const rpcUrls = parseRpcUrls(process.env.BSC_RPC_URL);
  const client = new AtxClient({
    ...(rpcUrls && { rpcUrls }),
    keystorePath: DEFAULT_KEYSTORE_PATH,
  });
  await client.ready();
  return client;
}

export function getDefaultKeystorePath() {
  return DEFAULT_KEYSTORE_PATH;
}

export function parseArgs(args) {
  const parsed = { _: [] };
  for (let i = 0; i < args.length; i++) {
    if (args[i].startsWith("--")) {
      const key = args[i].slice(2);
      parsed[key] = args[i + 1] || true;
      i++;
    } else {
      parsed._.push(args[i]);
    }
  }
  return parsed;
}

export function fmt(wei, decimals = 18) {
  const n = Number(wei) / 10 ** decimals;
  if (n === 0) return "0";
  if (n < 0.000001) return n.toExponential(4);
  return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
}

/** V3 fee tier (`100`|`500`|`2500`|`10000`) → human-readable swap fee, e.g. `0.25%`. Matches Uniswap tier display (`fee`/10000 as a percent label). */
export function feeTierToPercentString(fee) {
  const n = Number(fee);
  if (!Number.isFinite(n) || n <= 0) return "0%";
  const pct = n / 10000;
  const s = pct.toFixed(6).replace(/\.?0+$/, "") || "0";
  return `${s}%`;
}

export function jsonStringify(value, space = 2) {
  return JSON.stringify(
    value,
    (_, current) => (typeof current === "bigint" ? current.toString() : current),
    space,
  );
}

export function exitError(message, code = 1) {
  console.error(jsonStringify({ error: message }));
  process.exit(code);
}

export function getErrorMessage(error) {
  if (typeof error === "string") return error;
  if (error?.shortMessage) return error.shortMessage;
  if (error?.reason) return error.reason;
  if (error?.message) return error.message.split("\n")[0];
  return "Unknown error";
}

export async function runMain(fn) {
  try {
    await fn();
  } catch (error) {
    exitError(getErrorMessage(error));
  }
}

async function promptHidden(promptText) {
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
    return null;
  }

  return await new Promise((resolve, reject) => {
    let value = "";
    const wasRaw = process.stdin.isRaw;

    readline.emitKeypressEvents(process.stdin);
    process.stdin.setRawMode?.(true);
    process.stdin.resume();
    process.stdout.write(promptText);

    const onKeypress = (char, key) => {
      if (key?.ctrl && key.name === "c") {
        cleanup();
        process.stdout.write("\n");
        reject(new Error("Password input cancelled"));
        return;
      }

      if (key?.name === "return" || key?.name === "enter") {
        cleanup();
        process.stdout.write("\n");
        resolve(value);
        return;
      }

      if (key?.name === "backspace") {
        value = value.slice(0, -1);
        return;
      }

      if (char) {
        value += char;
      }
    };

    const cleanup = () => {
      process.stdin.off("keypress", onKeypress);
      process.stdin.setRawMode?.(Boolean(wasRaw));
      process.stdin.pause();
    };

    process.stdin.on("keypress", onKeypress);
  });
}

export async function resolvePassword(args, promptText = "Enter wallet password: ") {
  if (args.password) return args.password;

  const ttyPassword = await promptHidden(promptText);
  if (ttyPassword) return ttyPassword;

  exitError("Password required: use --password <pwd> or run in an interactive terminal");
}

export async function resolveNewPassword(args) {
  if (args.password) return args.password;

  if (process.stdin.isTTY && process.stdout.isTTY) {
    while (true) {
      const password = await promptHidden("Create wallet password: ");
      if (!password) exitError("Password cannot be empty");
      const confirm = await promptHidden("Confirm wallet password: ");
      if (password !== confirm) {
        console.error("Passwords do not match, please try again.");
        continue;
      }
      return password;
    }
  }

  exitError("Password required: use --password <pwd> or run in an interactive terminal");
}

export async function loadWallet(client, address, args) {
  try {
    return await client.wallet.load(address);
  } catch {
    if (args.password) {
      return await client.wallet.load(address, args.password);
    }

    const ttyPassword = await promptHidden(`Password for ${address}: `);
    if (ttyPassword) {
      return await client.wallet.load(address, ttyPassword);
    }

    exitError(`Password required for ${address}: use --password <pwd> or run in an interactive terminal`);
  }
}

export async function exportKeystore(client, address, args = {}) {
  if (typeof client.wallet.exportMetaMaskKeystore === "function") {
    return await client.wallet.exportMetaMaskKeystore(address, args.password);
  }
  return client.wallet.exportKeystore(address);
}


path: scripts/_v3math.js
content: /**
 * PancakeSwap V3 / Uniswap V3: derive token amounts from liquidity L at current sqrt price.
 * Matches Uniswap V3 Periphery LiquidityAmounts.getAmountsForLiquidity (same math as frontend v3math.ts).
 */

const Q96 = 2n ** 96n;

export function tickToSqrtPriceX96(tick) {
  const sqrtPrice = Math.sqrt(1.0001 ** tick);
  return BigInt(Math.round(sqrtPrice * Number(Q96)));
}

function getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) {
  let a = sqrtRatioAX96;
  let b = sqrtRatioBX96;
  if (a > b) [a, b] = [b, a];
  if (a === b || liquidity === 0n) return 0n;
  const diff = b - a;
  return (liquidity * Q96 * diff) / b / a;
}

function getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) {
  let a = sqrtRatioAX96;
  let b = sqrtRatioBX96;
  if (a > b) [a, b] = [b, a];
  if (a === b || liquidity === 0n) return 0n;
  return (liquidity * (b - a)) / Q96;
}

/**
 * @param {bigint} sqrtRatioX96 - current pool price from slot0
 * @param {number} tickLower
 * @param {number} tickUpper
 * @param {bigint} liquidity - position L (uint128)
 * @returns {{ amount0: bigint, amount1: bigint }}
 */
export function getAmountsForLiquidity(sqrtRatioX96, tickLower, tickUpper, liquidity) {
  if (liquidity === 0n || sqrtRatioX96 === 0n) return { amount0: 0n, amount1: 0n };

  let sqrtA = tickToSqrtPriceX96(tickLower);
  let sqrtB = tickToSqrtPriceX96(tickUpper);
  if (sqrtA > sqrtB) [sqrtA, sqrtB] = [sqrtB, sqrtA];

  if (sqrtRatioX96 <= sqrtA) {
    return { amount0: getAmount0ForLiquidity(sqrtA, sqrtB, liquidity), amount1: 0n };
  }
  if (sqrtRatioX96 >= sqrtB) {
    return { amount0: 0n, amount1: getAmount1ForLiquidity(sqrtA, sqrtB, liquidity) };
  }
  return {
    amount0: getAmount0ForLiquidity(sqrtRatioX96, sqrtB, liquidity),
    amount1: getAmount1ForLiquidity(sqrtA, sqrtRatioX96, liquidity),
  };
}


path: scripts/liquidity.js
content: #!/usr/bin/env node
import { createClient, loadWallet, parseArgs, exitError, runMain, fmt, jsonStringify } from "./_helpers.js";
import { formatUnits, parseEther } from "atxswap-sdk";

const POOL_TOKEN0_ABI = [
  {
    type: "function",
    name: "token0",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "address" }],
  },
];

const POOL_SLOT0_ABI = [
  {
    type: "function",
    name: "slot0",
    stateMutability: "view",
    inputs: [],
    outputs: [
      { type: "uint160" },
      { type: "int24" },
      { type: "uint16" },
      { type: "uint16" },
      { type: "uint16" },
      { type: "uint8" },
      { type: "bool" },
    ],
  },
];

const V3_MIN_TICK = -887200;
const V3_MAX_TICK = 887200;
const LOG_BASE = Math.log(1.0001);
const Q96 = 2n ** 96n;

function priceToTick(price) {
  if (price <= 0) return 0;
  return Math.round(Math.log(price) / LOG_BASE);
}

function nearestUsableTick(tick, tickSpacing) {
  return Math.round(tick / tickSpacing) * tickSpacing;
}

function feeToTickSpacing(fee) {
  switch (fee) {
    case 100:
      return 1;
    case 500:
      return 10;
    case 2500:
      return 50;
    case 10000:
      return 200;
    default:
      return 50;
  }
}

function tickToSqrtPriceX96(tick) {
  const sqrtPrice = Math.sqrt(1.0001 ** tick);
  return BigInt(Math.round(sqrtPrice * Number(Q96)));
}

function neededTokens(sqrtPriceX96, tickLower, tickUpper) {
  const sqrtA = tickToSqrtPriceX96(tickLower);
  const sqrtB = tickToSqrtPriceX96(tickUpper);
  if (sqrtPriceX96 <= sqrtA) return { need0: true, need1: false };
  if (sqrtPriceX96 >= sqrtB) return { need0: false, need1: true };
  return { need0: true, need1: true };
}

function calcOtherAmount(sqrtPriceX96, tickLower, tickUpper, amount, isAmount0) {
  if (amount === 0n) return 0n;

  const sqrtA = tickToSqrtPriceX96(tickLower);
  const sqrtB = tickToSqrtPriceX96(tickUpper);
  const sqrtP = sqrtPriceX96;

  if (sqrtP <= sqrtA || sqrtP >= sqrtB) {
    return 0n;
  }

  if (isAmount0) {
    const numeratorL = amount * sqrtP * sqrtB;
    const denominatorL = (sqrtB - sqrtP) * Q96;
    if (denominatorL === 0n) return 0n;
    const L = numeratorL / denominatorL;
    const amount1 = (L * (sqrtP - sqrtA)) / Q96;
    return amount1 > 0n ? amount1 : 0n;
  }

  const diffPA = sqrtP - sqrtA;
  if (diffPA === 0n) return 0n;
  const L = (amount * Q96) / diffPA;
  const amount0 = (L * (sqrtB - sqrtP) * Q96) / (sqrtP * sqrtB);
  return amount0 > 0n ? amount0 : 0n;
}

function sqrtPriceX96ToRawPrice(sqrtPriceX96) {
  const sqrtP = Number(sqrtPriceX96) / Number(Q96);
  return sqrtP * sqrtP;
}

function parsePercentValue(raw, label) {
  const normalized = String(raw).trim().replace(/%$/, "");
  const value = parseFloat(normalized);
  if (!Number.isFinite(value) || value <= 0) {
    exitError(`${label} must be a positive number`);
  }
  return value;
}

function parseBaseToken(raw) {
  const value = String(raw || "").trim().toLowerCase();
  if (value !== "atx" && value !== "usdt") {
    exitError('base token must be either "atx" or "usdt"');
  }
  return value;
}

function normalizeRangeMode(args) {
  const hasTick =
    args["tick-lower"] !== undefined &&
    args["tick-lower"] !== true &&
    args["tick-upper"] !== undefined &&
    args["tick-upper"] !== true;
  const hasPrice =
    args["min-price"] !== undefined &&
    args["min-price"] !== true &&
    args["max-price"] !== undefined &&
    args["max-price"] !== true;
  const hasRangePercent =
    args["range-percent"] !== undefined && args["range-percent"] !== true;
  const wantFull = args["full-range"] === true || args["full-range"] === "true";

  if (wantFull && (hasTick || hasPrice || hasRangePercent)) {
    exitError("Do not combine --full-range with other range options");
  }
  if ([hasTick, hasPrice, hasRangePercent].filter(Boolean).length > 1) {
    exitError("Use only one of --tick-lower/--tick-upper, --min-price/--max-price, or --range-percent");
  }
  if ((args["tick-lower"] !== undefined && !hasTick) || (args["tick-upper"] !== undefined && !hasTick)) {
    exitError("When using ticks, pass both --tick-lower and --tick-upper");
  }
  if ((args["min-price"] !== undefined && !hasPrice) || (args["max-price"] !== undefined && !hasPrice)) {
    exitError("When using price range, pass both --min-price and --max-price (USDT per 1 ATX)");
  }

  return { hasTick, hasPrice, hasRangePercent, wantFull };
}

/**
 * @param {number} human price: USDT per 1 ATX (18-dec pool, same as frontend)
 * @param {boolean} isAtxToken0 from pool
 */
function humanUsdtPerAtxToToken1OverToken0(h, isAtxToken0) {
  if (h <= 0) {
    throw new Error("min-price and max-price must be positive (USDT per 1 ATX)");
  }
  return isAtxToken0 ? h : 1 / h;
}

/**
 * @returns {{ tickLower: number, tickUpper: number }}
 */
function humanPriceRangeToTicks(minHuman, maxHuman, isAtxToken0, tickSpacing) {
  const rawA = humanUsdtPerAtxToToken1OverToken0(minHuman, isAtxToken0);
  const rawB = humanUsdtPerAtxToToken1OverToken0(maxHuman, isAtxToken0);
  let t0 = nearestUsableTick(priceToTick(rawA), tickSpacing);
  let t1 = nearestUsableTick(priceToTick(rawB), tickSpacing);
  let tickLower = Math.min(t0, t1);
  let tickUpper = Math.max(t0, t1);
  if (tickLower < V3_MIN_TICK) tickLower = V3_MIN_TICK;
  if (tickUpper > V3_MAX_TICK) tickUpper = V3_MAX_TICK;
  if (tickLower >= tickUpper) {
    tickUpper = tickLower + tickSpacing;
  }
  if (tickUpper > V3_MAX_TICK) {
    tickUpper = V3_MAX_TICK;
    tickLower = tickUpper - tickSpacing;
  }
  return { tickLower, tickUpper };
}

async function getPoolContext(client) {
  const [token0, slot0] = await Promise.all([
    client.publicClient.readContract({
      address: client.contracts.pool,
      abi: POOL_TOKEN0_ABI,
      functionName: "token0",
    }),
    client.publicClient.readContract({
      address: client.contracts.pool,
      abi: POOL_SLOT0_ABI,
      functionName: "slot0",
    }),
  ]);

  const atxToken0 = token0.toLowerCase() === client.contracts.atx.toLowerCase();
  const sqrtPriceX96 = slot0[0];
  const rawPrice = sqrtPriceX96ToRawPrice(sqrtPriceX96);
  const usdtPerAtx = atxToken0 ? rawPrice : 1 / rawPrice;

  return {
    isAtxToken0: atxToken0,
    sqrtPriceX96,
    currentTick: Number(slot0[1]),
    tickSpacing: feeToTickSpacing(client.poolFee),
    usdtPerAtx,
  };
}

function resolveRangeFromArgs(args, ctx) {
  const { hasTick, hasPrice, hasRangePercent } = normalizeRangeMode(args);

  if (hasPrice) {
    const minP = parseFloat(String(args["min-price"]));
    const maxP = parseFloat(String(args["max-price"]));
    if (!Number.isFinite(minP) || !Number.isFinite(maxP) || minP <= 0 || maxP <= 0) {
      exitError("min-price and max-price must be positive numbers (USDT per 1 ATX)");
    }
    const { tickLower, tickUpper } = humanPriceRangeToTicks(
      minP,
      maxP,
      ctx.isAtxToken0,
      ctx.tickSpacing,
    );
    return {
      tickLower,
      tickUpper,
      meta: {
        mode: "price",
        minPrice: minP,
        maxPrice: maxP,
        tickLower,
        tickUpper,
        tickSpacing: ctx.tickSpacing,
        isAtxToken0: ctx.isAtxToken0,
      },
    };
  }

  if (hasRangePercent) {
    const percent = parsePercentValue(args["range-percent"], "range-percent");
    const factor = percent / 100;
    const minPrice = ctx.usdtPerAtx * (1 - factor);
    const maxPrice = ctx.usdtPerAtx * (1 + factor);
    if (!Number.isFinite(minPrice) || !Number.isFinite(maxPrice) || minPrice <= 0 || maxPrice <= 0) {
      exitError("range-percent is too large for the current price; it would make the lower price non-positive");
    }
    const { tickLower, tickUpper } = humanPriceRangeToTicks(
      minPrice,
      maxPrice,
      ctx.isAtxToken0,
      ctx.tickSpacing,
    );
    return {
      tickLower,
      tickUpper,
      meta: {
        mode: "percent",
        rangePercent: percent,
        centerPrice: ctx.usdtPerAtx,
        minPrice,
        maxPrice,
        tickLower,
        tickUpper,
        tickSpacing: ctx.tickSpacing,
        isAtxToken0: ctx.isAtxToken0,
      },
    };
  }

  if (hasTick) {
    const tickLower = parseInt(String(args["tick-lower"]), 10);
    const tickUpper = parseInt(String(args["tick-upper"]), 10);
    if (!Number.isFinite(tickLower) || !Number.isFinite(tickUpper)) {
      exitError("tick-lower and tick-upper must be integers");
    }
    let tl = Math.min(tickLower, tickUpper);
    let tu = Math.max(tickLower, tickUpper);
    if (tl < V3_MIN_TICK) tl = V3_MIN_TICK;
    if (tu > V3_MAX_TICK) tu = V3_MAX_TICK;
    if (tl >= tu) {
      exitError("tick-lower and tick-upper must allow tickLower < tickUpper after normalizing to pool limits");
    }
    return {
      tickLower: tl,
      tickUpper: tu,
      meta: {
        mode: "tick",
        tickLower: tl,
        tickUpper: tu,
        tickSpacing: ctx.tickSpacing,
        isAtxToken0: ctx.isAtxToken0,
      },
    };
  }

  return {
    tickLower: V3_MIN_TICK,
    tickUpper: V3_MAX_TICK,
    meta: {
      mode: "full-range",
      fullRange: true,
      tickLower: V3_MIN_TICK,
      tickUpper: V3_MAX_TICK,
      tickSpacing: ctx.tickSpacing,
      isAtxToken0: ctx.isAtxToken0,
    },
  };
}

function resolveSingleSidedAmounts(ctx, tickLower, tickUpper, baseToken, amountRaw) {
  const amount = parseEther(amountRaw);
  if (amount <= 0n) {
    exitError("amount must be greater than 0");
  }

  const { need0, need1 } = neededTokens(ctx.sqrtPriceX96, tickLower, tickUpper);
  const needAtx = ctx.isAtxToken0 ? need0 : need1;
  const needUsdt = ctx.isAtxToken0 ? need1 : need0;

  if (baseToken === "atx") {
    if (!needAtx && needUsdt) {
      exitError("Current price is above the selected range; only USDT is needed for this position");
    }
    return {
      atxAmount: amount,
      usdtAmount: needUsdt
        ? calcOtherAmount(ctx.sqrtPriceX96, tickLower, tickUpper, amount, ctx.isAtxToken0)
        : 0n,
      meta: { baseToken, amount: amountRaw, needAtx, needUsdt },
    };
  }

  if (!needUsdt && needAtx) {
    exitError("Current price is below the selected range; only ATX is needed for this position");
  }

  return {
    atxAmount: needAtx
      ? calcOtherAmount(ctx.sqrtPriceX96, tickLower, tickUpper, amount, !ctx.isAtxToken0)
      : 0n,
    usdtAmount: amount,
    meta: { baseToken, amount: amountRaw, needAtx, needUsdt },
  };
}

function buildQuoteResult(ctx, range, amounts, extra = {}) {
  return {
    currentPrice: {
      usdtPerAtx: ctx.usdtPerAtx,
      atxPerUsdt: ctx.usdtPerAtx === 0 ? 0 : 1 / ctx.usdtPerAtx,
    },
    range: range.meta,
    estimatedAmounts: {
      atx: fmt(amounts.atxAmount),
      usdt: fmt(amounts.usdtAmount),
      atxWei: amounts.atxAmount.toString(),
      usdtWei: amounts.usdtAmount.toString(),
    },
    ...extra,
  };
}

function toAmountInput(wei) {
  const value = formatUnits(wei, 18);
  return value.includes(".") ? value.replace(/\.?0+$/, "") : value;
}

function parseSlippageBps(args) {
  if (args["slippage-bps"] === undefined || args["slippage-bps"] === true) {
    return undefined;
  }
  const value = parseInt(String(args["slippage-bps"]), 10);
  if (!Number.isFinite(value) || value < 0 || value > 10_000) {
    exitError("slippage-bps must be between 0 and 10000");
  }
  return value;
}

function buildSdkRangeArgs(args) {
  const { hasTick, hasPrice, hasRangePercent } = normalizeRangeMode(args);
  if (hasPrice) {
    const minPrice = parseFloat(String(args["min-price"]));
    const maxPrice = parseFloat(String(args["max-price"]));
    if (!Number.isFinite(minPrice) || !Number.isFinite(maxPrice) || minPrice <= 0 || maxPrice <= 0) {
      exitError("min-price and max-price must be positive numbers (USDT per 1 ATX)");
    }
    return { minPrice, maxPrice };
  }
  if (hasRangePercent) {
    return { rangePercent: parsePercentValue(args["range-percent"], "range-percent") };
  }
  if (hasTick) {
    const tickLower = parseInt(String(args["tick-lower"]), 10);
    const tickUpper = parseInt(String(args["tick-upper"]), 10);
    if (!Number.isFinite(tickLower) || !Number.isFinite(tickUpper)) {
      exitError("tick-lower and tick-upper must be integers");
    }
    return { tickLower, tickUpper };
  }
  return { fullRange: true };
}

async function quoteViaSdk(client, baseToken, amountRaw, args) {
  if (typeof client.liquidity.quoteAddLiquidity !== "function") {
    return null;
  }
  return await client.liquidity.quoteAddLiquidity({
    baseToken,
    amount: parseEther(amountRaw),
    range: buildSdkRangeArgs(args),
    slippageBps: parseSlippageBps(args),
  });
}

function sdkQuoteToJson(result, extra = {}) {
  return {
    currentPrice: result.currentPrice,
    range: {
      ...result.range,
      tickSpacing: result.pool.tickSpacing,
      isAtxToken0: result.pool.isAtxToken0,
    },
    estimatedAmounts: {
      atx: fmt(result.desiredAmounts.atx),
      usdt: fmt(result.desiredAmounts.usdt),
      atxWei: result.desiredAmounts.atx.toString(),
      usdtWei: result.desiredAmounts.usdt.toString(),
    },
    minAmounts: {
      atx: fmt(result.minAmounts.atx),
      usdt: fmt(result.minAmounts.usdt),
      atxWei: result.minAmounts.atx.toString(),
      usdtWei: result.minAmounts.usdt.toString(),
    },
    ...extra,
  };
}

await runMain(async () => {
  const client = await createClient();
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];

  if (!command) {
    exitError(
      "Usage: liquidity.js <quote-add|add|remove|collect|burn> [args] [--from address] [--password <pwd>]\n" +
        "  quote-add: <atx|usdt> <amount> with optional range --full-range | --tick-lower <n> --tick-upper <n> | --min-price <p> --max-price <p> | --range-percent <n>\n" +
        "  add: positional <atxAmount> <usdtAmount> OR auto-balance with --base-token <atx|usdt> --amount <n>, plus optional range options and --slippage-bps <n>",
    );
  }

  switch (command) {
    case "quote-add": {
      const baseToken = parseBaseToken(args._[1]);
      const amount = args._[2];
      if (!amount) {
        exitError(
          "Usage: liquidity.js quote-add <atx|usdt> <amount> [--full-range] [--tick-lower n --tick-upper n] [--min-price p --max-price p] [--range-percent n]",
        );
      }

      const sdkQuote = await quoteViaSdk(client, baseToken, amount, args);
      if (sdkQuote) {
        console.log(
          jsonStringify(
            sdkQuoteToJson(sdkQuote, {
              action: "quote add liquidity",
              input: { baseToken, amount },
            }),
            2,
          ),
        );
        break;
      }

      const ctx = await getPoolContext(client);
      const range = resolveRangeFromArgs(args, ctx);
      const amounts = resolveSingleSidedAmounts(ctx, range.tickLower, range.tickUpper, baseToken, amount);

      console.log(
        jsonStringify(
          buildQuoteResult(ctx, range, amounts, {
            action: "quote add liquidity",
            input: { baseToken, amount },
          }),
          2,
        ),
      );
      break;
    }

    case "add": {
      const positionalAtxAmount = args._[1];
      const positionalUsdtAmount = args._[2];
      const hasPositionalAmounts = !!positionalAtxAmount && !!positionalUsdtAmount;
      const hasAutoBalance =
        args["base-token"] !== undefined &&
        args["base-token"] !== true &&
        args.amount !== undefined &&
        args.amount !== true;

      if (hasPositionalAmounts && hasAutoBalance) {
        exitError("Use either positional amounts or --base-token/--amount auto-balance mode, not both");
      }

      if (!hasPositionalAmounts && !hasAutoBalance) {
        exitError(
          "Usage: liquidity.js add <atxAmount> <usdtAmount> [--from address] [--full-range] [--tick-lower n --tick-upper n] [--min-price p --max-price p] [--range-percent n] [--slippage-bps n]\n" +
            "   or: liquidity.js add --base-token <atx|usdt> --amount <n> [same range opts]\n" +
            "  Default: full range. Custom: either tick pair, min/max price (USDT per 1 ATX), or range-percent centered on current price.",
        );
      }

      const ctx = await getPoolContext(client);
      const fromAddress = args.from || client.wallet.list()[0]?.address;
      if (!fromAddress) exitError("No wallet found. Create one first.");
      const wallet = await loadWallet(client, fromAddress, args);
      const range = resolveRangeFromArgs(args, ctx);
      let liqOptions;
      let meta = { ...range.meta };
      let atxAmount;
      let usdtAmount;

      if (range.meta.fullRange) {
        liqOptions = { fullRange: true };
      } else {
        liqOptions = {
          fullRange: false,
          tickLower: range.tickLower,
          tickUpper: range.tickUpper,
        };
      }

      if (hasPositionalAmounts) {
        atxAmount = positionalAtxAmount;
        usdtAmount = positionalUsdtAmount;
      } else {
        const baseToken = parseBaseToken(args["base-token"]);
        const sdkQuote = await quoteViaSdk(client, baseToken, String(args.amount), args);
        if (sdkQuote) {
          atxAmount = toAmountInput(sdkQuote.desiredAmounts.atx);
          usdtAmount = toAmountInput(sdkQuote.desiredAmounts.usdt);
          meta = {
            ...meta,
            autoBalanced: true,
            input: {
              baseToken,
              amount: String(args.amount),
              needAtx: sdkQuote.needs.atx,
              needUsdt: sdkQuote.needs.usdt,
            },
            estimatedAmounts: {
              atx: fmt(sdkQuote.desiredAmounts.atx),
              usdt: fmt(sdkQuote.desiredAmounts.usdt),
            },
          };
        } else {
          const quoted = resolveSingleSidedAmounts(
            ctx,
            range.tickLower,
            range.tickUpper,
            baseToken,
            String(args.amount),
          );
          atxAmount = toAmountInput(quoted.atxAmount);
          usdtAmount = toAmountInput(quoted.usdtAmount);
          meta = {
            ...meta,
            autoBalanced: true,
            input: quoted.meta,
            estimatedAmounts: {
              atx: fmt(quoted.atxAmount),
              usdt: fmt(quoted.usdtAmount),
            },
          };
        }
      }

      const b = parseSlippageBps(args);
      if (b !== undefined) {
        liqOptions = { ...liqOptions, slippageBps: b };
        meta = { ...meta, slippageBps: b };
      }

      const result = await client.liquidity.addLiquidity(
        wallet,
        parseEther(atxAmount),
        parseEther(usdtAmount),
        liqOptions,
      );
      console.log(
        jsonStringify(
          { action: "add liquidity", txHash: result.txHash, range: meta },
          2,
        ),
      );
      break;
    }

    case "remove": {
      const fromAddress = args.from || client.wallet.list()[0]?.address;
      if (!fromAddress) exitError("No wallet found. Create one first.");
      const wallet = await loadWallet(client, fromAddress, args);
      const tokenId = args._[1];
      const percent = args._[2];
      if (!tokenId || !percent) {
        exitError("Usage: liquidity.js remove <tokenId> <percent> [--from address]");
      }
      const removePercent = parseInt(percent, 10);
      const result = await client.liquidity.removeLiquidity(wallet, BigInt(tokenId), removePercent);
      console.log(
        jsonStringify(
          {
            action: "remove liquidity",
            txHash: result.txHash,
            tokenId,
            percent: removePercent,
            multicall: removePercent === 100
              ? ["decreaseLiquidity", "collect", "burn"]
              : ["decreaseLiquidity", "collect"],
            note: removePercent === 100
              ? "100% removal collects withdrawable funds and then burns the NFT in the same transaction. Do not run collect again for this tokenId."
              : "Removal also collects withdrawable funds in the same transaction.",
          },
          2,
        ),
      );
      break;
    }

    case "collect": {
      const fromAddress = args.from || client.wallet.list()[0]?.address;
      if (!fromAddress) exitError("No wallet found. Create one first.");
      const wallet = await loadWallet(client, fromAddress, args);
      const tokenId = args._[1];
      if (!tokenId) exitError("Usage: liquidity.js collect <tokenId> [--from address]");
      const result = await client.liquidity.collectFees(wallet, BigInt(tokenId));
      console.log(jsonStringify({ action: "collect fees", txHash: result.txHash }, 2));
      break;
    }

    case "burn": {
      const fromAddress = args.from || client.wallet.list()[0]?.address;
      if (!fromAddress) exitError("No wallet found. Create one first.");
      const wallet = await loadWallet(client, fromAddress, args);
      const tokenId = args._[1];
      if (!tokenId) exitError("Usage: liquidity.js burn <tokenId> [--from address]");
      const result = await client.liquidity.burnPosition(wallet, BigInt(tokenId));
      console.log(jsonStringify({ action: "burn position", txHash: result.txHash }, 2));
      break;
    }

    default:
      exitError("Usage: liquidity.js <quote-add|add|remove|collect|burn> [args]");
  }
});


path: scripts/query.js
content: #!/usr/bin/env node
import { createClient, parseArgs, fmt, runMain, jsonStringify, feeTierToPercentString } from "./_helpers.js";
import { getAmountsForLiquidity, tickToSqrtPriceX96 } from "./_v3math.js";
import { parseEther, poolAbi } from "atxswap-sdk";

function usdtPerAtxFromSqrt(sqrtPriceX96, isAtxToken0) {
  const num = Number(sqrtPriceX96);
  const Q96 = Number(2n ** 96n);
  const ratio = (num / Q96) ** 2;
  return isAtxToken0 ? ratio : ratio === 0 ? 0 : 1 / ratio;
}

/** Human USDT per 1 ATX for display (matches app-style quotes). */
function fmtUsdtPerAtxPrice(value) {
  if (!Number.isFinite(value) || value <= 0) return "0";
  const s = value.toFixed(8).replace(/\.?0+$/, "");
  return s.length ? s : "0";
}

await runMain(async () => {
  const client = await createClient();
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];

  switch (command) {
    case "price": {
      const price = await client.query.getPrice();
      console.log(jsonStringify({
        usdtPerAtx: price.usdtPerAtx,
        atxPerUsdt: price.atxPerUsdt,
        sqrtPriceX96: price.sqrtPriceX96.toString(),
      }, 2));
      break;
    }

    case "balance": {
      const address = args._[1];
      if (!address) { console.error("Usage: query.js balance <address>"); process.exit(1); }
      const bal = await client.query.getBalance(address);
      console.log(jsonStringify({
        address,
        bnb: fmt(bal.bnb),
        atx: fmt(bal.atx),
        usdt: fmt(bal.usdt),
      }, 2));
      break;
    }

    case "quote": {
      const direction = args._[1];
      const amount = args._[2];
      if (!direction || !amount) { console.error("Usage: query.js quote <buy|sell> <amount>"); process.exit(1); }
      const amountWei = parseEther(amount);
      const quote = await client.query.getQuote(direction, amountWei);
      console.log(jsonStringify({
        direction: quote.direction,
        amountIn: fmt(quote.amountIn),
        amountOut: fmt(quote.amountOut),
        priceImpact: (quote.priceImpact * 100).toFixed(4) + "%",
      }, 2));
      break;
    }

    case "positions": {
      const address = args._[1];
      const tokenId = args._[2];
      if (!address) { console.error("Usage: query.js positions <address> [tokenId]"); process.exit(1); }
      const queriedPositions = await client.query.getPositions(address, {
        includeCollectableFees: true,
        ...(tokenId ? { tokenId: BigInt(tokenId) } : {}),
      });
      const positions = tokenId
        ? queriedPositions.filter((position) => position.tokenId === BigInt(tokenId))
        : queriedPositions;
      if (positions.length === 0) {
        console.log("No ATX/USDT positions found.");
      } else {
        const slot0Result = await client.publicClient.readContract({
          address: client.contracts.pool,
          abi: poolAbi,
          functionName: "slot0",
        });
        const currentTick = Number(slot0Result[1]);

        const priceSnapshot = await client.query.getPrice();

        for (const p of positions) {
          const isAtxToken0 = p.token0.toLowerCase() === client.contracts.atx.toLowerCase();
          const collectable0 = p.collectable0 ?? p.tokensOwed0;
          const collectable1 = p.collectable1 ?? p.tokensOwed1;

          let amount0;
          let amount1;
          if (p.principal0 !== undefined && p.principal1 !== undefined) {
            amount0 = p.principal0;
            amount1 = p.principal1;
          } else {
            ({ amount0, amount1 } = getAmountsForLiquidity(
              priceSnapshot.sqrtPriceX96,
              p.tickLower,
              p.tickUpper,
              p.liquidity,
            ));
          }

          const principalAtx = isAtxToken0 ? amount0 : amount1;
          const principalUsdt = isAtxToken0 ? amount1 : amount0;

          const sqrtTickLower = tickToSqrtPriceX96(p.tickLower);
          const sqrtTickUpper = tickToSqrtPriceX96(p.tickUpper);
          const priceAtLower = usdtPerAtxFromSqrt(sqrtTickLower, isAtxToken0);
          const priceAtUpper = usdtPerAtxFromSqrt(sqrtTickUpper, isAtxToken0);
          const bandMin = Math.min(priceAtLower, priceAtUpper);
          const bandMax = Math.max(priceAtLower, priceAtUpper);

          const spotUsdt = priceSnapshot.usdtPerAtx;
          const tickBoundLo = Math.min(p.tickLower, p.tickUpper);
          const tickBoundHi = Math.max(p.tickLower, p.tickUpper);
          const currentPriceInRange =
            currentTick >= tickBoundLo && currentTick < tickBoundHi;

          const collectableAtxRaw = isAtxToken0 ? collectable0 : collectable1;
          const collectableUsdtRaw = isAtxToken0 ? collectable1 : collectable0;

          console.log(jsonStringify({
            tokenId: p.tokenId.toString(),
            fee: p.fee,
            feePercent: feeTierToPercentString(p.fee),
            priceRangeUsdtPerAtx: {
              min: fmtUsdtPerAtxPrice(bandMin),
              max: fmtUsdtPerAtxPrice(bandMax),
            },
            currentPriceUsdtPerAtx: fmtUsdtPerAtxPrice(spotUsdt),
            currentPriceInRange: currentPriceInRange,
            liquidity: p.liquidity.toString(),
            principal0: fmt(amount0),
            principal1: fmt(amount1),
            principalAtx: fmt(principalAtx),
            principalUsdt: fmt(principalUsdt),
            pendingFees: {
              atx: fmt(collectableAtxRaw),
              usdt: fmt(collectableUsdtRaw),
            },
            tokensOwed0: fmt(p.tokensOwed0),
            tokensOwed1: fmt(p.tokensOwed1),
            collectable0: fmt(collectable0),
            collectable1: fmt(collectable1),
            collectableAtx: fmt(collectableAtxRaw),
            collectableUsdt: fmt(collectableUsdtRaw),
          }, 2));
        }
      }
      break;
    }

    case "token-info": {
      const tokenAddr = args._[1];
      if (!tokenAddr) { console.error("Usage: query.js token-info <tokenAddress>"); process.exit(1); }
      const info = await client.query.getTokenInfo(tokenAddr);
      console.log(jsonStringify({
        address: info.address,
        name: info.name,
        symbol: info.symbol,
        decimals: info.decimals,
        totalSupply: fmt(info.totalSupply, info.decimals),
      }, 2));
      break;
    }

    default:
      console.error("Usage: query.js <price|balance|quote|positions|token-info> [args]");
      process.exit(1);
  }
});


path: scripts/swap.js
content: #!/usr/bin/env node
import { createClient, loadWallet, parseArgs, fmt, exitError, runMain, jsonStringify } from "./_helpers.js";
import { parseEther } from "atxswap-sdk";

await runMain(async () => {
  const client = await createClient();
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];
  const amount = args._[1];

  if (!command || !amount || !["buy", "sell"].includes(command)) {
    exitError("Usage: swap.js <buy|sell> <amount> [--from address] [--slippage bps] [--password <pwd>]");
  }

  const fromAddress = args.from || client.wallet.list()[0]?.address;
  if (!fromAddress) exitError("No wallet found. Create one first.");

  const wallet = await loadWallet(client, fromAddress, args);
  const amountWei = parseEther(amount);
  const slippage = args.slippage ? parseInt(args.slippage) : undefined;

  if (command === "buy") {
    const result = await client.swap.buy(wallet, amountWei, slippage);
    console.log(jsonStringify({
      action: "buy ATX",
      txHash: result.txHash,
      usdtSpent: fmt(result.amountIn),
      atxReceived: fmt(result.amountOut),
    }, 2));
  } else {
    const result = await client.swap.sell(wallet, amountWei, slippage);
    console.log(jsonStringify({
      action: "sell ATX",
      txHash: result.txHash,
      atxSold: fmt(result.amountIn),
      usdtReceived: fmt(result.amountOut),
    }, 2));
  }
});


path: scripts/transfer.js
content: #!/usr/bin/env node
import { createClient, loadWallet, parseArgs, exitError, runMain, jsonStringify } from "./_helpers.js";
import { parseEther } from "atxswap-sdk";

await runMain(async () => {
  const client = await createClient();
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];

  if (!command) {
    exitError("Usage: transfer.js <bnb|atx|usdt|token> <to> <amount> [tokenAddress] [--from address] [--password <pwd>]");
  }

  const fromAddress = args.from || client.wallet.list()[0]?.address;
  if (!fromAddress) exitError("No wallet found. Create one first.");

  const wallet = await loadWallet(client, fromAddress, args);

  switch (command) {
    case "bnb": {
      const to = args._[1];
      const amount = args._[2];
      if (!to || !amount) exitError("Usage: transfer.js bnb <to> <amount> [--from address]");
      const result = await client.transfer.sendBnb(wallet, to, parseEther(amount));
      console.log(jsonStringify({ action: "send BNB", to, amount, txHash: result.txHash }, 2));
      break;
    }

    case "atx": {
      const to = args._[1];
      const amount = args._[2];
      if (!to || !amount) exitError("Usage: transfer.js atx <to> <amount> [--from address]");
      const result = await client.transfer.sendAtx(wallet, to, parseEther(amount));
      console.log(jsonStringify({ action: "send ATX", to, amount, txHash: result.txHash }, 2));
      break;
    }

    case "usdt": {
      const to = args._[1];
      const amount = args._[2];
      if (!to || !amount) exitError("Usage: transfer.js usdt <to> <amount> [--from address]");
      const result = await client.transfer.sendUsdt(wallet, to, parseEther(amount));
      console.log(jsonStringify({ action: "send USDT", to, amount, txHash: result.txHash }, 2));
      break;
    }

    case "token": {
      const tokenAddr = args._[1];
      const to = args._[2];
      const amount = args._[3];
      if (!tokenAddr || !to || !amount) {
        exitError("Usage: transfer.js token <tokenAddress> <to> <amount> [--from address]");
      }
      const result = await client.transfer.sendToken(wallet, tokenAddr, to, parseEther(amount));
      console.log(jsonStringify({ action: "send token", [REDACTED_SECRET], to, amount, txHash: result.txHash }, 2));
      break;
    }

    default:
      exitError("Usage: transfer.js <bnb|atx|usdt|token> [args]");
  }
});


path: scripts/wallet.js
content: #!/usr/bin/env node
import {
  createClient,
  exportKeystore,
  getDefaultKeystorePath,
  parseArgs,
  fmt,
  exitError,
  runMain,
  resolveNewPassword,
  jsonStringify,
} from "./_helpers.js";
import { writeFileSync } from "node:fs";
import { resolve as resolvePath } from "node:path";

const DELETE_FORCE_PHRASE = "force delete wallet";

await runMain(async () => {
  const client = await createClient();
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];

  switch (command) {
    case "create": {
      const existing = client.wallet.list();
      if (existing.length > 0) {
        exitError(`Wallet already exists (${existing[0].address}). Only one wallet is allowed per skill instance.`);
      }
      const password = await resolveNewPassword(args);
      const name = args._[1];
      const result = await client.wallet.create(password, name);
      console.log(jsonStringify({
        action: "create",
        address: result.address,
        keystoreFile: result.keystoreFile,
        keystoreDir: getDefaultKeystorePath(),
        name: name || null,
        passwordSaved: result.passwordSaved,
        ...(result.passwordSaveError
          ? { passwordSaveError: result.passwordSaveError }
          : {}),
      }, 2));
      break;
    }

    case "list": {
      const wallets = client.wallet.list();
      if (wallets.length === 0) {
        console.log(jsonStringify({ wallets: [] }, 2));
        break;
      }
      const results = [];
      for (const w of wallets) {
        const entry = { address: w.address, name: w.name || null };
        try {
          const bal = await client.query.getBalance(w.address);
          entry.bnb = fmt(bal.bnb);
          entry.atx = fmt(bal.atx);
          entry.usdt = fmt(bal.usdt);
        } catch (e) {
          entry.balanceError = e?.shortMessage || e?.message?.split("\n")[0] || String(e);
        }
        results.push(entry);
      }
      console.log(jsonStringify({ wallets: results }, 2));
      break;
    }

    case "export": {
      const address = args._[1];
      if (!address) {
        exitError("Usage: wallet.js export <address> [--out <file>]");
      }
      const { keystore, keystoreFile } = await exportKeystore(client, address, args);
      const outPath = typeof args.out === "string" ? resolvePath(args.out) : null;
      const json = jsonStringify(keystore, 2);
      if (outPath) {
        writeFileSync(outPath, json);
        console.log(jsonStringify({
          action: "export",
          address,
          format: "keystore-v3",
          source: keystoreFile,
          output: outPath,
        }, 2));
      } else {
        console.log(json);
      }
      break;
    }

    case "forget-password": {
      const address = args._[1];
      if (!address) exitError("Usage: wallet.js forget-password <address>");
      await client.wallet.forgetPassword(address);
      console.log(jsonStringify({ action: "forget-password", address, success: true }, 2));
      break;
    }

    case "has-password": {
      const address = args._[1];
      if (!address) exitError("Usage: wallet.js has-password <address>");
      const saved = await client.wallet.hasSavedPassword(address);
      console.log(jsonStringify({ address, hasSavedPassword: saved }, 2));
      break;
    }

    case "delete": {
      const address = args._[1];
      if (!address) {
        exitError("Usage: wallet.js delete <address> --backup-confirmed yes --force-phrase \"force delete wallet\"");
      }
      if (args["backup-confirmed"] !== "yes") {
        exitError("Refusing to delete wallet: export and back up the keystore first, then rerun with --backup-confirmed yes");
      }
      if (args["force-phrase"] !== DELETE_FORCE_PHRASE) {
        exitError("Refusing to delete wallet: rerun with --force-phrase \"force delete wallet\" after the user explicitly sends that exact phrase");
      }

      await client.wallet.delete(address);
      console.log(jsonStringify({
        action: "delete",
        address,
        deleted: true,
        backupConfirmed: true,
        forcePhrase: DELETE_FORCE_PHRASE,
      }, 2));
      break;
    }

    default:
      exitError("Usage: wallet.js <create|list|export|forget-password|has-password|delete> [args] [--password <pwd>]");
  }
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)