Relay
A memory for public trading ideas, built to say what it cannot prove.
- Kind
- Team project. Deployed on devnet.
- Timeline
- October 2026
- Team
- Three people, with Mariam Manjavidze and Luka Vacheishvili.
- Built with
- TanStack Start, React, Hono, Bun, PostgreSQL, Rust, Anchor, Solana devnet, Docker, Caddy
01Context
A trader posts a screenshot: up forty percent. You buy, and twenty minutes later you are down. The screenshot was true. It was about a trade made earlier, at a different price, and nobody shows you that part.
Relay started as a feed of trade plans on Solana, each with an entry range and an expiry. Partway through we restructured it. The stronger product was the memory: take what a trader said in public, keep the source and the date, and attach what happened next.
It was a three-person project for a Solana course in October 2026. Everything on chain runs on devnet with test tokens.
02My role
Most of the code: the server, the discovery data and its rules, deployment, the devnet integration and most screens.
03A product whose main feature is saying "we don't know"
It is easy to make a trading site look authoritative. The hard part was the opposite. Every screen had to be exact about what kind of thing it was showing. A public post proves that someone said something. It does not prove that they traded, that they own a wallet, or that they made money.
04Decisions
How do you keep quotes and trades from blurring together?
- Options
- One feed, with a badge on each card
- Two domains that never share code
- Chose
- Discovery, the public-source records, and plans, the on-chain ones, are separate features with separate data. A test reads the source tree and fails if a discovery screen imports a wallet, trade or fixture module.
- Because
- A badge can be left off one card. An import that fails the build cannot.
- It costs
- Some duplication between the two halves, kept on purpose.
What does the app show when it has no data?
- Options
- Zero, or an empty list
- The reason
- Chose
- Missing stays missing. If the chain cannot be read, the plans page says so and shows when it was last read successfully, instead of "No open plans". Unavailable is never drawn as zero.
- Because
- An empty list and a failed read look identical unless the server tells them apart, so the plan service reports the state of its last read with every response.
- It costs
- More states to design and test. Loading, empty, unavailable and error are four different screens.
How is a later post tied to an earlier idea?
- Options
- Same asset, so they are related
- A fixed vocabulary with a stated basis
- Chose
- Exactly one of four: an update the author confirmed, later analysis on the same topic, uncertain, or unrelated. The timeline says which.
- Because
- Two posts that mention Bitcoin are not a story. If the author did not link them, Relay says the link is not established.
- It costs
- Coverage is recorded by hand, and the site says so. Three profiles and a handful of ideas.
Who is allowed to be right about money?
- Options
- The server, with the chain as a backup
- The chain, with the server as a mirror
- Chose
- The chain. The server mirrors it and is never an authority. Amounts are integers everywhere, a failed transaction stays failed, and nothing sent by the client is trusted for an amount or an outcome.
- Because
- A server that can correct a balance is a server that can be wrong about one.
- It costs
- Every read depends on a network endpoint that can be slow or down, which is why the second decision exists.
Going deeper
A guard added after a mistake
During the restructure, a data load reached a remote database by way of the root environment file. Since then, the commands that write data print the database host first and refuse a remote one unless it is named on the command line.
The repository's standing rules
Devnet only. No type assertions in TypeScript, enforced by a lint rule. Money is integers. No invented traders, posts or identities. Fictional fixtures live on a development-only route that returns 404 in a production build.
Deployment
Two Bun processes and Postgres in Docker Compose, behind Caddy, on one small server. A deploy script runs from a systemd timer every five minutes and picks up new commits.
05The result
What works
- Live at trade-relay.app: three public-source profiles, recorded by hand and labelled as not claimed by their authors, with idea timelines, a watchlist kept in the browser and an updates view.
- Plans published and followed on Solana devnet through a simulated venue.
- More than 500 tests across the domain logic, server, app and on-chain program, plus a browser suite with accessibility checks. The count is from the project's October roadmap.
What is not there
- Devnet only, by decision. No real money moves.
- The profiles are not claimed by the traders, and every profile says so.
- Coverage is manual. The only automatic input is titles, links and dates from each author's own public feed.
06Reflection
The restructure taught me more than the build did. We had a working feed of trade plans and a pitch for it, and the honest reading was that the timeline of what people had said was the better product. Taking something you already built off the front page is harder than building it.
What I would carry from this project anywhere is tests that enforce the architecture, not only the behaviour.