๐ Introducing SteemTwist โ Steem with a Twist
Hello Steem Dev community! ๐
I'm excited to share a project I've been working on: SteemTwist, a decentralised microblogging dApp built entirely on the Steem blockchain. No backend, no build tools, no server โ just four static files and the chain.
๐ Live app: https://puncakbukit.github.io/steemtwist
๐ฆ Source code: https://github.com/puncakbukit/steemtwist
๐ White Paper
https://github.com/puncakbukit/steemtwist/blob/main/SteemTwist_WhitePaper.pdf
What is SteemTwist?
SteemTwist is a Twitter/X-style microblogging experience powered by Steem. Every post, reply, vote, follow, and encrypted message is a native Steem blockchain operation. Content is permanent, censorship-resistant, and owned entirely by the author's Steem key โ not by any company or service.
A SteemTwist user is called a Twister ๐. Short posts are Twists, replies are Thread Replies, upvotes are Twist Love โค๏ธ, resteems are Retwists ๐, and notifications are Signals ๐.
Tech Stack
The entire app runs in the browser from four files:
| File | Role |
|---|---|
index.html | HTML shell with CDN scripts (SRI-verified) and CSS tokens |
blockchain.js | All Steem API and Keychain helpers โ no Vue dependency |
components.js | Vue 3 components compiled at runtime from CDN |
app.js | Vue Router views, routes, and global state |
Dependencies (all CDN, all SRI-verified):
- steem-js โ Steem RPC client
- Steem Keychain โ signing and memo encryption
- Vue 3 + Vue Router 4 โ UI framework
- marked.js โ Markdown rendering
- DOMPurify โ HTML sanitisation
Hosting is on GitHub Pages. Because Vue Router uses createWebHashHistory, all routes work with zero server configuration.
How Data is Stored
SteemTwist uses a thin convention on top of standard Steem comment operations. At the start of each month, the @steemtwist account publishes two root posts:
feed-YYYY-MM โ all Twists are direct replies to this
secret-YYYY-MM โ all encrypted Secret Twists are posted here
Twists have permlinks in the format tw-YYYYMMDD-HHMMSS-username. The full tree looks like this:
@steemtwist/feed-2026-03
โโโ @alice/tw-20260315-091530-alice โ Twist
โโโ @bob/tw-20260315-102244-bob โ Live Twist โก
โ โโโ @alice/tw-20260315-150012-alice โ Thread Reply
โ โโโ @carol/tw-20260315-160000-carol โ Flag reply
โโโ ...
All Twists are broadcast with max_accepted_payout: "0.000 SBD" and allow_curation_rewards: false. Twist Love works as social appreciation without moving money.
Features
๐ Feed & Navigation
- Home โ personalised stream from Twisters you follow, with Firehose ๐ฅ real-time streaming (filtered to followed users only)
- Explore โ global Twist Stream with New / Hot / Top sort modes and a live Firehose
- Understream ๐ โ a toggle on every page that widens the view from SteemTwist-only content to the full Steem blockchain (all posts, full account history)
- Profile โ paginated Twist history with a pinned Twist slot, Understream support, and a refresh button
๐ Twists
- Up to 280 characters with full Markdown and a real-time Write / Preview tab
- Edit โ re-broadcasts the comment op; the card updates instantly in the feed
- Delete โ uses
delete_commentif the post has no votes or replies; falls back to blanking the body otherwise - Pin โ pin one Twist to the top of your profile via an on-chain
custom_jsonoperation - Retwist โ resteem any Twist
- Twist Love โ upvote at 100% weight
๐ค Social
- Rich profile cards with avatar, reputation (1โ100 scale), bio, location, website (https-only validation), join date, and stats
- Paginated Followers / Following / Friends (mutual follows) pages with Follow/Unfollow buttons per row
- Follow and Unfollow using Steem's native
followplugin
๐ Signals (Notifications)
Scans the latest 500 account-history entries and classifies each into one of six types:
| Signal | Trigger |
|---|---|
| Twist Love โค๏ธ | Incoming upvote on your Twist |
| Reply ๐ฌ | New Thread Reply on your Twist |
| Mention ๐ข | Your @username appears in a Twist |
| Follow ๐ค | Someone followed you |
| Retwist ๐ | Someone resteemed your Twist |
| Secret Twist ๐ | An incoming encrypted message |
Unread signals show a badge count in the nav. The badge clears when you visit the Signals page.
โก Live Twists โ The Unique Feature
This is the part I'm most excited about. A Live Twist is a Steem comment whose json_metadata carries a JavaScript code payload alongside type: "live_twist". When a viewer opens the Twist, they see a โถ Run button. Clicking it executes the code inside a sandboxed iframe and renders the output inline in the feed.
Live Twists turn static posts into interactive widgets โ polls, calculators, charts, blockchain dashboards, animated greeting cards, games โ all stored permanently on-chain.
On-Chain Format
{
"type": "live_twist",
"version": 1,
"title": "Click Counter",
"code": "let n=0; function draw(){ app.render('<button id=b>Clicks: '+n+'</button>'); document.getElementById('b').onclick=()=>{n++;draw();}; } draw();"
}
The body of the Steem comment defaults to "โก Live Twist โ view on SteemTwist" so it renders gracefully on Steemit and other clients.
Sandbox API
Author code communicates with the host page through an app object:
| Method | Description |
|---|---|
app.render(html) | DOMPurify-sanitise and inject HTML; auto-resizes the iframe |
app.text(str) | Set body as plain text (max 2,000 characters) |
app.query(type, params) | Call a read-only Steem API method |
app.ask(type, params) | Like app.query() but returns a Promise โ safe for concurrent calls |
app.action(type, params) | Request a Keychain-signed blockchain operation with a confirmation modal |
app.log(...args) | Append to the built-in console panel |
app.ask() uses per-request IDs so concurrent queries never collide:
const [ticker, props] = await Promise.all([
app.ask("getTicker", {}),
app.ask("getDynamicGlobalProperties", {})
]);
app.render("<b>STEEM:</b> $" + parseFloat(ticker.latest).toFixed(4) +
"<br><b>Block:</b> #" + props.head_block_number);
Over 70 read-only Steem API methods are available via app.query() / app.ask(), and 10 Keychain-signed action types are available via app.action() (vote, reply, follow, transfer, delegate, power up/down, vote witness, retwist).
40-Template Gallery
The composer ships with 40 ready-to-use templates in four tabs:
| Tab | Templates |
|---|---|
| Simple | Poll, Quiz, Clicker, Calculator, Chart, Expandable, Story, Demo, Explorer, Prototype |
| Greetings | Birthday, New Year, Congratulations, Wedding, Graduation, Eid, Christmas, Thank You, Get Well, Anniversary |
| Queries | Account Info, Trending Tags, STEEM Price, Hot Posts, Follower Count, Top Witnesses, Chain Stats, Post Viewer, Order Book, Reward Pool |
| Actions | Vote on a Post, Reply, Follow/Unfollow, Transfer STEEM/SBD, Delegate SP, Power Up, Vote for Witness, Retwist, Query then Vote, Query then Follow |
Security
Live Twists are isolated by ten layered controls:
sandbox="allow-scripts"only โ null origin, no same-origin access, no top navigationfetch,XMLHttpRequest,WebSocket, andwindow.openall throw inside the sandbox- DOMPurify sanitises every
app.render()call using a shared config identical in both viewer and composer preview - 10 KB code size limit enforced before publish and before โถ Run
- User must click โถ Run โ never auto-executed on page load
- Keychain is unreachable from the sandbox โ all
app.action()calls route through the parent page - Each
app.action()shows a confirmation modal with HTML-escaped parameters before touching Keychain - Parent validates
e.origin === "null"ande.source === iframe.contentWindowon every inboundpostMessage - All
app.query()parameters are sanitised: strings capped to 256 chars, limits clamped to 1โ100 - All
app.action()parameters are validated: username regex, decimal-only amounts, currency/unit allowlists
Live Twist Flag System ๐ฉ
Users can flag a Live Twist authored by someone else. Flagging is a two-step Keychain sequence:
- A
โ10000weight downvote viarequestVote - A reply comment via
requestBroadcastwith the reason stored injson_metadata
The two-step design is required because Keychain rejects vote ops bundled in requestBroadcast. The flag reason is one of 12 JS-specific categories:
| Reason | Description |
|---|---|
| ๐ช Session Hijacking | Stealing session IDs via document.cookie |
| ๐ณ Web Skimming / Formjacking | Intercepting card numbers at form submission |
| ๐๏ธ Storage Theft | Reading tokens from localStorage / sessionStorage |
| ๐ DOM-type XSS | Executing malicious scripts via URL parameters |
| ๐ฃ Phishing Form Insertion | Injecting fake login forms into legitimate pages |
| ๐ช UI Redressing | Tricking users into clicking hidden/overlaid elements |
| โ๏ธ Cryptojacking | Background CPU mining while the page is open |
| ๐ Browser Fingerprinting | Tracking users without cookies via JS-collected data |
| ๐ Sensor / Location Abuse | Deceptive permission prompts for device sensors |
| ๐ ๏ธ Client-Side Logic Tampering | Bypassing JS-based admin checks via dev tools |
| โฉ๏ธ CSRF | Sending unintended requests to third-party sites |
| โ ๏ธ Other | Any other harmful behaviour not listed above |
๐ Secret Twists โ End-to-End Encrypted Messaging
Secret Twists provide private messaging between Steem accounts using the memo key pair. Encryption and decryption are delegated entirely to Steem Keychain โ plaintext never touches SteemTwist's code.
Sender โ requestEncodeMessage (Keychain) โ broadcast to @steemtwist/secret-YYYY-MM
Recipient sees ๐ signal โ requestVerifyKey (Keychain) โ message revealed
- Unlimited message length with full Markdown support
- Nested encrypted replies โ each decrypted individually on demand
- Only the recipient (not the original sender) can reply
- Invisible in the regular Twist feed โ only appears in the Secret Twists inbox
RPC Fallback Nodes
All blockchain calls use automatic node rotation on failure:
https://api.steemit.comhttps://api.justyy.comhttps://steemd.steemworld.orghttps://api.steem.fans
Forking / Deploying Your Own Instance
The TWIST_CONFIG object in blockchain.js centralises all deployment-specific constants:
const TWIST_CONFIG = {
ROOT_ACCOUNT: "steemtwist",
ROOT_PREFIX: "feed-",
SECRET_ROOT_PREFIX: "secret-",
TAG: "steemtwist",
POST_PREFIX: "tw",
DAPP_URL: "https://puncakbukit.github.io/steemtwist"
};
To run your own independent instance: update these values, push the four files to any static host, and publish your own monthly root posts. No server configuration is needed.
What's Next
This is version 0.1 โ a solid foundation, but there's plenty of room to grow. I'd love to hear feedback from the Steem Dev community on:
- The Live Twist sandbox API โ are there read-only query types you'd find useful that aren't currently supported?
- The data model โ any thoughts on the monthly root post approach vs. alternatives?
- Security โ any concerns or suggestions about the sandbox design?
Feel free to try it out, spin up your own fork, or open issues on GitHub. All contributions are welcome.
Thanks for reading! ๐
Built with steem-js ยท Steem Keychain ยท Vue 3 ยท MIT License
Assisted by:
See also:
- By eliminating downvotes, Blurt ensures users are rewarded based on positive consensus, not through whale punishment. Please join through this link or this link with the invite code "puncakbukit."
- Introducing SteemBiota โ Raise, Breed & Evolve Creatures on the Steem Blockchain!
- Reversteem โ Play Reversi on the Steem Blockchain!
- @steem.amal: Charity At Your Fingertips
- Maximize curation rewards: follow our trail! Maksimalkan reward kurasi: ikuti trail kami! ใใฌใคใซใใใฉใญใผใใใญใฆใฌใผใทใงใณๅ ฑ้ ฌใๆๅคงๅ๏ผ



Nice..thank you very much for your good efforts in making steem more amazing โค๏ธ
0.00 SBD,
0.03 STEEM,
0.03 SP
Thanks.. :-)
You're welcome
Upvoted! Thank you for supporting witness @jswit.
Looks Interesting , Also have you tried #blazedit