Polydesk
Eight AI agents on a visual canvas that analyse prediction markets and DeFi positions, propose trades, and execute them under risk limits.
A working prototype, built in five weeks in spring 2026. The product, the agents' analysis and the transaction builders are real. The automatic execution path is only partly connected, and I list exactly where below. The demo at polydesk.net runs on sample data with no trading keys and executes nothing.
| Role | Year | Kind | Try it |
|---|---|---|---|
| Concept, product and brand design, engineering | 2026 | AI agents, trading | polydesk.net |
| 8 | agents: four for prediction markets, four for DeFi |
| 324 | commits between 19 March and 25 April 2026 |
| 128 | backend test functions in 16 files |
The problem
Prediction markets move on news before most traders have read it, and DeFi positions need watching around the clock. The tools for both assume a person at a screen. An agent can watch, but letting an agent spend money needs three things most demos skip: a reason a person can read, a limit the agent cannot argue with, and a clear line between proposing a trade and making it.
What I built
- Eight agents with one shape. Each agent takes a market or a position and returns the same thing: a recommendation, a confidence between 0 and 1, the reasoning in words, and the signals behind it. Four work on Polymarket (Analyst, Scout, Momentum, Sentinel) and four on DeFi (Yield Farmer, Liquidation Guardian, Arbitrageur, Airdrop Hunter).
- A canvas as the workspace. Wallets, markets, protocols, agents and results are nodes. You drag a market on, attach an agent, chain agents together, and watch results arrive. Nine templates and a demo mode load a working desk.
- No wallet needed to start. You sign in with an email, Google or a social account, and a wallet is created for you. People who already have a wallet connect it instead.
- Two ways to execute, and a third half-built. By default the person signs every trade in their own wallet and the server never holds a key. A second mode keeps an encrypted key on the server and signs for you. A third, signing through a Coinbase MPC wallet so no key is held at all, is written but not connected.
- Transaction builders for six protocols. Aave v3, Compound v3, Lido, Jupiter, Marinade and marginfi, each producing real calldata.
- A news pipeline feeding the agents. Twelve scheduled jobs pull news, social posts, market snapshots, protocol yields and oracle prices, then classify and score them.
- The product around it. Sign-in with email or a wallet, card funding, a Telegram bot for alerts, billing, a trader leaderboard, and the brand.
How it is structured
Signing in without a wallet
Most crypto products lose people at the first screen, which asks for a wallet they do not have. Polydesk uses Privy for sign-in, set to create an embedded wallet for anyone who arrives without one.
| Step | What happens |
|---|---|
| Sign in | Email, Google, X, Discord, Farcaster, or an existing wallet |
| Wallet | Created on first login for users without one. Polygon is the default chain; Ethereum, Arbitrum, Optimism and Base are enabled |
| Session | The interface sends its Privy token to the API, which looks the user up with Privy and issues its own session token |
| Own wallet | Sign-In with Ethereum: the server issues a one-time nonce, the wallet signs a message containing it, and the server checks the signature and retires the nonce |
| Funding | A card on-ramp (MoonPay) in the interface, or a transfer from another wallet |
How a trade reaches the chain
| Venue | How the transaction is built | How it is signed and sent |
|---|---|---|
| Polymarket | An order for the central limit order book, on Polygon, placed through the user's Polymarket proxy wallet | Manual: the person signs in the browser. Automatic: the server signs with the user's stored Polymarket credentials and posts the order |
| Aave v3, Compound v3, Lido | ABI-encoded calldata for supply, withdraw, repay or stake. An approve is added only when the current allowance is too small | Gas is estimated with a 20% margin; the transaction is signed, sent, and waited on for a receipt |
| Jupiter, Marinade, marginfi (Solana) | Jupiter returns a quote and then a serialized transaction for that route, with a slippage limit of 0.5% by default | Signed with the user's keypair and sent |
Multi-step actions are built as a list and run in order, stopping at the first failure: approve, then supply, for example. Every transaction is written to the database as pending before it is sent and updated with the hash and the outcome after.
The Liquidation Guardian has three thresholds on a lending position's health factor: warn at 1.3, repay a quarter of the debt at 1.15, repay half at 1.05.
In the code
These are short excerpts from Beach-Bum/Polydesk, which is private.
Every Polymarket agent returns this. reasoning is what the person reads on the canvas before approving; signals is the same judgement as data.
@dataclass
class AgentAnalysis:
"""Output of an agent's analysis step."""
market_id: str
agent_type: str
recommendation: str # BUY_YES, BUY_NO, SELL_YES, SELL_NO, HOLD, EXIT
confidence: float # 0-1
reasoning: str # Human-readable explanation
signals: dict = field(default_factory=dict) # Structured signal data
suggested_size: float = 0.0 # Suggested position size in USDC
suggested_price: float = 0.0 # Suggested entry/exit priceThe limits sit in the executor, not in the agent, so no agent can talk its way past them. A blocked trade is logged with the reason.
@dataclass
class RiskLimits:
"""Per-agent risk controls."""
max_bet_usd: float = 100.0
daily_loss_limit_usd: float = 500.0
max_open_positions: int = 10
def check_risk_limits(self, request: TradeRequest, deployment_id: str) -> str | None:
if request.size > self.risk_limits.max_bet_usd:
return f"Trade size ${request.size:.2f} exceeds max bet ${self.risk_limits.max_bet_usd:.2f}"
...
if daily_pnl < -self.risk_limits.daily_loss_limit_usd:
return f"Daily loss limit reached ..."The Sentinel agent is plain arithmetic, with no model in it: a stop-loss and a trailing stop.
self.stop_loss_pct = self.config.get("stop_loss_pct", 0.20) # 20% loss = exit
self.trailing_stop_pct = self.config.get("trailing_stop_pct", 0.15) # 15% from high
stop_loss_triggered = pnl_pct <= -self.stop_loss_pct
trailing_stop_triggered = drawdown_from_high >= self.trailing_stop_pct and hwm > entry_priceWhere a model is used, and where it is not
| Step | Done by | Why |
|---|---|---|
| Classifying a news item | A model | Reading language is what it is for |
| Estimating a probability | A model, in five fixed steps: decompose, base rates, key factors, weigh evidence, calibrate | The steps make the reasoning inspectable |
| Finding related markets | Local embeddings and a vector store | No model call, so it is fast and costs nothing per search |
| Stop-loss, trailing stop, health factor | Arithmetic | A rule about money should give the same answer every time |
| Risk limits | Code in the executor | The agent proposing a trade must not be the one that allows it |
| Signing | The person's wallet by default | The server should not hold a key it does not need |
The stack
| Part | Built with |
|---|---|
| Interface | React 19, TypeScript, Vite, a node canvas (@xyflow/react) |
| Wallets and sign-in | Privy, wagmi, viem |
| API | FastAPI, SQLAlchemy, PostgreSQL |
| Agents and pipeline | Python, the Claude API, ChromaDB with local sentence embeddings |
| Chain | Polymarket's order book client, web3.py, solders, Coinbase AgentKit |
| Prices | Pyth, Chainlink, Birdeye, DeFiLlama, behind one oracle with a 10-second cache |
About 25,000 lines of Python and 47,000 of TypeScript.
What a DeFi engineer would find
I read the execution code line by line before writing this page. These are the things I would expect a reviewer to raise, most serious first.
Not connected
- MPC signing is never called. Launching an automatic strategy creates a Coinbase MPC wallet and stores its id, and the executor has a function to sign with it. Nothing passes the id to the executor, so automatic execution always takes the stored-key path.
- The Liquidation Guardian does not watch the chain. The code to read a live health factor exists and is used by the wallet scan. The scheduled job that runs the guardian reads the health factor saved when the agent was deployed, so it re-checks an old number every five minutes.
- Arbitrage is one leg, on one chain. The scanner finds round-trip spreads on Solana and builds a single swap. The EVM scanner returns nothing, and the cross-chain scan compares oracle prices without costing the bridge or the fill. Nothing is atomic.
Security
- One of the two key-encryption modules falls back to a default key if its setting is missing. The other refuses to start in production without one. Both should refuse.
- The encryption is labelled AES-256 and is Fernet, which is AES-128 with an HMAC. Sound, but mislabelled.
Risk limits
- Three of five DeFi limits are never read. Position size, daily loss and slippage are declared; the executor checks only the allowed protocols and chains.
- The Polymarket position limit is never read, and the daily loss limit is really a daily net-spend limit: it adds up buys and sells, not realised losses.
Transaction handling
- A Solana transaction is reported as successful if it is still unconfirmed after 30 seconds, and a confirmed-but-failed one is not told apart from a good one.
- EVM transactions use a legacy gas price, take the nonce from the latest block and not the pending one, are not simulated before sending, and leave the ETH value out of the gas estimate, so a payable call falls back to a fixed gas limit.
- A failed position read returns a health factor of 999, the same value used for "no debt", so an RPC error looks like safety.
Evidence
- The interface has no tests. All 128 test functions are on the backend, and none exercises a transaction against a fork or a test network.
- The backtest has no recorded result, so I state no accuracy.
- I have not placed a live order while writing this.
What I would do first: connect the MPC path and delete the stored-key mode, make the guardian read the chain, enforce every declared limit in the executor, and run every transaction builder against a forked mainnet in the tests.
Open questions
Six months after the last commit the app would not start. A library two steps down the dependency chain had shipped a breaking release, and the sign-in library imported it at startup, so the container exited before the server came up. The repository pinned its direct dependencies with a minimum version only. I pinned the transitive one and the app started. The lesson is ordinary and I had skipped it: a deployed app needs a lock file.
The design answers with defaults: the manual mode is the default, the limits live outside the agent, and every proposal carries its reasoning. Automatic mode removes the person from each trade, which is exactly where the limits matter most and where, today, most of them are not enforced. Whether a 20% stop-loss and a $500 daily limit are the right numbers is a question the backtest was meant to answer and has not.
No. The agents are Python classes run by a scheduler every five minutes, each returning the same result type. They do not talk to each other over MCP, A2A or any other agent protocol; chaining on the canvas passes one agent's output into the next agent's input.