
Sweem
A yield router on Base that moves your money to the best venue without ever taking custody.
Demo
About
The same dollar of USDC on Base earns a completely different rate depending on which contract happens to hold it, and the spread is neither small nor stable. A snapshot from our own mainnet subgraph, live as of writing, shows three Aave V3 markets on one chain paying 1.78 percent, 0.0002 percent and 0 percent. Nobody watches that. The two things you can do about it today are both bad: hand your money to a vault that takes custody and issues you a share token, or read a dashboard that tells you to go move the funds yourself. sweem is the third option. The money stays in your own wallet, and something else does the watching. A basket in sweem is a published weight configuration, not a pool. There is no vault, no share token, no NAV, and no redemption queue. When you subscribe to a basket, your own Privy embedded wallet starts following those weights. Each slice of your deposit is routed to the highest yielding venue for that asset, and it is moved again when the rates move. What you own is whatever your wallet holds, which is why our portfolio endpoint reconciles our database against on chain balances rather than treating the database as the truth. Two consequences follow from refusing custody, and they shape the entire system. First, the component that signs can only move funds, never hold them. Every destination in every transaction it builds is either a venue on a committed allowlist or the user's own address. The executor process has no wallet of its own. A withdrawal pays out to the owner and nowhere else. An approval targets the asset contract while a deposit targets the venue, so the two can never be confused. Second, there is no accounting to get wrong, which means the only way to be wrong is to be wrong about a number we read. So the system refuses to guess. A missing price feed, a stale Chainlink round or an unreachable node produces a null value and a stated reason, and the leg is skipped rather than routed. A supply APY of zero means "no trustworthy rate yet", never "this pays nothing, route here". A fabricated one dollar price misroutes money exactly as badly as a fabricated APY, and it does it silently. Roughly fifteen tests in this repo exist solely to enforce that distinction between unknown and zero. The user experience is four steps. Log in with an email, which creates a Privy embedded wallet. Grant a delegated signer with one click. Pick assets, either concrete instruments like USDC and wstETH or a family like ETH that sweem resolves to the best routable instrument at deposit time. Deposit USDC. From there the router does the work, and every allocation is shown before anything is signed. Revocation is one button and it is the feature we are proudest of. Removing the signer strips the app's ability to act, and the backend immediately reports the wallet as undelegated and returns a 412 naming the missing precondition on any attempt to execute. Granting delegation is a promise; being able to take it back in one click without asking us is the proof. What is actually proven on chain today: a full round trip on Base Sepolia, a five dollar USDC deposit routed into Aave V3 and then withdrawn back out, both transactions sent from the user's own embedded wallet at 0x0e5cC5C5c14e89a4DaFf87ea314C25EF37c8C08f and not from any wallet this service controls. A product you can enter and cannot leave is not a product, so closing that loop came before anything else. We are equally clear about what is not proven. The keeper that decides when to rebalance is built and unit tested, and it has never executed against a live position. Base mainnet is read only so far: 28 venues allowlisted, rates indexed, prices read, swap quotes taken against real pools, and zero transactions sent. That list lives in the README under a heading called "What is not done", and nothing in it is hidden elsewhere in the repo.
How it's made
The Sweem client talks to a Go wallet service, which is the only client of a Rust executor, which is the only thing that signs. A Go market data service answers "where is the best yield for asset X" from Redis. A Go keeper watches for drift and decides when to move, and it never signs anything, so it can be crashed, restarted or duplicated without putting funds at risk. Its decision is a trigger, not an authorization: the wallet service rechecks delegation, drift and every guard before it calls the executor. Postgres holds users, baskets, weights, subscriptions, positions and executions. Everything is fail closed at boot. The wallet service refuses to start if it cannot fetch its Privy verification key. The executor refuses to start on a missing, unparseable or empty allowlist. The keeper forces dry run without a shared secret. On The Graph, we wrote and deployed one subgraph that indexes Aave V3, Compound III and MetaMorpho from a single manifest on both Base mainnet and Base Sepolia, and normalizes all three into one Venue entity. The leverage claim is concrete: three protocols publish yield in three incompatible units and none of them is an APY. Aave publishes liquidityRate, a ray scaled 1e27 annual simple rate. Compound III publishes getSupplyRate(getUtilization()), a per second factor scaled 1e18 and not a per block one, which a careless reading would make roughly two times wrong on Base's two second blocks. MetaMorpho publishes nothing at all, so the rate has to be derived from ERC 4626 share price growth between two snapshots. All three land on the same compounding step, written as expm1(n times log1p(r)) rather than (1+r)^n, because at a per second r of about 1e-9 the addition in 1 + r discards most of r's significant digits in float64. The share price is scaled 1e36 rather than 1e18 because MetaMorpho shares carry a DECIMALS_OFFSET of 12 on USDC vaults, and 1e18 leaves about six significant digits and rounds an hour of yield away. mwETH has an offset of 0, which is how we learned the scale is not optional. We deliberately did this inside the subgraph mappings rather than in Go. In Go, comparability would be a property of our service, useful to us and to nobody else. In the mapping, the normalized schema is the artifact: anyone can query venues and get Aave, Compound and Morpho side by side, already compounded identically, without running a line of our code. A script pins the subgraph's rate math against the Go implementation so a divergence is a failing check rather than a wrong route. Two more Graph products are load bearing. The Token API answers what the wallet actually holds on Base, and the portfolio endpoint reconciles that against our own position rows; a position that disagrees reports reconciled false with a reason rather than being quietly trusted, and a position we cannot price reports a null value rather than a number. The decentralized network gateway serves third party subgraphs for protocols we do not index ourselves, such as Moonwell, and acts as a mainnet fallback while ours syncs. Deployed and live, not mocked: * Base mainnet: https://api.studio.thegraph.com/query/1759980/sweem-base/v0.0.5 * Base Sepolia: https://api.studio.thegraph.com/query/1759980/sweem-base-sepolia/v0.0.1 One deliberate and slightly hacky decision worth naming: mainnet data sources start at a recent block rather than at their deployment blocks, because we never query history. Aave's and Comet's rates are spot reads. Only two paths derive a rate from a series, and the start block is sized to about two and a half times the wider of them. Indexing from deployment meant scanning 49 million blocks to serve numbers that are identical either way. Making that safe required removing two discovery dependencies first, so we seed every Aave reserve from getReservesList in a once block handler and adopt the nine large pre existing MetaMorpho vaults by verified address rather than waiting for a factory event that fired ten million blocks ago. A fast sync that indexes nothing is worse than a slow one. Privy is the wallet layer, the auth layer and the signing layer, and there is no other key material in this system. The embedded wallet is created on login, every transaction is sent from it, and access tokens are verified as ES256 with the issuer and audience pinned. Privy has no JWKS endpoint, so the verification key is fetched once at boot over HTTP Basic and the service refuses to start without it. That key arrives as a single line SPKI PEM that Go's pem package will not parse, so we rewrap it at 64 characters. Delegation is the product. The user calls addSigners with a key quorum that wraps the executor's authorization public key, plus a policy id. From then on the executor can sign for that wallet, and only by producing a valid ECDSA P-256 signature over an RFC 8785 JCS canonicalization of the exact request. We implemented that from scratch in Rust, because the executor is not a Node process and no Rust SDK exists. Two details cost real time and are now pinned in tests: an empty object body canonicalizes to the empty string rather than to an empty object literal, and the signature is DER encoded standard base64 rather than raw r concatenated with s in base64url. We checked the implementation byte identical against the official Node SDK's own signature generator and committed those golden vectors, so a serialization regression fails cargo test instead of failing in production. The Privy policy is generated, not hand written. A Go command reads the same two allowlist files the executor reads and emits a policy that pins the callable addresses, the methods allowed per venue kind, the chain ids, and the rule that actually matters, which constrains approve.spender to allowlisted targets. An unconstrained approve is how funds genuinely leave a wallet. Native value transfers are denied outright. We measured enforcement rather than assuming it, using nine probes against a throwaway wallet with eth_signTransaction, which signs without broadcasting so nothing was spent: allowed calls signed, while an approve to an unlisted spender, a transfer to an unlisted address, a wrong chain id, a non zero value and personal_sign all came back as policy_violation. On Uniswap, a deposit into a non USDC asset becomes approve router, swap, approve venue, supply, with the user's own wallet as the swap recipient so the output never lands in a contract we control. We use SwapRouter02 and QuoterV2 on Base through alloy's sol macro. The contribution is two rules. First, a swap is never submitted without a live quote. amountOutMinimum is always the quoted amount times (10000 minus slippage bps) divided by 10000, never zero, and the caller's requested slippage is clamped server side to between 10 and 300 bps with a 50 bps default, so a caller cannot widen its own loss bound. An unreachable quoter fails the leg rather than submitting an unbounded trade. Second, the direct pools are a trap and nothing warns you. We measured price impact at two sizes before trusting any path. USDC to wstETH direct at the 500 tier fills at minus 3.23 percent on 100 dollars and minus 78.4 percent on 1000 dollars. USDC to cbETH direct at 3000 fills at minus 1.79 and minus 15.5 percent. Routed through WETH, the same trades cost minus 0.0003 and minus 0.0033 percent. Nothing in the contract interface distinguishes a deep pool from a nearly dead one at the same fee tier. The obvious, well typed, fully tested single hop function loses most of the user's money. So we shipped a static measured path allowlist rather than integrating a routing API: 40 paths covering 20 pairs, every one chosen by measurement, with no external dependency and no way to use a path we did not measure. Tokens that failed the bound on the exit leg, such as tBTC at minus 91 percent, stay unreachable rather than reachable at a loss, and a test asserts that every entry path has a matching exit with the tokens and fees exactly reversed. A hold venue you can enter and cannot leave is worse than no venue. That finding and three others, including the exactInput calldata offset, the QuoterV2 non view warning reading worse than it is, and sqrtPriceLimitX96 silently partial filling, are written up in FEEDBACK.md, written from the integration rather than after it. The executor is the security boundary. It is written in Rust, roughly 3,200 lines, with 77 tests that need no network to run. The single most important design decision in the codebase is that the destination address never comes from the request. The request names a venue id; the address is resolved from a committed local file. That makes the set of contracts a delegated signer can call independent of the request path, so a compromised caller, or a compromised indexer, cannot make the executor call an arbitrary contract. It is also why the allowlist is generated at build time and committed rather than fetched at runtime. That allowlist is generated by a Go command from the same indexed data the market data service serves, plus on chain verification. A venue is emitted only if it is encodable, meaning it maps to a venue kind the executor implements; priceable, meaning a verified Chainlink feed exists for the underlying on that chain; liquid, meaning it clears a TVL floor and a measured withdrawable liquidity floor; verified on chain via symbol, decimals and a protocol identity check such as getReservesList, baseToken, asset or isMToken; and exitable, meaning an allowlisted swap path back to USDC exists. Everything else is skipped with a printed reason. The result is 32 venues, 28 on Base mainnet and 4 on Base Sepolia, across five venue kinds. We deploy no contracts of our own; every address the system touches already existed. In the market data service, two sources feed one reconciler, split by what each protocol can actually publish. Spot rates come from direct eth_call, because getReserveData and getSupplyRate are the rate right now and need no indexing at all. Derived rates come from the subgraph, because a MetaMorpho vault's yield exists only as share price drift between two indexed snapshots. Each APY component is taken from whichever source can measure it: the base rate from RPC, rewards from the subgraph since COMP emissions need a price feed that does not exist on Base, and intrinsic staking yield from whichever has it. Where the two sources disagree on the base rate by more than 0.25 percentage points, the gap is logged with both numbers, which is how a repeat of the per block versus per second mistake would surface in one cycle rather than after a rebalance. The reason this matters is that a reverting call, a rate limited node, a frozen Aave reserve or a Comet whose symbol does not match our table must never become a rate of zero. Every one of them omits the venue with a logged reason. Zero would read as "this market pays nothing" and route money in exactly the wrong direction. One subtlety we got wrong first and then fixed: liquid staking tokens earn without any protocol interaction, because the exchange rate against the underlying simply rises. Aave pays about 0.1 percent on wstETH because nobody borrows it, so a real 3.1 percent position reads as 0.1 percent, and a naive keeper would helpfully improve the user out of it. We now read those intrinsic rates from the Chainlink exchange rate feeds that publish them on Base, since a feed keeps its past rounds on chain and the drift is therefore readable without indexing anything, and stack the intrinsic component onto every lending venue in that asset. Each provider is identified by calling description before its answer is used, because a market price also drifts and annualizing a market dip would invent yield outright, which is exactly why ezETH is excluded on Base. The keeper decides when to move. It is a pure decision function, fully unit tested, that runs guards cheapest first and then prices the actual economics. Already in the best venue, position below a floor, rate limited, inside a hysteresis window, drift below an absolute floor, and finally breakeven: the extra yield over an assumed holding period must beat the gas cost times a safety margin. Venues are ranked on reward discounted APY rather than the headline rate, because emissions are less durable than borrower paid interest. Nothing in the cost calculation is a configured guess. The gas price comes from eth_feeHistory over five blocks, taking the next base fee plus the median priority tip, because one outlier block should not reprice every user's decision. The ETH price comes from the Chainlink aggregator on Base, read on chain with no API key and no rate limit, with the decimals read rather than assumed and a stale round rejected outright. Gas units are measured from real Base transactions and pinned at the top of the observed range, since over pricing makes the keeper too cautious while under pricing makes it churn. If any input is missing or stale the pass is skipped and counted, never downgraded to a default, because a rebalance decided on fabricated cost data is worse than no rebalance. The keeper also owns the one thing nothing else resolves. When the executor's receipt poll times out, the transaction is probably in the mempool but we do not yet know whether the money moved, so the wallet service records the row as pending and deliberately withholds the position. The keeper sweeps those rows, fetches the receipt, and writes the position the wallet service withheld. It claims each row with a conditional update so only one instance can win, and writes the position with an upsert keyed exactly as the wallet service keys it, so running two keepers cannot double count. The frontend is Next.js 16 with React 19 and the React compiler, Tailwind v4, hand rolled primitives rather than a component library, and Framer Motion with every animation guarded by a reduced motion check. The allocation view is a React Flow diagram showing wallet to asset to venue with live APYs. Settlement is the interesting part of the UI, because partial failure is a first class outcome rather than an error: a deposit of a three asset basket is three independent legs, so the API returns 207 Multi Status with a per leg result, and the toast reports "two of three legs settled" with the step by step trace and a Basescan link per leg. A pending leg says so explicitly and warns that retrying can send the same money twice. The amount field only clears on a clean settle. Numbers are rendered with tabular figures and four decimal places below one dollar, and anything unpriceable renders the words "value unknown" rather than a zero. None of this is demoable if it is wrong, so it is tested accordingly. The 77 Rust tests cover canonicalization against the vendor SDK's golden vectors, every ABI selector, the allowlist load invariants, and the property that every allowlisted non USDC venue must have a swap path back to USDC or it must not be allowlisted. The Go suites cover the APY conversions against hand computed values, the largest remainder allocator proving no cent leaks across awkward weight and amount combinations, the reconciler never fabricating a value under six distinct failure modes, the keeper's breakeven and hysteresis boundaries including the case where a clock skewed future timestamp must stay locked, and the sweeper's idempotency under a double run. A script pins the subgraph's rate math against the Go implementation, and a browser free assertion script pins the client's formatters and its split arithmetic. None of them need a network. What we would call the actual engineering here is not the feature list. It is that the system consistently refuses to convert "we could not measure this" into a number, and that the refusal is enforced by tests rather than by discipline. Every place a default would have been easier, there is a null and a reason instead.