Inspiration

Every team that touches crypto ends up with the same problem: someone has to be the one who actually holds the keys. Usually that means a single wallet, a single person, and a lot of trust concentrated in one place, or a multisig tool that lives outside of where the team actually works and talks. I wanted the opposite: treasury operations that live inside Slack, where the conversation already happens, without ever concentrating custody in one person, one device, one bot, or one process.

The idea came down to one simple rule I wanted to be true no matter what, not just something I promised to follow: the bot can propose things, but it can never approve them. If that stays true even if the bot's own system gets hacked, then everything else built on top of it, natural language, automation, other integrations, is safe.

What it does

Moot is a Slack-native treasury and execution platform for stablecoins and crypto on Solana. From inside Slack, a team can:

  • Pay people. Propose a transfer from a treasury, get an approval card, execute once threshold is met.
  • Create treasuries. A real Squads v4 multisig with a name, member list, approval threshold, optional timelock, and allowed tokens.
  • Run escrow. A payer, payee, and arbiter arrangement with a fixed amount and milestone. The payee can request release but can never approve their own payment.
  • Place holds. Any member can pause a proposal before it executes, even if fully approved, and a treasury admin releases it.
  • Gate GitHub PR merges. A governance-only multisig, no funds involved, that gates merging a specific pull request behind the same quorum engine used for payments, proving the pattern generalizes beyond money.
  • Talk to it in plain English. Almost everything above works by just mentioning @moot and describing what you want, not only through a slash command.

Every one of these still moves through the same custody model: a member's approval count $a$ has to satisfy $a \geq \tau$ against the real, live, on-chain threshold $\tau$ before anything executes, and that check is re-read at execution time, not trusted from an earlier step.

How I built it

The architecture comes down to one rule: only one process ever touches Solana, the database, or GitHub. The Slack app (slack-app, a Bolt app in Socket Mode) never signs a transaction, never opens the database, never calls the GitHub API directly. Every action it takes is a call to mcp-server over the Model Context Protocol. This meant that when it came time to add request auth, there was exactly one place to add it, and when it came time to make the whole thing multi-tenant, there was exactly one data layer to migrate.

I built the core loop first: create a treasury (a Squads v4 multisig), draft a proposal, get an approval card in Slack, collect wallet signatures through a small signing web view, execute once threshold is met, all checked against live on-chain state at each step. Escrow and holds came next. Both are really just the same basic idea underneath: a treasury with a certain set of permissions, so I didn't need to build anything new to make either one work.

Then I generalized the pattern on purpose: a GitHub PR merge, gated behind the exact same quorum-and-approval engine as a payment, with a Memo instruction instead of a transfer, to prove the security model is about authorization, not about money specifically.

After that came natural language: a single Claude-based intent classifier that turns a plain English mention into one of a fixed set of typed actions, never executing anything itself, only ever producing a draft or a clarifying question that still has to pass through the same confirmation flow a slash command would.

The last major phase turned Moot from a single-workspace tool into something anyone can install. I rewrote the storage layer from a flat JSON file to SQLite with team_id on every table, swapped a fixed bot token for Bolt's installationStore pattern, closed a webview information leak with short-lived signed tokens, and made the GitHub App multi-tenant too, one App, many installations, each workspace connecting its own repo owner through GitHub's own install flow.

Challenges I ran into

The hardest problems in the end were not the interesting ones.

I tried Oracle Cloud's Always Free tier first, since it is genuinely free forever rather than a 12-month trial, but account signup did not cooperate, so I moved to AWS instead. Getting SSH working took real debugging: a security group rule locked to the wrong IP produced a silent timeout, and testing over a VPN produced a different failure, a connection reset mid-handshake, which briefly looked like two separate problems until both turned out to be downstream of the same misconfigured rule. I fixed the IP, and it still timed out, which turned out to be because I had allocated an Elastic IP but never actually associated it with the instance, so all along I was connecting successfully to the instance's original, different public address without realizing it.

Once deployed, the whole thing froze mid-session, terminal included. CPU usage looked almost idle, which ruled out the obvious explanation and pointed instead at memory: a t3.micro has 1GB of RAM and no swap by default, and two Node processes plus a reverse proxy compiling a native SQLite binding during install were enough to exhaust it. A reboot and a 1GB swapfile was applied.

The very last bug was the smallest and the most instructive. A hero screenshot on the landing page was invisible on actual mobile devices but visible in Chrome's mobile device emulator. The cause was a CSS media query with a height threshold meant only for extreme landscape cases. It was a one-line fix once found, and a good reminder that a device simulator is a simulation, not the device.

Accomplishments that I'm proud of

  • The custody guarantee is not a policy, it is a permission bitmask. Moot's own signing key holds Initiate and Execute on every treasury it manages, never Vote, so it is architecturally incapable of approving its own proposals.
  • Natural language reached full feature parity with the slash commands, not a limited subset of them, while every action that moves funds or changes membership still goes through the identical confirmation or draft-then-approve path either way.
  • I closed an information leak in the webview API (a bare treasury and proposal ID was enough to view another workspace's proposal details) before it ever mattered, by going looking for it rather than waiting for it to show up.
  • Multi-tenancy is fully proven. I proved isolation with a test: two fake workspaces, same channel ID, same Slack user ID, same treasury name, and each only ever sees its own data.
  • The whole thing is actually live: OAuth install, DNS, TLS certificate, a workspace installing it through the public "Add to Slack" flow, not just a local demo.

What I learned

  • Squads v4's permission model does the hard work for you, if you use it honestly. A multisig member can hold Initiate and Execute without Vote. That single fact is the entire custody guarantee.
  • The check that matters is the one that runs last. Evaluating policy at draft time is for the user's benefit. The authoritative check happens again, live, against on-chain state, in the instant before execute_proposal runs.
  • Multi-tenancy is a data modeling problem before it is anything else. Bolt's OAuth installationStore makes per-workspace tokens almost free to add. The real work was making team_id a first-class column everywhere, including on tables keyed by an on-chain address that is already globally unique, so isolation does not depend on every future query remembering to filter correctly.
  • A capability leak does not have to be exploitable to be worth fixing. It only has to leak information to be worth closing.
  • Small servers lie about why they are broken. A frozen terminal looked like a network problem. It was memory. A missing image was one CSS threshold.

What's next for Moot

  • Approve and hold via natural language, with disambiguation when a treasury has more than one pending proposal open, since right now those two actions are deliberately still button and reaction driven.
  • Mainnet support, moving past devnet once the custody model has had more exercise.
  • More tokens beyond USDC, and a treasury admin management flow that does not require re-creating a treasury to change its policy.
  • A database migration if usage ever outgrows a single SQLite file on one VM, without changing anything above the storage layer, since nothing outside mcp-server knows the storage is SQLite in the first place.
  • Slack token rotation and Enterprise Grid org-wide installs are both deliberately left off for now to keep the multi-tenant migration scoped, both straightforward additions on top of the installationStore that already exists.
Share this project:

Updates