My GitHub profile is a stock now. It's called $VJK, it trades in rupees during Indian market hours, and anyone with a GitHub account can buy or sell it. My commits decide what it's worth.
Where this came from
Over the last few weeks I kept running into posts here on DEV where people had turned their GitHub profile into something completely different. Giorgi built a cyberpunk console with a city made out of his contributions. Someone turned theirs into a roguelike dungeon, someone else into a spaceship mission console, and there's a Minecraft tree that grows from your commits.
I loved all of them. My own profile README at that point was empty. I had cleaned it up and never put anything back.
I wanted to make one too, but I didn't want to build another city or another game. What I liked about those posts is that each profile says something about the person who made it. So I asked myself what mine would say. The honest answer is that I spend a lot of my free time reading about markets and finance.
So I listed myself on the stock market.
What's on the profile
From top to bottom:
- A ticker that scrolls the price, the fair value and the latest market news.
- A quote panel with the price, the day's open, high, low and volume, and how far the price can still move today.
- A daily candlestick chart. The dashed orange line is the "fair value" my commits give the stock.
- Four buttons: buy 5, buy 25, sell 5 and sell 25.
- The top shareholders and the latest trades.
- My GitHub numbers, shown like a company's fundamentals. My languages show up as "sector allocation".
It follows your GitHub theme, so light mode users get a light version.
How a trade works
There's no website and no server. Trading happens through GitHub issues.
When you click BUY 5, GitHub opens a new issue in my profile repo with the title BUY 5 $VJK already filled in. You can change the number to anything from 1 to 25. Click Create, and that's your order.
Opening the issue starts a GitHub Actions workflow. It reads the title, checks the order and fills it. If it's your first trade, you get ₹10,000 of play money to start with.
Every share you buy pushes the price up 0.15%, and you pay the price after your own push. $VJK listed at ₹55.10, so buying 5 shares at that price goes like this:
- the price moves from ₹55.10 to ₹55.50
- you pay ₹55.50 × 5 = ₹277.50
- you're left with ₹9,722.50 in cash and 5 shares
The bot then redraws the images on my profile, commits them, leaves a receipt on your issue and closes it. This is the receipt for that exact trade:
✅ Filled · BUY 5 $VJK @ ₹55.50
| Fill price (after your own impact) | ₹55.50 |
| Order value | ₹277.50 |
| Your trade moved the price | ▲ +0.73% |
| Your position | 5 shares, avg ₹55.50 |
| Cash | ₹9,722.50 |
| Portfolio value | ₹10,000.00 (+0.00%) |
Selling is the same thing in reverse. Each share you sell pushes the price down 0.15%, and you get the price after your push. If you sold those 5 shares ten minutes later and nobody else had traded, the price would drop back to ₹55.10 and you'd get ₹275.50. You'd be down ₹2.
That's on purpose. If you could buy and sell straight away without losing anything, someone would write a script to pump the price and dump it.
A few other rules:
- One order every 10 minutes per person.
- If you try to sell more than you have, it sells what you have. If you have nothing, the order is rejected.
- If you don't have enough cash, you get as many shares as you can afford.
- Bots can't trade, and neither can I.
Market hours and limits
The market keeps Indian market hours: 09:15 to 15:30 IST, Monday to Friday. If you place an order outside those hours, it waits. The bot comments that it's queued, leaves the issue open, and fills it at the next morning's open. Indian brokers call this an after-market order (AMO). Closing the issue cancels it.
The price can't move more than 10% from the previous day's close. When it hits the upper limit, buying stops for the rest of the day, and when it hits the lower limit, selling stops. Indian exchanges call this a circuit. Here it also stops one person with ₹10,000 from doubling the price in an afternoon.
Prices move in steps of ₹0.05, and big amounts use Indian digit grouping, so you'll see ₹1,00,000.00 and not ₹100,000.00.
How my commits move the price
This is my favourite part. Every weekday at 15:33 IST, right after the close, the workflow looks at my contributions from the last 30 days and works out a fair value:
export function fairValue(contrib30, cfg) {
const value = cfg.listingPrice * Math.sqrt((contrib30 + 10) / (cfg.baseline30 + 10));
return snap(Math.round(value * 100), cfg.tickPaise);
}
A normal month for me is about 30 contributions, and that's worth ₹100 a share. A month with nothing drops it to ₹50. The square root is there so one crazy day doesn't make the number explode, and the + 10 keeps a quiet month from taking it to zero.
Then the closing price moves 20% of the way toward that fair value, still inside the 10% limit. So during the day you decide the price, and at the close my commits pull it back toward what they say it's worth.
Commits I make at night or on weekends still count. They stay in the 30-day window, so the next weekday close picks them up, and they keep counting for 30 days.
A chart from day one
A brand new stock would start with an empty chart. So when $VJK listed, it backtested the previous 70 trading days: start at ₹100 and run every close as if the market had already existed, using my real contribution history.
The chart is pretty honest about my year. I was busy in July, and the price climbed to ₹174. I didn't push anything public in August or September, and it slid all the way down to ₹50. Then I made 30 contributions on October 4th, and the last session before listing closed right at its +10% limit.
So $VJK listed at ₹55.10, with a fair value of ₹101.25. If I keep committing, it should keep climbing for a while.
Dividends
Every time one of my pull requests gets merged, everyone holding shares gets ₹2 per share. It's a small reason to hold instead of flipping.
What it looks like with traders
This one is from a local rehearsal. I ran two and a half weeks of made-up trading through the same code, with fake usernames, to see how the leaderboard and the chart look once people are actually trading. The real market starts empty.
How it's built
Everything on the profile is an SVG
GitHub doesn't run JavaScript in a README, and it strips <style> tags. But an SVG loaded through <img> can carry its own CSS, including animations. So every panel is an SVG. The ticker scrolls, the market status dot pulses, and the buy and sell buttons have a shine that sweeps across them. A link can only wrap a whole image, which is why each button is a separate file.
SVGs shown this way can't load anything external either, not even Google Fonts. So I embedded JetBrains Mono, cut down to just the characters I use. That comes to about 5 KB per weight. JetBrains Mono has no ₹ sign, so the rupee comes from a 628-byte cut of Noto Sans Mono, which has the same character width. Avatars get downloaded and embedded too.
For dark and light mode, the README uses <picture>:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/dark/quote.svg?v=f3a9c1d2e0">
<img src="assets/light/quote.svg?v=8b2e4a7c91" width="100%" alt="...">
</picture>
The ?v= part is a hash of the file. GitHub caches images, so when a panel changes, the README points at a new URL.
One workflow for everything
The workflow runs at the opening bell, at the closing bell, and every time someone opens an issue with VJK in the title:
on:
schedule:
- cron: "47 3 * * 1-5" # 09:17 IST
- cron: "3 10 * * 1-5" # 15:33 IST
issues:
types: [opened]
workflow_dispatch:
Every run does the same job: fetch my GitHub stats, ring whichever bell is due, settle every open order, redraw the SVGs, regenerate the README, then commit and push. The receipts only go out after the push works, so nobody gets a receipt for a trade that isn't in the ledger.
The ledger is just data/market.json in the repo, and its git history is the full record of every trade.
Things I had to work around
GitHub cancels queued runs. With a concurrency group, GitHub keeps one run going and one waiting, and cancels the rest. If ten people order at once, most of those runs never happen. So no run belongs to a single issue. Each run settles every open order, and whichever run survives does the work of the cancelled ones.
Queued runs start from an old commit. A waiting run checks out the commit from when it was triggered, which can be behind what the previous run just pushed. Checking out the branch name instead fixes that, and if a push still gets rejected, the job resets to the new tip and runs the tick again.
Bursts and rate limits. Every order costs two API calls, a comment and a close, and GitHub limits how fast a bot can create content. So each run settles at most 20 orders. If more are waiting, it triggers itself again with workflow_dispatch, one of the few events a workflow's own token is allowed to start.
Issue titles are user input. Anyone can open an issue with any title. Here the title only ever gets matched against a regex in Node. It never goes into a shell command and never gets written into the README. rudratosh wrote a good post on what profile automation workflows can expose, and it's worth reading before you build one of these.
No empty commits. Rendering is deterministic. If nothing changed, the files come out byte for byte the same and the workflow skips the commit.
The bot's commits aren't mine. They're authored by github-actions[bot], so trading can't inflate my contribution graph, and through it, my own stock price.
No dependencies
It's plain Node with no npm packages. The market itself is a pure function of the current state, my stats and the time, which made it easy to test. 29 tests cover the rules, the rendering and a full run of the workflow script against fixtures.
Try it
Go buy some $VJK on my profile. The market's open 09:15 to 15:30 IST on weekdays, and orders placed outside those hours wait for the next morning.
If you want your own stock, the code is in the same repo. scripts/README.md has the steps: change the symbol and the username, and set how many contributions a normal month is for you.
I'm curious who ends up as the biggest shareholder.





Top comments (0)