Every sentence below was written by reading the code that does the thing it describes, not from a template. Where this page and the code disagree, one of them is a bug and we want to hear which. There is no sign-up here, no password, and no real patient — so most of what a privacy policy usually has to cover does not exist. What is left is unusual enough to be worth reading properly, and the first section is the one that cannot be undone.
Vitals · vitals 0.10.4 · operated by MegaWiz Co., Ltd., Bangkok · paripol@megawiz.co
One button on this site writes to a public blockchain: anchor this run on chain, at the end of a finished case. It is off by default, it is never automatic, and you can play every case here for the rest of your life without ever pressing it. Nothing else on this site puts anything on a chain.
When you do press it, the record is copied to every machine that keeps that chain. We cannot delete it, edit it, hide it or correct it, and neither can you. There is no takedown, no support ticket and no key we hold that would help. If you anchor a run you later regret, the only thing anyone can do is anchor better runs beside it. The bad one stays.
If you then prove that run and claim a level, three more things exist on chain under your key, permanently: a claim record repeating the case, score, maximum, deterministic score, tier and exam flag for each proven attempt; a progress record holding your level, your distinct-case count, your attempt count and your XP; and a commitment account holding a count of every attempt you have ever declared. That count only ever goes up. Clearing a commitment does not reduce it. It is deliberate — it is what stops somebody committing five runs and anchoring only the good one — and it means the chain permanently knows how many times you started, whether or not you finished.
That last one is the whole risk, and it is yours to manage. The key is a pseudonym only for as long as you keep it one. The moment you tell anyone that key is yours — put it on a CV, hand it to a residency programme, post it — everything in the first list above is permanently and publicly attached to your name, including the runs where the patient died. That is exactly the feature: a record a stranger can check without trusting us. It is also exactly the cost. Decide which runs you want attached to you before you anchor them, because afterwards there is no mechanism.
Which chain, honestly. This bay anchors to Solana devnet today — a public test network. Devnet carries no money, and its operators reset it from time to time; that is their decision, not ours, and it is not a deletion mechanism you should rely on for anything. Treat what you anchor as permanent, because the intention is to move to mainnet, where it plainly is. The page you anchor from prints which cluster it is writing to, live, so you never have to take this paragraph's word for it.
Nothing here asks you to sign up. There is no email field, no password to reset and no profile.
When your browser first needs an identity it generates an Ed25519 keypair with the
browser's own WebCrypto and keeps the private half in that browser's localStorage. The
private key never leaves your browser: the server prepares a transaction, your browser signs it,
the server submits it. The server cannot produce that signature and holds no key of yours.
You never see a seed phrase because you never need to fund anything. A relay key we own pays every transaction fee — it pays, and it never plays. That is why a nineteen-year-old can anchor a run without buying SOL or installing a wallet.
The consequence, stated plainly: clear your browser's site data and that key is gone. With it goes your ability to prove an anchored run was yours. We cannot recover it, because we never had it. If you play on more than one machine, link the second device to the first from inside the app rather than starting again.
Every one of them, by the name you would see if you opened your browser's storage yourself:
vitals.key — the private half of your keypair, and vitals.account,
which account it belongs tovitals.lang — the language you chosevitals.cleared, vitals.starred, vitals.stars and
vitals.tiers — which episodes you have finished, which you starred, and how
wellvitals.seen and vitals.teasers — which cards and which teasers have
already played, so they do not play at you twicevitals.sets — not about you at all: the list of case sets this server sent us,
kept so the shelf still draws itself when the network does not answervitals-analytics-consent (§8) and vitals-review-draft (§7)All of it is local to that browser. Clearing site data removes all of it.
That list is meant to be exhaustive, and it was one key short until we checked it
against the code rather than against the last version of this paragraph. It is now checked by a
test that reads the pages and fails if any of them keeps something this list does not name — but if
you open your storage and find a vitals. key that is not above, the test has a hole in
it and we want to hear about it.
Every patient in Vitals is synthetic — an authored case, not a record of a real person. There is no PHI in this system to protect, leak or lose. That is a design constraint rather than a claim about our security, and it is what makes the rest of this page short.
It cuts the other way too, and §5 says so: because there is no real patient in the loop, the text you type is sent to a cloud model. Do not put anything about a real patient into it.
Opening a case creates one record on our side, keyed by a random session id. It holds:
| Field | What it is |
|---|---|
| ep | which case you are playing |
| owner | your player public key, when your browser made one. A kiosk or a browser that cannot sign has none, and then the random session id is the only thing protecting the run |
| sce_hash | the hash of the exact scenario file, so a rewritten case cannot silently resume your run under different rules |
| tape | everything you did: every clock tick, every order in the words you typed, every question you asked, every dial you turned, every device you removed, every shock and its joules |
| said | the conversation — your question and the patient's reply, kept so the patient remembers what they have already told you |
| anchored | whether this run has been written to the chain |
| commit | the commitment hash, the slot it landed at, and the nonce. The nonce stays here and never reaches the chain |
| exam_mode | whether you declared this an exam before playing |
The patient's replies are stored but never hashed and never anchored — a model's words are not evidence of anything, so they are kept out of the proof path entirely. Your questions are on the tape, and the tape's hash is what an anchor commits to.
The code sweeps away runs that nobody has touched for 24 hours — a closed tab is not a patient. Two limits on that, and we would rather write them down than let the number stand alone:
So on the live site, a run you started is not deleted by age today. That is a gap, we know about it, and stating it is cheaper than implying a deletion that is not happening. If you want a run of yours deleted, mail us and it will be — this is on our server, and we can.
Everything that is not a run is marked durable and is never expired by age, on purpose: the month's meter counts, the usage tally, reviewer submissions, and above all the leaf list. That last one is what every Merkle proof this server issues is rebuilt from — expire it and the anchors on chain survive, provably meaningless. Anything added later defaults to durable too, because an oversight that keeps data uses disk, and an oversight that deletes data destroys it.
When the patient speaks, a language model is speaking. We prefer to run that model on our own hardware — a local gateway, where nothing leaves the building — and the app uses it whenever it can reach it. The public bay has no GPU, so it uses a cloud model: Google Cloud's Vertex AI, google/gemini-3.1-flash-lite by default, inside the Google Cloud project this service runs in. The status line and the debrief name which model answered.
This is a deliberate exception to our standing rule that inference stays local, recorded as such in the repository. It is defensible for one reason only: the patient is synthetic, so there is no patient data in what is sent.
Nothing else. Not your key, not your session id, not your name — there isn't one — and not your IP address, because the request is made by our server, not by your browser.
What you type is up to you, and that is the point of saying this. Do not type anything about a real patient, and do not type anything about yourself you would not send to Google. It goes to the model, it goes on the tape, and the tape's hash is what you would be anchoring.
Nothing the model says is ever an input to a score. Same tape, same outcome, same marks, with no language model anywhere in the scoring path.
The bay is free and the inference is paid for by donations, so one visitor must not be able to spend the month. Requests are counted per network address — the only thing a stranger cannot rotate for free.
The address itself: the last entry in the
X-Forwarded-For header, or the socket's IP when there is no such header. It is used
as a key in a map held in the running process's memory, pointing at a list of
request timestamps.
This page used to say first, and so did the code. Everything before the last entry is text the caller wrote, so a visitor who wrote their own header got a fresh allowance on every request — which is a limit that only applies to people who were not avoiding it. Reading the last entry keeps the property the first one was chosen for: it is the address your network presents to us, the same for everyone in one building and different for two.
Where it is not: it is never written to our store, never to a disk, never to Firestore. Restart the server and every one of them is gone. The application writes no access log of its own and never logs a request.
How long: timestamps older than 24 hours are dropped whenever that address is seen again, and the whole map is pruned once it passes 4,096 addresses. So: 24 hours, in memory, with an entry for an address that never returns sitting idle until the next prune.
What is not kept beside it: no user agent, no cookie, no request path, no identifier, and no link between an address and a run, a key, a score or a review.
Three counters use it, and they are separate: opening runs, submitting reviews, and talking to the patient. Only the last one spends money, and only that one is also capped for the whole bay per month.
One thing that is not ours. This service runs on Google Cloud Run, and the platform keeps its own request logs, which include the source IP address, under Google Cloud's retention for that project. We do not read them for anything but debugging a fault, but they exist and we would rather name them than let "we do not log your IP" stand as the whole story.
That page is sent by hand to a small number of reviewers — a final-year student and a practising physician — and is linked from nowhere. If you are reading this having filled it in, this section is the one that is about you.
It does tell us not to name you in the rubric files we publish. We write "reviewed by a practising physician" instead.
It does not anonymise the record. If you typed a name and a contact, they are stored exactly as you typed them, in the same document as everything else, whether the box is ticked or not. It is a request about how we credit you — not a delete, and not a mask.
If you want us to hold nothing that identifies you, leave the name and contact boxes empty. The form works perfectly that way, and the answers are just as useful.
The form has no login and no caller identity of any kind — deliberately, because requiring an account to tell us what is wrong means the review never arrives. One honest consequence: two people who both leave the name blank and answer identically are stored as a single record, because nothing on that endpoint can tell them apart.
Your draft is saved as you type under vitals-review-draft in your own browser, and
it never leaves your device until you press send. A successful send clears it.
What we do with a review: read it, and change the product. That is all. It is never an input to anybody's run, score, mark sheet or anchored record — the code that files a review touches no session, no tape and no tree, and an opinion about the product is data recorded beside a run and never inside one. Reviews are kept rather than expired, because they are documents we act on over months.
You can have yours deleted, or have your name taken out of it. Mail paripol@megawiz.co and it will be done. We can do this because a review lives on our server and not on a chain.
แบบฟอร์มนั้นเก็บ: บทบาท (นักศึกษาหรือแพทย์) คำตอบทุกข้อพร้อมคำถามที่แสดงและตัวเลือกที่กด กล่องข้อความท้ายฟอร์ม รุ่นของระบบที่ท่านตรวจ และเวลาที่เซิร์ฟเวอร์ได้รับ — รวมถึงชื่อและช่องทางติดต่อ ถ้าท่านกรอก ทั้งสองช่องไม่บังคับ
ช่อง “ไม่ประสงค์ให้ระบุชื่อ” หมายถึงเราจะไม่ใส่ชื่อท่านในไฟล์เกณฑ์ให้คะแนน โดยใช้คำว่า “ตรวจโดยแพทย์เวชปฏิบัติ” แทน แต่ไม่ได้ลบชื่อและช่องทางติดต่อออกจากข้อมูลที่เก็บไว้ ถ้ากรอกไว้ ระบบยังเก็บตามที่พิมพ์ ถ้าไม่ต้องการให้เก็บสิ่งที่ระบุตัวตนได้เลย ให้เว้นทั้งสองช่องว่าง — ฟอร์มยังส่งได้ตามปกติและคำตอบมีค่าเท่าเดิม
ร่างคำตอบถูกเก็บไว้ในเบราว์เซอร์ของท่านเองจนกว่าจะกดส่ง เราใช้คำตอบเพื่อแก้ผลิตภัณฑ์เท่านั้น ไม่นำไปคิดคะแนนของใคร · ตัวคำตอบไม่ถูกเผยแพร่ สิ่งเดียวที่อาจปรากฏต่อสาธารณะคือชื่อท่านในฐานะผู้ตรวจ ในไฟล์เกณฑ์ให้คะแนน และจะไม่ปรากฏถ้าท่านติ๊กช่องไม่ประสงค์ให้ระบุชื่อ · ต้องการให้ลบข้อมูลของท่านหรือถอนชื่อออก ส่งอีเมลถึง paripol@megawiz.co แล้วเราจะดำเนินการให้ — ข้อมูลส่วนนี้อยู่บนเซิร์ฟเวอร์ของเรา จึงลบได้ ต่างจากสิ่งที่ถูกบันทึกลงบล็อกเชนในข้อ 1
The landing page, the donate page, the play page and the deck load Google Analytics 4 (G-LH5JSMQBSL) — and only when the hostname ends in vitals.academy. On a local build, a preview URL or anything else, no tag is loaded at all.
The tag is loaded with Google's consent mode set to denied for both analytics and advertising storage. In that state it sets and reads no cookies and stores no identifier on your device — which is what the banner says. Here is the part a banner never says: the tag file is still fetched from googletagmanager.com and events are still sent to Google without storage, and any request to Google reveals your IP address and user agent to Google simply by being a request. "No cookies" is true. "Nothing is sent" would not be.
Accept grants analytics storage and GA behaves as ordinary GA — cookies, a
client id, the usual. Decline leaves it denied. Either way the choice is remembered
in your own browser under vitals-analytics-consent; clear site data and you will be
asked again.
The name of the control you clicked and the path of the page. The control's name is deliberately
never its visible text — the code reads data-ga, id,
aria-label, href or the bare tag name, and never textContent,
so a control that could ever render something a learner typed cannot report it.
/review loads no analytics at all, and neither does this page or the terms page.
The typefaces are served by this site itself. Until 30 September 2026 most pages loaded them from Google's font servers, which was a request from your browser to Google before any banner; they no longer do. All four are under the SIL Open Font License.
Four pages load no web font at all, and they are worth naming because they reach nothing but this server: the three bedside device screens — /device/monitor, /device/vent and /device/pump — and the deck. Those set type in whatever the machine already has. The device screens also load no analytics, so a monitor left running on a second display makes no request to anyone but this server.
Three endpoints are open to anyone: /api/usage, /api/meter and /api/fuel. Open them; the list below is everything in them.
curl or a
Python script, and a single combined figure invited quoting automated traffic as peopleNo individual is identifiable in any of it. There is no signup here, so there is nothing that is a person to count — every one of those numbers is runs or devices, and the endpoint says so in its own first field rather than leaving it to this page.
The device count is the size of a set, and the set's contents are never served. Each entry is the first four bytes of sha256(month ‖ your public key) — 32 bits, salted with the month. It cannot be reversed to a key, it de-duplicates one browser within a month and nothing more, and the same browser is a different fingerprint next month. Only the current month and the one before it are kept.
One endpoint is not aggregate, and it is fair to name it: /api/chain?player= followed by a public key reports how many runs that key has proven on this server's current tree. It needs the key to answer, and it reads that number off the chain — where anyone holding the key could already read it.
Vitals is built and operated by MegaWiz Co., Ltd., Bangkok, Thailand — the company behind the Embla clinical-simulation engine. Contact: paripol@megawiz.co.
Vitals runs in its own Google Cloud project, separate from Embla's, deliberately. Embla holds identified users who consented to Embla; a service sharing that project could read those documents whether or not it ever meant to, so the separation is the default rather than something permissions have to be talked into.
Mail us. There is no form and no account to log into, because there is no account.
Not us, not you, not a court order we could comply with. §1 says why, and it is the reason that section is first.
No lawyer has read this. It is a description of what the software does, written by the people who wrote the software. It does not state your rights under Thailand's PDPA, the GDPR or any other statute, it does not name a governing law or a supervisory authority, and it is not a waiver of anything you are owed under them. Those are questions for a professional, and when one has answered them this page will say so and say who. Until then we would rather leave a gap that is visible than fill it with a clause we invented.
This page is compiled into the server binary, so it can only change when a new build is deployed. The build it belongs to is stamped at the top.