Search projects, hackathons, sponsors and tech

    ← All projects

    Swish

    Only wallet you will ever need for agentic economy, wallet where your agents are the account holders

    Demo

    About

    Swish is an allowance wallet for AI agents. An agent can only spend what it said it would spend it on. THE PROBLEM. Giving an agent a wallet is giving a wallet to no one. The usual answer is a spending limit, which bounds how much and says nothing about to whom or for what. The Grok/Bankr wallet was drained for $330K in March 2025 and $175K again in May 2026; a guardrail was built after the first attack and did not survive a later rewrite, because it lived in application code rather than in what could actually move the money. HOW IT WORKS. Before anything is signed, the agent declares exactly what it is about to pay — recipient, asset, maximum amount, expiry, reason — as a typed object on Sui. Swish then simulates the agent's real transaction and diffs the actual on-chain effects against that declaration. An undeclared beneficiary, a capability grant slipped into the same transaction block, or an asset the declaration never named is a violation, and a violation means no proof object is ever minted. WHY IT HOLDS. In allowance_vault.move, grep for "balance::split" finds three call sites. Two are execute_declared and execute_with_override; both take a Declaration and a proof object by value and destroy them on the way in, so there is no boolean to forget to check and no path a future refactor can quietly drop. The third is owner_withdraw, which asserts the caller is the vault owner and touches no agent machinery at all. WHAT IS LIVE. The Move package is published and upgraded in place on Sui testnet (0x896cf6e9...), with a shared Vault<SUI> at 0xa8db67c3... and 16 unit tests passing, negative cases written first. Both execution paths have been exercised on chain. SPONSOR TECHNOLOGIES. Sui is the whole enforcement layer: linear types make "this permission no longer exists" a fact the compiler checks, simulateTransaction over gRPC produces the effects that get diffed, and ed25519_verify checks the attestation on chain. World is the human gate — when a payment breaches its envelope it stops for one verified person rather than whoever holds a key, using IDKit v4 uniqueness proofs, with the nullifier recorded so a replay is recognised as the same human instead of dead-ending. Intercepta screens the counterparty before a signature exists rather than after; a known exploiter address returns 90/100, High, MALICIOUS_ADDRESS, and the payment never gets signed. THE WALLET AROUND IT. Each agent carries an editable AGENT.md brief fed to the model as instructions before every run. Guardrails cover per-payment cap, rolling window, daily and per-counterparty ceilings, risk threshold and allowed assets, plus limits the operator names themselves, each measured by the same engine and with the operator's own wording becoming the text of the refusal. Money lives in one vault and each agent gets a bounded allocation from it. The agent loop runs on Groq with tool order enforced by what the model is able to call, not by asking it nicely. Swish speaks MCP in both directions: it exposes a server so someone else's agent can ask this wallet for money and be told no, and it registers outside MCP servers your own agents call out to, with per-agent, per-tool permissions that form the tool set rather than filtering it. It ships as a Chrome MV3 extension registering through the Wallet Standard, so any dApp that can connect to a Sui wallet can connect to an agent with an allowance instead — demonstrated against an included storefront that knows nothing about Swish and simply gets refused. THE HONEST BOUNDARY. Swish does not judge intent. An agent that honestly declares a bad payment to an address you already cleared will execute. What it guarantees is that every outflow is either exactly what was declared to someone you pre-approved, or it stopped and asked a verified human. Testnet only.

    How it's made

    Sui Move 2024 for the contracts. Next.js 16 (App Router, React 19, Tailwind v4) for the wallet. @mysten/sui v2 and @mysten/dapp-kit for chain access. AI SDK v6 ToolLoopAgent on Groq for the agent loop. A Chrome MV3 extension for the Wallet Standard surface. THE CONTRACTS. Four modules: allowance_vault, declaration, proofs, agent_cap. The design rule is that a permission is an object that gets destroyed, not a flag that gets checked. Declaration and MatchProof/OverrideApproval are passed by value into execute_declared/execute_with_override and consumed there, so double-spending a permission is a type error rather than a bug you have to remember not to write. Attestations are signed off-chain with Ed25519 and verified on-chain with ed25519_verify, binding the proof to the exact effects digest we simulated. 16 unit tests, negative cases first. THE THING THAT COST THE MOST TIME. Sui testnet has fully retired JSON-RPC. Every method 404s, not just the exotic ones, which we only found after funding an address and seeing nothing. The whole chain layer was rewritten onto SuiGrpcClient, with simulateTransaction replacing devInspect/dryRun. That has a knock-on effect: dapp-kit's SuiClientProvider still builds a JSON-RPC client internally, so the demo storefront cannot broadcast in the browser. It posts the signed bytes to its own backend and submits over gRPC, which is what a real dApp backend does anyway and is genuinely Swish-agnostic: it takes bytes and a signature from any Sui wallet. HAND-ROLLED WALLET STANDARD. An injected page script cannot use npm imports without a bundler, so inpage.js dispatches wallet-standard:register-wallet and answers wallet-standard:app-ready directly. About twenty lines instead of a build step. A FINDING WORTH FLAGGING. dapp-kit's useSignAndExecuteTransaction never calls a wallet's sui:signAndExecuteTransaction. It always goes through sui:signTransaction and broadcasts the result itself. We had implemented sign-only as a deliberate refusal, which silently made the wallet useless to almost every dApp. The fix answers the original worry properly: before checking anything, Swish resolves the gas coin the site cannot know about, sets the transaction to expire at the end of the current epoch so a held signature cannot be replayed indefinitely, builds once, then simulates, diffs and signs those exact bytes. The Wallet Standard returns bytes alongside signature precisely so a wallet can hand back something different from what it was given. WHAT EACH PARTNER TECHNOLOGY ACTUALLY BOUGHT US. Sui is the only reason the guarantee is structural: linear types make "this permission no longer exists" a compile-time fact, and simulateTransaction gives the pre-flight effects that make the diff possible. World turns "ask a human" into one specific verified person rather than whoever holds a key; IDKit v4 took seven bugs to get right, the last being that action: "" is falsy in JavaScript, which silently swallowed the widget, and that a nullifier_replayed refusal is World recognising you. We now read the nullifier out of that refusal and treat it as recognition rather than a dead end. Intercepta screens counterparties before a signature exists rather than after; it is mainnets-only, so screening runs against a real EVM exploiter address and genuinely returns 90/100 MALICIOUS_ADDRESS. HACKY BITS, STATED PLAINLY. The vault's balance is decoded from raw BCS at a fixed byte offset: Vault<T> is id: UID (32 bytes) followed immediately by Balance<T> (one u64, little-endian), so bytes 32..39. gRPC returns object content as bytes and pulling in a full layout resolver was not worth it. The code says so and names the field-order assumption that would break it. Topping up the vault runs as a single programmable transaction because sui client split-coin needs a gas coin that is not the coin being split, so on a single-coin address it fails with "insufficient SUI balance" while sitting on plenty; splitting from gas inside a PTB sidesteps that and costs one gas payment instead of two. owner_withdraw shipped via sui client upgrade using the recorded UpgradeCap rather than a fresh publish, because republishing mints a new type identity and would have orphaned the live vault, its funds and its on-chain allow-list. AGENT KEYS. Real Ed25519 keypairs sealed with AES-256-GCM under an env secret and persisted to a server-side keystore. That last part was a bug fix: the browser's backup snapshot correctly strips secrets, so restoring from it handed back agents with no key, and every dev-server restart was permanently orphaning funded addresses. THE AGENT LOOP. prepareStep and activeTools restrict the model to exactly the next tool in sequence, so ordering is enforced by what it is able to call rather than by prompt instruction. Groq's free tier runs it; Anthropic is the fallback when the 8k-tokens-per-minute limit bites. Each agent's AGENT.md brief is composed into the instructions ahead of the hard rules, which are then restated after it, so a brief can narrow what an agent intends but never widen what it may do. MCP IN BOTH DIRECTIONS. A server exposing five tools so an outside agent can ask this wallet for money and be refused, and a client registering outside servers whose tools are granted per agent and per tool. Registration probes the server immediately with initialize then tools/list, and handles streamable-HTTP servers that answer a plain POST with an SSE frame, where a bare JSON.parse chokes on the data: prefix.