mm-plugin-unison
v0.1.2
Published
Unison for MetaMask Agent Wallet: sealed batch auctions on Monad, priced at Chainlink's next observation, with a receipt your agent checks from the chain itself.
Maintainers
Readme
mm-plugin-unison
Unison for MetaMask Agent Wallet. Your agent trades WMON and tokenized stocks on Monad mainnet in sealed batch auctions. Each order's price doesn't exist when it is sent: the auction prices it at Chainlink's first observation after the order's block, at one price for everyone in the auction. Then the agent checks its own receipt from the chain.
An agent wallet made by MetaMask's mm 7.0.0 traded on Monad mainnet on 6 October 2026 (the whole run):
$ mm unison order WMON sell 10
Intent: Unison: sealed sell of 10 WMON at ≥ 0.028502 AUSD, priced at Chainlink's next observation
Tx submitted: https://monadvision.com/tx/0xdeb070f9b98975fe8f2cf51b8f485876752a7d7169e1daa5e36a47bf119d7039
Sealed in block 111160954. Its price doesn't exist yet: the auction prices at Chainlink's first observation after this block.
Priced: 0.028605 AUSD, one price for everyone in the auction (Chainlink: 0.028659).
Receipt: 6 of 6 checks pass against Chainlink's own history. https://www.unisonfi.com/receipt/mainnet/1/111160954
Hint: Sold 10 WMON at 0.028605 AUSD, the auction's one price; receipt 6/6 verified: https://www.unisonfi.com/receipt/mainnet/1/111160954The order was sealed at 22:48:23 UTC. Chainlink's oracles observed MON 8 s later, and the auction cleared at that price. The agent then checked all six facts from the chain, in 45 seconds end to end. It bought 9 WMON back the same way, then withdrew. The plugin never touched a key: MetaMask's agent wallet signed each transaction, showing its intent first.
The standing challenge, read by the same CLI. These are the same house sniper running the same strategy on both pots:
$ mm unison challenge score --address 0xcEc80166Ab48cb3C4ebD98671524761b1fd81276
8 of 30 counted fills, edge -21.2 bp (0.001469 AUSD lost); the pot pays above 2 bp. Not yet. Pot: 18 AUSD.
$ mm unison challenge score --rule old --address 0xcEc80166Ab48cb3C4ebD98671524761b1fd81276
8 of 30 counted fills, edge 12.29 bp (0.000853 AUSD); the pot pays above 2 bp. Not yet. Pot: 1 AUSD.It loses on Unison's rule and wins on the old one.
Why an agent wants this
An agent that trades on a venue priced by an oracle can be sniped. A faster bot sees the next price before it lands on chain and trades against the agent's stale quote. It can also snipe others, and then a person has to trust it didn't. On Unison neither can happen: the order is sealed before its price exists, and the receipt proves it.
- Fair by construction. Every order in an auction gets the same price: Chainlink's first observation after the newest order was sealed. Being first buys nothing.
- Verified, not trusted.
mm unison receiptre-derives the price from Chainlink's own history and recomputes the receipt hash, from the chain alone.mm unison orderdoes it for every fill. - A standing challenge for agents. An on-chain pot pays any account whose fills beat Chainlink's next observation over enough trades. The contract checks every markout.
mm unison challenge …enters it, on Unison's market or on a control market kept on the old rule.
Commands
| Command | What it does | Capabilities |
|---|---|---|
| unison markets | Each market's state, Chainlink's newest observation, the last auction's price | none |
| unison quote <market> <side> <qty> | The limit, what the order locks, the fee; before signing anything | none |
| unison receipt <tx \| link \| market upTo> | Checks an auction from the chain: the receipt hash, and the price's place in Chainlink's history | none |
| unison balance | Wallet and Unison balances, MON for gas, open order slots | wallet-read |
| unison deposit <amount> [token] | Approves exactly that amount, then deposits it to the Unison balance. For WMON, first wraps any shortfall from the wallet's MON, keeping half a MON for gas | wallet-read, wallet-submit |
| unison order <market> <side> <qty> | Seals an order, waits for its auction, settles it, verifies the receipt | wallet-read, wallet-submit |
| unison claim | Claims settled orders the keeper hasn't | wallet-read, wallet-submit |
| unison withdraw <amount\|all> [token] | Back to the wallet | wallet-read, wallet-submit |
| unison challenge open\|fund\|order\|settle\|score\|claim\|withdraw | Enter the standing challenge, trade through the challenge account, check the score, claim the pot | wallet-read (score), wallet-submit (the rest) |
Every command takes --json. skills/unison/SKILL.md teaches an agent the golden path, the fields to read and what each error code means.
How it uses Agent Wallet
The plugin never touches a key. Every write is one
ctx.walletExecutor(io, commandId)request:kind: "transaction",chainId: 143;- calldata built from Unison's ABIs;
- a gas limit from
estimateGasplus 12%, since Monad charges the limit; - an intent a person approving it can read, e.g. "Unison: sealed buy of 10 WMON at ≤ 0.0288 AUSD, priced at Chainlink's next observation".
Guard mode asks the person; Beast mode signs within its policy.
Writes stop before signing anything that would fail. A call that would revert is decoded against Unison's errors and stopped with a hint, e.g. "Deposit at least 0.29 AUSD first: mm unison deposit 0.29 AUSD".
Reads go through
ctx.publicClient(143)in commands that write. Read-only commands use Monad's public RPC and need no sign-in.Capabilities are per command.
markets,quoteandreceiptdeclare none. Writes declarewallet-submitonly where they send.Approvals are exact. Deposits approve the amount deposited, never an unlimited allowance.
One self-contained bundle. The Unison SDK, viem and the mainnet deployment record are bundled into readable, unminified
dist/, so the plugin installs with no dependencies. The host's@metamask/agent-wallet/pluginstays external, as the host requires.
Install
From npm (once published):
mm config set experimentalPlugins true
mm plugins install mm-plugin-unisonFrom this repository:
pnpm install
pnpm --filter mm-plugin-unison run stage # builds, writes oclif.manifest.json, and stages the package outside the repo
mm config set experimentalPlugins true
mm config set experimentalAllowUnverifiedInstalls true
npm_config_install_links=true mm plugins install "file:<the staged path it prints>" --accept-permissionsThe stage script also prints the command for PowerShell.
The plugin is staged outside the repository, and installed as a copy, on purpose. Node resolves a plugin's @metamask/agent-wallet/plugin import from where the plugin really lives:
- Installed in place, it would find this package's own dev copy of the CLI, which the host warns breaks plugins.
- Linked, npm's default for
file:, it would find no copy of the host at all.
Copied into mm's plugin directory, as an npm install does, it finds the running CLI, which mm links in beside its plugins. minCliVersion is >=6.2.0: the host checks it with semver.satisfies, and the template's ^6.2.0 would refuse today's mm 7.
The contracts it calls (Monad mainnet, chain 143)
| Contract | Address |
|---|---|
| UnisonExchange | 0x1696170d40E703F1378989383c21Ec96ED1Adf75 |
| ChainlinkCausalReference | 0xB161400dDfC592fD66b57dDaE46966ED74Ce891d |
| LatencyChallenge (causal) | 0xDcD3E86518db6A40C4feBa576efff598cA3B90d1 |
| LatencyChallenge (old-rule control) | 0x5Ce9D9f491E2d16c94F56eD09BEa23e7109976e9 |
All are verified on Sourcify. The plugin reads them from deployments/monad-mainnet.json at build time.
Tests
pnpm --filter mm-plugin-unison testThe unit tests cover:
- order planning: limits rounded toward the trader, the exchange's exact buy-lock arithmetic, refusals;
- selecting the agent's wallet from the host's state;
- decoding reverts into hints;
- receipt lookups.
test/fork.test.ts runs the write paths through the same Writer interface against an anvil fork of Monad mainnet, when FORK=1.
