oswiki-sign
Local trading signer for OSWiki. It is a small MCP server that runs on your own machine, next to Claude Desktop, Cursor or Claude Code, and lets the AI list, buy, cancel, make offers and accept offers for you on Ethereum and ApeChain (Seaport 1.6).
The private key stays on your machine. The AI never sees it, the hosted OSWiki MCP never sees it, and the OSWiki API never sees it. Every order and transaction is built and checked locally against the chain before anything is signed.
Quick setup
npx -y @oswikixyz/sign setup
Run it in a terminal (Node.js 20 or newer). Five short steps:
- Wallet: creates a new trading wallet (recommended) or imports a private key at a hidden prompt, and stores the key in
~/.oswiki-sign/key(Windows:%USERPROFILE%\.oswiki-sign\key), readable only by you. The key is only shown if you choose to back it up. - Limits: one "Use recommended limits?" answer takes 0.05 ETH / 25 APE max per buy, floor guard 50%, 50 trades per day, dry run off and sends off; "n" asks for each: max per buy on each chain (or no buys), the floor guard (% of the floor), trades per day, dry run, daily send limits.
- Collections: any, only these (allowlist) or any except these (blocklist). Type slugs or
0xaddresses; setup looks them up on Ethereum and ApeChain, showsname (chain) 0x...for each and stores the addresses. Otherdeed always brings both of its contracts. Without the OSWiki API only raw0xaddresses are accepted. - Confirmation: every trade waits for your OK in oswiki-sign's own dialog (recommended), or the AI may trade on its own within your limits (
OSWIKI_CONFIRM_MODE=off, see Confirmation dialog). - Client: adds oswiki-sign to Claude Code (
claude mcp add --scope user), Cursor (~/.cursor/mcp.json) or Claude Desktop (claude_desktop_config.json), or prints the config. The config holds only the path of the key file and the exact version. For Claude Code it also adds the hardening deny rules to~/.claude/settings.json(backed up first;setup --no-hardeningskips that); for Cursor and Claude Desktop it prints the equivalent advice.
If an existing client config still holds the key inline (OSWIKI_WALLET_KEY), setup names that file and tells you how to move the key to a key file (choose "Import key" in step 1); oswiki-sign no longer starts with an inline key. Every file it changes is backed up first (<file>.bak-<time>), other MCP servers and settings are kept, and an existing key file is only replaced if you confirm it (it is then moved to key.bak-<time>). Run setup again to change anything; it keeps your wallet. Colors are off with NO_COLOR or when the output is not a terminal. npx -y @oswikixyz/sign setup --help shows the steps. To do everything by hand instead, see Manual setup.
Update
The client config pins the exact version on purpose: code that holds your key never updates by itself. When wallet_status or the server log says Update available, run in a terminal:
npx -y @oswikixyz/sign@latest update
It finds the oswiki-sign entry of Claude Code (user scope), Cursor and Claude Desktop, changes only the pinned version (env and settings stay as they are, files are backed up first), checks that the new version starts, and tells you which apps to restart. No prompts. The update check asks registry.npmjs.org at most once a day; OSWIKI_UPDATE_CHECK=0 turns it off.
How it works
- The AI calls a
preview_*tool. oswiki-sign reads the chain and the OSWiki/v1API, builds the exact order or transaction, runs every safety check and returns a preview: price, fees, royalty, payout, approvals, warnings. - You read the preview and tell the AI to go ahead.
- The AI calls
executewith theconfirmIdfrom that preview. oswiki-sign re-checks the counter, ownership and a simulation, then signs and sends exactly what was previewed. AconfirmIdworks once and expires after 10 minutes.
Tools: wallet_status, my_listings, my_offers, preview_listing, preview_buy, preview_cancel, preview_offer, preview_accept_offer, preview_transfer, preview_unwrap, execute, update_limits.
wallet_status lists every setting with its exact env name, current value and source (setup/env or chat), and how to change it.
One-call trades
The preview tools do their own lookups, so "buy the cheapest Arbinaut" is one preview_buy and one execute. Wherever a tool takes contract, it also takes collection: a slug, an exact collection name or a 0x address, resolved on the given chain. A name is accepted only when it exactly matches a verified collection; anything else is refused with the closest candidates. The card shows the collection name and its verified mark. Exact ids keep working everywhere.
While oswiki-sign waits for your approval in its dialog or on the browser page, it sends MCP progress notifications every 10 seconds when the client asked for them, so clients that reset their timeout on progress keep waiting.
Changing limits from the chat
update_limits changes the per-buy, daily, daily send, daily sell and below-floor caps, the floor guard, trades and cancels per day, dry run, the confirmation mode and the collection allow/block lists (0x addresses only). Every change, tighter or looser, is shown to you as old -> new in oswiki-sign's own dialog and applies only after you approve it there, in every confirmation mode. It then takes effect at once, no restart. Clients that cannot show the dialog get the browser approval page instead; only with that switched off (OSWIKI_BROWSER_APPROVAL=0) do you need to run setup again.
Approved changes are saved in ~/.oswiki-sign/settings.json, signed with a key derived from your wallet key, and win over the setup/env values. A file that fails that check (edited by hand, or from another wallet) is ignored with a warning. Re-running setup shows the chat changes and offers to reset them.
preview_cancel takes askIds (your OSWiki listings, including their linked OpenSea listings), bidIds (your offers) and orders: the order components of signed orders the OSWiki API rejected, exactly as an execute result printed them, so they can still be cancelled on-chain.
Confirmation dialog
When the AI calls execute, oswiki-sign itself opens a confirmation dialog in your client showing the exact trade. Only ticking "Sign and send this trade" sends it; the AI cannot answer this dialog. Claude Code shows the dialog automatically. If your client cannot show it (Claude Desktop and some Cursor versions), oswiki-sign opens the same approval as a page in your browser instead, see Browser approval page.
OSWIKI_CONFIRM_MODE=off switches the dialog off on purpose, so the AI can trade on its own: for trades you asked for, or under a standing instruction such as "buy any Gimboz under 2,500 APE". The AI is told it may execute when you asked for the trade or a standing instruction covers it, must stay within the limits and must report every executed trade. Be honest with yourself about what this means: nothing stops a trade except your limits. A manipulated listing name, web page or message that the AI reads could make it trade, up to your per-buy, daily, sell, below-floor and trade-count limits, in the collections you allow. oswiki-sign says so at startup, in wallet_status and under every preview. All other limits stay exactly as you set them. Use it only with a small wallet, tight limits and, ideally, an allowlist. In Claude Code, wallet_status warns in this mode when the hardening deny rules are missing from ~/.claude/settings.json.
Browser approval page
For clients without the dialog (no MCP elicitation), oswiki-sign opens a one-time approval page in your default browser when the AI calls execute, update_limits or a send. It shows the exact card and an Approve and a Decline button; the tool call waits for your answer. The page is served by oswiki-sign itself on 127.0.0.1 only, under a random 128-bit link that works once and expires after 5 minutes (unanswered = declined). Approving takes a POST from that page with a second per-approval secret, and requests with a foreign Host or Origin are refused, so neither the AI nor another website can approve for you. The link is never shown to the AI. An Approve that comes less than a second after the page was opened is refused (click it again). If another program on this computer opened and approved the link before your browser did, your browser shows a warning instead of the approval page; for that, the local server keeps answering an approved link for 10 minutes, otherwise it stops as soon as no approval is open. If the AI client cancels the tool call while the page is open, the approval is declined. If the browser cannot be opened, the approval is declined.
This page does not protect against an AI agent that has its own shell or terminal on this computer, unless that agent is sandboxed with access to localhost blocked: such an agent can open the page and approve it itself. The key file has the same limit, since an agent that can run commands as you can read it.
OSWIKI_BROWSER_APPROVAL=0 switches the page off: then clients without the dialog cannot send or change settings from the chat, and trades are refused unless you set OSWIKI_CONFIRM_MODE=ai (less safe: the AI's word then counts as your confirmation).
Selling limits
Listings and accepted offers spend nothing, but selling too cheaply loses value just the same. Two daily budgets per chain bound that, counted per UTC day and checked at preview and again at execute:
- Below the floor (
OSWIKI_DAILY_MAX_BELOW_FLOOR_ETH/_APE, default: the per-buy max of that chain): for every listed item and accepted offer, the floor minus the price counts (the full price when the collection has no floor), using the same all-marketplace floor as the floor guard. Without a per-buy max the budget is 0: selling below the floor is refused, at or above the floor still works. Example: with a 1 ETH budget and a 1 ETH floor, listing items at 0.51 ETH stops after the second one. - Value sold (
OSWIKI_DAILY_MAX_SELL_ETH/_APE, default 10x the per-buy max; no cap without a per-buy max): the price of every listed item and accepted offer counts, so mass-selling is bounded even at or above the floor.
Every listing and offer preview shows what both budgets have left after it; wallet_status shows what is used today.
Sending NFTs and funds
preview_transfer sends NFTs and funds from the trading wallet to one other wallet on one chain, in one approval: any number of NFTs (up to 50; ERC-721, and ERC-1155 with an amount), ETH/APE and WETH/WAPE. ERC-721 tokens go one safeTransferFrom each, ERC-1155 ids of one contract go in one safeBatchTransferFrom (or safeTransferFrom for a single id); the transactions are sent one after another with the usual pending-transaction safety.
-
Always your approval. Every send is shown to you in oswiki-sign's own dialog (chain, every item and amount, the full recipient address, its ENS name and any warnings) and only goes out when you approve it there. This holds in every confirmation mode, also with
OSWIKI_CONFIRM_MODE=offorai. Clients that cannot show the dialog use the browser approval page; with that switched off they cannot send. The AI is told to send only to an address or ENS name you gave it yourself. -
Recipient checks. The recipient is a full
0xaddress or an ENS name. A mixed-case address must pass its EIP-55 checksum (all-lowercase and all-uppercase are accepted). An ENS name is resolved twice, independently: through your Ethereum RPC and through the OSWiki API. Both must agree, or the name is refused; when the OSWiki API is unreachable, ENS names are refused and you are asked for the0xaddress. On ApeChain, a name's own ApeChain address (ENSIP-11, coin type 2147516787) is used when it is set (both sources must agree on it); without one, its Ethereum address is used only when it is a plain wallet (no code on Ethereum or ApeChain), and a name that points to a smart wallet is refused, since that wallet may not exist on ApeChain. The zero and burn addresses, the token's own contract, the WETH/WAPE contract, marketplace contracts and the wallet itself are refused. On ApeChain, an address that is a smart contract on Ethereum but has no code on ApeChain is refused (an Ethereum smart wallet may not exist on ApeChain). A contract recipient gets a loud warning, and so does an address that has never sent a transaction or held funds on that chain, and a wallet with an EIP-7702 delegation on that chain (the card names the delegate: drained wallets often use sweeper delegations that forward everything they receive). A reverse ENS name is shown as set by the recipient, not as proof of identity; names with non-ASCII or punycode characters get a look-alike warning. -
Checked on-chain first. Ownership (
ownerOf/balanceOf) and balances are read from the chain, and every transfer is simulated. When a collection does not let you send a token (for example ERC721-C transfer rules), or the simulation reverts, the preview names each item it affects instead of sending the rest silently. -
Limits. Sending ETH/WETH or APE/WAPE is off until you set a daily send limit (
OSWIKI_DAILY_MAX_SEND_ETH/OSWIKI_DAILY_MAX_SEND_APE, in setup or withupdate_limitsand your approval); sending NFTs works without one. Sent ETH plus WETH (APE plus WAPE) count toward that limit; every sent NFT counts as one trade towardOSWIKI_MAX_TRADES_PER_DAY;OSWIKI_BLOCKED_COLLECTIONSandOSWIKI_ALLOWED_COLLECTIONSapply. Gas limit caps per transaction: 90k (ERC-721), 80k (one ERC-1155 id), 60k + 40k per id (ERC-1155 batch), 70k (WETH/WAPE), 21k for ETH on Ethereum (so plain ETH to a contract wallet is refused: send WETH instead), 150k for APE on ApeChain. -
Sent but not confirmed. When a transaction was broadcast but its receipt did not arrive in time (or the RPC did not answer the broadcast), it is reported as SENT, not yet confirmed, with its hash: do not retry, check the explorer. It stays tracked as pending, and nothing new is sent on that chain until it confirms or is replaced.
preview_unwrap turns WETH/WAPE back into ETH/APE in the same wallet (withdraw). Nothing leaves the wallet, so it follows the normal confirmation mode.
Royalties
When the collection has on-chain ERC-2981 royalty info, the royalty in a preview is checked against it. When it does not, the royalty comes only from the OSWiki API, the preview labels it UNVERIFIED (from OSWiki API, collection has no on-chain royalty info), and oswiki-sign refuses to pay more than 10% on such a collection.
Before you start
- Use a separate trading wallet. Create a fresh wallet for oswiki-sign and fund it only with what you are willing to trade. Never use your main wallet or one that holds anything you do not plan to trade.
- Dry run if you like. With
OSWIKI_DRY_RUN=1previews work andexecutesigns and sends nothing; the confirmation dialog already stops every trade you do not approve. - Set the limits. Buys and offers stay off on a chain until its max-buy limit is set.
- Keep the hosted OSWiki MCP (
https://mcp.oswiki.xyz/mcp) installed as well: it is where the AI looks up collections, floors, listings and sales. oswiki-sign only trades.
Manual setup
Quick setup does all of this for you. oswiki-sign is published on npm as @oswikixyz/sign. It needs Node.js 20 or newer. Always pin the exact version (@oswikixyz/[email protected]), never latest or a range, and update only on purpose.
1. Store the key in a file
oswiki-sign reads the private key from a file (OSWIKI_WALLET_KEY_FILE). Never put the key itself in a config file, an environment variable or a command line; oswiki-sign refuses to start when OSWIKI_WALLET_KEY holds the key inline.
macOS / Linux (the key is typed at a hidden prompt, so it never lands in your shell history):
mkdir -p ~/.oswiki-sign && chmod 700 ~/.oswiki-sign
(umask 077; printf 'Private key: '; IFS= read -rs k; printf '%s\n' "$k" > ~/.oswiki-sign/key; unset k; echo)
Windows (PowerShell; the file is readable by your user only):
$d = "$env:USERPROFILE\.oswiki-sign"; New-Item -ItemType Directory -Force $d | Out-Null
$k = Read-Host 'Private key' -AsSecureString
[IO.File]::WriteAllText("$d\key", [Net.NetworkCredential]::new('', $k).Password); Remove-Variable k
icacls "$d\key" /inheritance:r /grant:r "${env:USERNAME}:R" | Out-Null
Why a file: a key in the MCP config's env block (or in your shell environment) sits in plain text in a file your AI client can read, and while oswiki-sign runs it is visible to every process of the same user (/proc/<pid>/environ on Linux, ps eww on macOS). With a key file only the path is in the environment; oswiki-sign reads the file once at start.
Use the absolute path of the key file below (Windows: "C:\\Users\\you\\.oswiki-sign\\key" in JSON).
2. Add it to your AI client
Claude Desktop (claude_desktop_config.json) and Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"oswiki-sign": {
"command": "npx",
"args": ["-y", "@oswikixyz/[email protected]"],
"env": {
"OSWIKI_WALLET_KEY_FILE": "/Users/you/.oswiki-sign/key",
"OSWIKI_DRY_RUN": "1",
"OSWIKI_MAX_BUY_ETH": "0.5",
"OSWIKI_MAX_BUY_APE": "500"
}
}
}
}
If your client on Windows cannot start npx, use "command": "cmd" with "args": ["/c", "npx", "-y", "@oswikixyz/[email protected]"].
Claude Code (user scope, so it is stored in ~/.claude.json and not in a project):
claude mcp add --scope user oswiki-sign \
-e OSWIKI_WALLET_KEY_FILE=/Users/you/.oswiki-sign/key \
-e OSWIKI_DRY_RUN=1 \
-e OSWIKI_MAX_BUY_ETH=0.5 \
-e OSWIKI_MAX_BUY_APE=500 \
-- npx -y @oswikixyz/[email protected]
On native Windows, Claude Code needs cmd /c to start npx: end the command with -- cmd /c npx -y @oswikixyz/[email protected].
Check the setup from a terminal with the same environment variables set:
npx -y @oswikixyz/[email protected] status
When the dry-run previews look right, remove OSWIKI_DRY_RUN (or set it to 0) and restart the client.
Claude Code hardening
oswiki-sign's limits and its confirmation dialog only help if the agent cannot quietly change its own setup. Setup adds the deny rules below to your user settings (~/.claude/settings.json) when you pick Claude Code (setup --no-hardening skips that); to do it by hand, put this there so it applies to every project:
{
"permissions": {
"deny": [
"Read(~/.oswiki-sign/**)",
"Edit(~/.oswiki-sign/**)",
"Read(~/.claude.json)",
"Edit(~/.claude.json)",
"Edit(~/.claude/settings.json)",
"Edit(~/.claude/settings.local.json)",
"Edit(//**/.claude/settings.json)",
"Edit(//**/.claude/settings.local.json)",
"Edit(//**/.mcp.json)"
]
},
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/.oswiki-sign"],
"denyWrite": ["~/.oswiki-sign"]
}
}
}
~/is your home directory and//the filesystem root, so the//**/rules match a project's.claude/settings.json,.claude/settings.local.jsonand.mcp.jsonin any folder (on Windows across all drives). A single leading/would mean the folder of the settings file, not the root.- Blocking edits to Claude Code's settings and MCP files stops an agent from adding an
Elicitationhook that auto-answers the confirmation dialog, switchingOSWIKI_CONFIRM_MODE, raising the limits or swapping the server. Blocking~/.oswiki-signkeeps the key file and the daily counters out of reach. - Permission rules cover Claude's file tools and the shell commands Claude Code recognizes (
cat,head,sed,tee, redirections), not a script that opens files itself (for examplenode -eorpython -c). The sandbox closes that gap at the OS level for every shell command and its child processes, so keep it on (default above;/sandboxshows its state). It runs on macOS, Linux and WSL2; on native Windows Claude Code runs commands unsandboxed, so run Claude Code inside WSL2 if you trade with it on Windows. allowUnsandboxedCommands: falsestops Claude from retrying a blocked command outside the sandbox.
Cursor, Claude Desktop and other clients have their own settings; the same idea applies: the agent must not be able to read the key file or edit the MCP config.
Verify the package
Every release is built and published by the release workflow of this repository with npm provenance, from a v* tag on main. No release is built on a personal machine.
- The package page on npmjs.com shows a provenance badge linking to the exact commit and workflow run.
npm audit signaturesin a project that installed@oswikixyz/sign@<version>checks the registry signature and the provenance attestation.- The package has no runtime dependencies and no install scripts: everything is bundled into
dist/index.js.
Environment
Otherdeed has two contracts
Otherdeeds live in two collections on Ethereum:
If you restrict trading with OSWIKI_ALLOWED_COLLECTIONS and want to trade Otherdeeds, list both:
OSWIKI_ALLOWED_COLLECTIONS=0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258,0x790b2cf29ed4f310bf7641f013c65d4560d28371
Safety
-
Separate wallet. Fund it with what you plan to trade, nothing more.
-
Key file, not the key. Use
OSWIKI_WALLET_KEY_FILEand keep the agent away from~/.oswiki-signand its own config (see Claude Code hardening). -
Set the max-buy limits. Buys and offers stay off on a chain until its
OSWIKI_MAX_BUY_*is set; pick the most you would ever spend in one trade. -
Dry run to try it out. Try a listing, an offer and a buy with
OSWIKI_DRY_RUN=1and read the previews. -
Never paste the key into a chat. oswiki-sign never asks for it, and the AI must not either.
-
Read every preview. Nothing happens until you approve the trade in the confirmation dialog or browser approval page (unless you turned confirmation off).
-
Confirmation off means the AI trades alone. With
OSWIKI_CONFIRM_MODE=offyour limits are the only brake: keep the wallet small, the caps tight and an allowlist set. -
Pin the exact version. Never run an unpinned or "latest" oswiki-sign.
-
What oswiki-sign enforces in code, whatever the AI or the API say:
- only Seaport 1.6 and the OpenSea conduit; fee exactly 1% to the known OSWiki fee recipient of the chain; royalty at most 25%, and at most 10% when it is unverified;
- Gondi listings are bought only as plain Seaport 1.6 orders (zone 0, no conduit) for exactly the NFT asked for, with the 0.5% Gondi fee to Gondi's known fee recipient and every payment in one currency (ETH, or WETH through an exact approval to Seaport);
- approvals only to the OpenSea conduit (your NFTs) and to Seaport for WETH/WAPE, for the exact amount, never unlimited;
- transactions only to Seaport, WETH/WAPE, or
setApprovalForAllon the collection being traded; Seaport buys deliver only to this wallet and never through a fulfiller conduit, and accepted offers pay only this wallet; transfers and plain value sends only to the one recipient you approved in the dialog; - every send to another wallet needs your approval in oswiki-sign's own dialog, in every confirmation mode;
- the Seaport counter is always read from the chain (never assumed), purchases are re-encoded locally and must match the listing's own hash and exact price, and every transaction is simulated first;
- prices are explicit numbers: no listing at 0, no guessed floor;
- every
executeneeds your confirmation in oswiki-sign's own dialog or browser approval page (unless you choseOSWIKI_CONFIRM_MODE=aion a client without the dialog and withOSWIKI_BROWSER_APPROVAL=0, oroff); - every setting change from the chat needs your approval in that dialog or page, in every mode;
- blocked collections are refused in every mode; the allowlist, if set, too;
- the limits above, daily value caps per chain (buys and offers, sends, value sold, value given up below the floor), gas limit and fee caps per transaction, and no resend while an earlier transaction may still land;
- the key is read only from a key file, never from an inline
OSWIKI_WALLET_KEY; - the daily counters and the chat settings are signed with a key derived from your wallet key, so editing
state.jsoncannot raise the counters and an editedsettings.jsonis ignored.
The limits are local to your machine: anything that can write files as your user (including an agent with shell access) can reset them by deleting
~/.oswiki-sign/state.json, or change them in the MCP config. That is why the Claude Code hardening deny rules and the sandbox matter; the per-trade caps (OSWIKI_MAX_BUY_*) and the separate wallet are what bound the worst case.
Report security issues privately to [email protected], not in a public issue.
Development
npm ci
npm run typecheck
npm test
npm run build # esbuild bundle into dist/index.js
npm run check:pack # the package may contain only package.json, README.md, LICENSE, dist/index.js
npm run test:bundle # runs the bundle without node_modules
tests/fixtures/seaport-vectors.json holds the Seaport test vectors shared with the OSWiki website and backend; it must not be edited by hand.
Releasing
- Bump
versioninpackage.jsonandpackage-lock.json(npm version <x.y.z> --no-git-tag-version), merge tomain. - Tag that commit on
mainand push the tag:git tag v<x.y.z> && git push origin v<x.y.z>. - The release workflow checks that the tag equals
v+ thepackage.jsonversion and that the commit is onmain, runs all checks, then stages the version on npm with provenance (npm stage publish) through npm trusted publishing (no npm token is stored anywhere). The trusted publisher only allows staging, so nothing goes live from CI alone. - A maintainer approves the staged version with 2FA, in the Staged Packages tab on npmjs.com or with
npm stage approve <stage-id>. Only then is it live. The job summary links the steps. A reviewer on the GitHubnpmenvironment is optional, as an extra gate before the workflow runs. - Update the pinned version in the install snippets and on the OSWiki developer docs.