Case study · Solo build
A credential and escrow system where every claim is checkable
Mark SiazonStellaroid EarnStellar testnetPublished 30 July 2026
Outcome: the credential page became the second most visited part of the site
I built a system that anchors a training credential on Stellar and releases payment against it, then put it in front of real people. In the four months from April to July 2026, 48 visitors opened one specific public credential page and 42 opened the proof index, second only to the landing page at 302 visitors.
The whole premise rests on one bet: a credential is only worth anchoring if someone will actually open it. The traffic says they do.
The contract behind those pages rebuilds byte for byte from release tag v3.0.0, and a weekly CI job re-checks it against live testnet bytecode.
Placement
Top 5 of 105 participants, Rise In Stellar Smart Contract Bootcamp
The problem
A bootcamp graduate finishes a course and gets a PDF certificate. An employer has no cheap way to check it is real. So the graduate re-proves the same skills through unpaid trial work, and the employer pays for verification with their own time.
Two failures sit underneath:
- The credential is not independently checkable. Verifying it means trusting the issuer, or emailing them.
- Proof and payment are unrelated. Nothing ties “this person is qualified” to “this person got paid for the work”.
What I built
A Soroban smart contract on Stellar testnet with 19 public functions covering the full path: an issuer registry with admin approval, credential registration and verification, and an escrow for paid trials with milestone approval and release.
- 01RegisterApproved issuer anchors a credential hash
- 02VerifyIssuer marks it verified on chain
- 03ReadAnyone opens the proof page, no wallet
- 04FundEmployer escrows testnet XLM against it
- 05ApproveGraduate submits, employer approves
- 06ReleaseContract releases the escrow
Around it, a Next.js 16 application with four properties I cared about more than features.
Proof pages need no wallet
A credential lives at a public URL keyed by its SHA-256 hash. An employer opens it, reads the status straight from chain, and installs nothing.
Payment is escrowed, not promised
An employer funds an opportunity against a verified credential. The graduate submits a milestone, the employer approves, and the contract releases the testnet XLM. Money and proof move through the same record.
All amounts are Stellar testnet XLM and carry no monetary value. Testnet removes the financial risk. The mechanics are still real: authorization checks, state transitions and the settlement path all execute exactly as they would on a live network.
Two wallets natively, six more through a kit
Freighter and Albedo directly, plus LOBSTR, xBull, Hana, Rabet, Klever and Bitget through Stellar Wallets Kit. All behind one provider interface, lazy loaded so the multi-megabyte SDK stays out of first load.
A read-only MCP server for agents
/api/mcp exposes six tools, so an AI agent can query credentials and escrows straight from chain with no wallet, login or API key. First-party dependencies only, rate limited at the edge, covered by 14 end-to-end tests.
What the data showed
The site went live in March 2026. The first recorded visitors arrived in April; every earlier month is zero. Figures are from Vercel Web Analytics, production.
| Month | Visitors | Page views |
|---|---|---|
| April 2026 | 66 | 274 |
| May 2026 | 111 | 699 |
| June 2026 | 43 | 175 |
| July 2026 | 122 | 467 |
| Total | 342 | 1,615 |
One visitor can open several pages, so the per-page figures below do not sum to 342.
Proof pages were the draw
48 visitors on one specific credential page, 42 on the proof index, against 302 for the landing page.
I stopped treating proof pages as an output and started treating them as the product, which is why they became crawlable, shareable and embeddable.
A third of traffic was mobile
31% mobile and 2% tablet, which lines up with the 33% running Android or iOS. I had been building desktop first. That number is why the app got app-style bottom navigation, bottom-sheet dialogs, safe-area handling for notches, and became an installable progressive web app.
Where the traffic came from
19 visitors arrived from Google. Facebook and Messenger sent 49 between them. The audience skewed Philippines at 50% and United States at 35%, which is the direct reason Tagalog shipped alongside English. The other four locales are a forward-looking bet, ready ahead of the data.
Reproducible builds: verifying the deployed WASM hash yourself
I wanted the deployment claim to be something a stranger could confirm without trusting me.
The deployed bytecode attests its own toolchain
The contract records rsver 1.95.0, rssdkver 26.1.0 and cliver 27.0.0 in its own metadata. Read it with stellar contract info meta and you learn how it was built without reading my documentation.
Tag v3.0.0 rebuilds to the live hash, and CI checks it weekly
Deployed WASM hash
1b7479f1ca0f12846bbfdd8f0681670692e29e1f20618150912f010b7caf4b9f
Tag v3.0.0 rebuilds to that hash, which is what is live on testnet. A CI job re-runs the rebuild weekly and compares it against bytecode fetched from the network.
So the claim rots loudly instead of quietly.
Reproducibility verifies against a tag, never a branch
Reproducibility broke and I assumed the claim had been false. It had not. It was aimed at the wrong commit. The source was byte-identical to the tag, but the crate had moved directory and a dependency bump had landed after deployment. Either change alone alters the emitted WASM. Deployed artifacts verify against the tag they were built from, never against a moving branch.
The same source builds to a different hash on Windows and Linux
Identical source, identical Rust 1.95.0, identical Stellar CLI 27.0.0, and the emitted WASM hash still differs by host operating system. The deploy happened on Windows, so the weekly job runs on Windows deliberately, not on a cheaper Linux runner. I ruled out line endings and build paths before accepting that conclusion.
What I caught and corrected
Every claim on this page gets checked, and sometimes the check is what finds the problem. The corrections are the part I would want to read.
A “0 CodeQL alerts” claim my own re-check overturned
After enabling CodeQL I queried the alerts for my branch, got an empty array, and wrote “0 alerts” into the security checklist. The query returns empty before the analysis associates, which is not the same as finding nothing. The real first run surfaced 7 alerts. I rewrote the row with the actual findings and now verify per alert, not per branch list.
Four of the seven were real and got fixed. An unpinned GitHub action, now pinned to a commit SHA. Two log-injection findings: a lead-capture route that logged a submitted email address, and a client error handler that wrote an unescaped stack across multiple log rows. And an unanchored regular expression in a test, replaced with an exact match. The lead-capture route now writes a fixed breadcrumb string, so no user-submitted content reaches a log line at all, which the source shows. The remaining three were false positives, dismissed with the reasoning recorded.
An overstated control, corrected in all four places
A security checklist claimed the robots file blocked crawlers from proof routes. It does the opposite, on purpose, because a credential nobody can link defeats the point of publishing it. That claim appeared in four places, including the live documentation site.
Overstating a control is worse than lacking one, because it stops you looking.
Stale hardcoded counts, now pointed at a live source
Running a structured QA cohort added 8 issuers, 8 credentials and 8 escrows to the contract, which silently made the registry counts in my own submission documents wrong. Those figures should have come from the live status endpoint, not from prose. The documents now point at something self-updating instead of a snapshot I have to remember to fix.
A measurement gap the analytics surfaced
/start is a one-tap flow to connect a wallet, auto-fund on testnet and sign a real transaction. It does not appear in my top pages, and I never checked whether anyone reached it. Building the funnel is not the same as measuring it.
How I count users
Every number on this page follows one rule: state what was measured, and state it next to the number rather than in a footnote. The user count is where that rule earns its keep.
The testnet coverage spans 54 wallet accounts, and the split travels with the figure everywhere it appears: 30 independent participants from the public review, plus 24 accounts I created and operate myself for structured QA across eight role-based flows. All 54 are real, separately funded testnet accounts with public transaction history, and the two groups prove different things. The 30 show real people chose to use it. The 24 show the full issuer, graduate and employer path holds up under repeatable, scripted exercise.
I would rather hand someone a smaller number they can trust than a larger one that collapses when they check.
Every one of the 30 participant wallets and all 72 QA transactions resolve on Horizon right now.
Check every claim yourself
| Contract | CAD6C24POQGRYXMBNBEGVDHUROF5ZC37XRDC6NCVILTXWMYJIBMISZCV on Stellar testnet |
| Deployed WASM | 1b7479f1ca0f12846bbfdd8f0681670692e29e1f20618150912f010b7caf4b9f |
| Source verification | Reproducible from tag v3.0.0 |
| Re-verification | Weekly CI rebuild compared against bytecode fetched live from testnet |
| Contract surface | 19 public functions, 17 typed error codes, 16 events |
| Tests | 12 contract, 99 frontend unit, 42 end to end. The end-to-end suite drives the real contract flows against live testnet, including register, verify, pay, the escrow lifecycle, and all 14 MCP endpoint cases. |
| On-chain state | 14 issuers, 114 credentials, 25 escrowed paid trials as of 30 July 2026. Current figures are on the live status page. |
| Security | CodeQL across TypeScript, Rust and Actions; five edge rate-limit rules; CSP with nonces; no secrets in the client bundle |
| Stack | Soroban and Rust, Next.js 16, React 19, TypeScript, Tailwind, Playwright |
A solo build still gets reviewed. Standing in for the second reviewer, by design: static analysis across all three languages, 153 tests, and a weekly job whose whole purpose is to catch me being wrong about the deployment.