One research instrument I built alone, three production systems at Amazon Health, and an independent product I still operate. Each entry states what the system does, what I was responsible for, how it is put together, and which engineering problem was actually hard.
A behavioral research instrument built from a measurement limitation in a study I worked on: repeated, in-context mood sampling paired with recorded phone use.
Role: Sole authorContext: IndependentSince: 2026Status: Available on Google PlayStack: Kotlin · Jetpack Compose · Room · on-device analysis
PhoneMood records active phone use and app-level foreground time, requests a 1–10 mood rating once a configurable amount of active use has accumulated, and analyzes the resulting records on the device. It exists because of a specific gap: in the PISA 2022 study I contributed to, pre-sleep digital self-regulation is measured by a single retrospective self-report item, and the paper’s own limitations section proposes adding behavioral measures such as smartphone logs. PhoneMood is my attempt to build one.
My ownership
Everything, and the repository shows it: the research questions and literature review, the sampling protocol, the Android application, the session engine, the statistical engine, the export schema, and the privacy model — one person, no framework doing the analysis for me. Where the design follows a published finding, the review says which one.
PhoneMood instrument architectureAs implemented in the public repository.
Usage eventsApp-level foreground records
→
Session engineActive use vs elapsed time
→
Check-in promptIssued at a use threshold
→
Response or non-responseAnswer time recorded either way
→
Statistical engineWithin-person models
OutputReview screens + JSON export
Engineering problems worth naming
Time alignment. A mood rating is only interpretable if you know what it is anchored to. The session engine separates active use from elapsed time — a locked screen interrupts active use — and the database records when a prompt was issued separately from when it was answered, so a late answer is analyzed as a late answer.
Missing observations are observations. Unanswered prompts, dismissals, and gaps in usage monitoring are written to the database and carried into the analysis sample. Dropping them would quietly select the sample toward the moments when someone felt like replying, which is exactly the bias the instrument exists to avoid.
Reconstructable analysis. Dataset construction applies the inclusion rules in one place and exports them alongside the data, so the observations, the decisions that produced the sample, the model inputs, and the estimates travel together.
Methodology
Methodological choice
Research rationale
Usage-triggered momentary assessment
Pair app-level behavioral records with mood reports anchored to actual response times, preserving the sequence of use and experience.
Within-session centering and weighted regression
Estimate mood change as active use accumulates within a session, separating that relationship from differences in average mood between sessions. Weight observations so each session contributes equal total weight.
Conditional comparisons of app use
Model the ending mood rating with adjustment for starting mood, total active use, elapsed time, and time of day. Interpret app time relative to time allocated to other apps, using comparisons supported by the observed data.
Uncertainty and stability checks
Estimate uncertainty with HC3 or day-clustered covariance as specified by the analysis policy. Refit models while leaving out days or sessions to examine whether a pattern depends on a particular block of observations.
Traceable sample construction and analysis
Retain unanswered prompts, response delays, and monitoring gaps. Export observations, inclusion decisions, model inputs, and estimates together so the analysis can be reconstructed.
Recorded phone use, mood ratings, and answered versus issued prompts on one screen. Synthetic demonstration data.A mood rating requested after 30 minutes of active use. Synthetic demonstration data.Daily mood across a selected period, with the underlying records available. Synthetic demonstration data.
Where it stands
Implemented
Active-use and app-level recording, session boundaries, and prompts triggered at a configurable use threshold
Notification and floating-card check-ins with snooze, dismiss, and pause controls
Within-session models, conditional app comparisons, HC3 and day-clustered uncertainty, and deletion-based stability checks
Daily and period review, exploratory associations, and JSON export with quality metadata
Fully local operation — no account, no server, no internet permission — with English and Chinese interfaces
Current testing
An engineering validation record covering build, unit, device, lint, and emulator checks for the 1.0.0 build
Analysis unit tests and schema checks run against clearly labeled synthetic exports
Small-scale user testing underway; participant counts and procedures are not documented, and no participant findings are reported
Planned evaluation
Compare usage estimates against recorded behavior, and check device coverage, prompt timing, and the mood item itself
Examine within-person use and mood associations with explicit handling of prior mood, time of day, and missing responses
Test whether reviewing one’s own records helps a person recognize patterns, keeping that separate from any claim about outcomes
These are three different kinds of claim and the project keeps them apart. Tests show that the software behaves as specified. Any association the app can estimate is observational and exploratory. Whether the mood item is a valid measure of anything is a study to run, not a result to report — and no participant findings, psychometric validation, or health claims are made here.
Why it matters for research
The instrument makes a measurement question answerable: how closely does recorded phone and app use agree with what people report, and which experiences do usage-triggered prompts miss? The pattern — a question, a sampling rule, an instrument, and an analysis path a reader can reconstruct — is what I would bring to a group’s data collection.
A multimodal verification pipeline that compares four kinds of evidence about one prescription and reports the specific disagreements to the pharmacist who decides.
Role: Software Development EngineerContext: Amazon Health, team systemYears: 2022–2026Stack: Python · AWS Bedrock / Claude · Lambda · DynamoDB
What I built
Before a prescription ships, someone has to reconcile the prescription image, the text extracted from the label, the order record, and the database entry for that medication. The copilot performs those comparisons and presents what disagrees, with the evidence attached. It is decision support inside an existing pharmacist workflow; the pharmacist retains final authority over every outcome.
My ownership
I built pipeline components and comparison logic, and developed the checking and escalation controls together with pharmacists and applied scientists. This was a team system: the parts I owned are the evidence comparison path and the controls that decide what the system may pass without human attention, not the whole architecture on my own.
Human-in-the-loop verification architectureProduction architecture; pharmacists retain final authority.
Evidence inputsRx image, label text, order + DynamoDB
→
AWS Lambda pipelinePython orchestration
→
Specialist agentsRationality, image and label checks
From one model to coordinated specialists. The architecture moved from a single model to a single agent, and then to specialist agents for prescription rationality, image-to-record comparison, and label-to-record comparison. Narrowing what each agent judges makes its output checkable against one specific piece of evidence, and structured cross-checks between them reduce hallucination risk before pharmacist review. The cost is more calls and more orchestration per prescription, inside a latency budget the workflow sets.
“I cannot tell” has to survive to the screen. In the output below, the system lists the mismatched fields and then states that the available image does not support a definitive comparison. A disagreement and an inconclusive comparison call for different actions, so the output preserves that distinction instead of collapsing everything into one score.
Evaluation at two levels. Agents were evaluated individually and as a combined system against pharmacist-reviewed cases, because components that each look acceptable can still fail together. False-positive handling, gating, and escalation rules were designed with the pharmacists who would be interrupted by them.
The system reports specific mismatches and, in the last item, states that the available image does not support a definitive comparison. Demonstration data.
Scale and results
3,000+ images / dayPrescription images processed by the verification pipeline in production.
Under 800 msAverage latency of the verification pipeline, not of the full pharmacist task.
92% pre-screening accuracyReported for the system against pharmacist-reviewed cases, before pharmacist review — it says nothing about the quality of the pharmacist’s decision.
95% less manual verification, 4× throughputWorkflow effects reported for the deployment. These are operational measures, not evidence of clinical outcomes.
Why it matters for research
This is where one of my research questions comes from: what does a person need to see in order to judge model-generated evidence well, and when should a system withhold a recommendation rather than produce one? Building an answer and testing it empirically are different tasks. I have done the first and would like to do the second.
PharmacyLite platform
The full-stack application pharmacists and operations staff actually work in: scan, read the record, verify, handle the exception, complete — with every step auditable.
Role: Architected the review applicationContext: Amazon Health, team systemYears: 2022–2026Stack: React · Java · AWS ECS · DynamoDB
What I built
Staff scan a prescription label or vial barcode, read the prescription and shipment record attached to it, record a pass or fail with a reason, and route anything unusual into an exception queue. PharmacyLite is where all of that happens. It is a separate product from the verification copilot: the copilot produces evidence, the platform is where people act on it and where the record of that action lives.
My ownership
I architected the review application across the React interface, the Java APIs, and the backend workflow orchestration, including the review-state model and the APIs other pharmacy services call. I also ran iterative workflow sessions with pharmacists and operations staff and folded what they reported back into the flow.
Review workflow architectureOne surface over fill, shipment, prescription, and review state.
Barcode scanReact review interface
→
Java review APIsPrescription + shipment lookup
→
Workflow orchestrationAWS ECS services
→
Review state + historyDynamoDB records
RecordAuditable pass, fail, or exception
Engineering problems worth naming
A review is a state machine, not a form submission. A verification can be in progress, passed, failed, cancelled, or complete, and it can be interrupted — a shift ends, a vial goes back on the shelf, a scan is repeated. Each transition is written with its history, so a review that stopped halfway can be resumed or cancelled explicitly instead of being left in a state nobody can interpret a week later.
Exceptions are the normal path for the hard cases. A failed verification has to carry a reason and a route to someone who can resolve it. Structuring review outcomes and problem information this way is what makes operational monitoring and after-the-fact investigation possible at all.
State the platform owns, records it does not. The application connects fill, shipment, and prescription records owned by other pharmacy services with the review state it owns, so the review surface stays consistent with systems it cannot control.
The workflow platform pharmacists and operations staff work in. Demonstration data.
Scale and results
200+ internal usersPharmacists and operations staff using the platform.
10,000+ reviews / dayPrescription review requests handled by the platform, at 99.9% uptime.
~30% fewer manual stepsReduction in manual verification steps after iterative workflow sessions with pharmacists and operations staff.
Separate from the copilotTwo different products in the same workflow; the figures above describe this platform only.
Why it matters for research
Human-in-the-loop research needs the loop to exist. Building the surface where a person sees evidence, decides, and leaves a durable record is the same instrumentation problem as behavioral data collection, with a different subject and higher stakes.
Pharmacy fulfillment cost estimation
A high-throughput service that answers, for a medication at a given facility, which processing path it takes and what dispensing it costs — fast enough to sit inside a routing decision.
Role: Built the API, data flow, and serving pathContext: Amazon HealthYears: 2022–2026Stack: Java · AWS ECS · DynamoDB / DAX · Lambda + SQS · CDK
What I built
The same medication can be dispensed through different processing paths at different fulfillment facilities, and those paths cost different amounts. A request names a set of medications and one facility; the response carries a per-medication dispensing cost and the processing path behind it. The estimate covers dispensing, not a complete shipping and packing cost model.
My ownership
I built the cost API, the facility- and medication-level data flow behind it, the asynchronous update path, and the low-latency serving path, with the ECS API and the queue-driven ingestion defined as separate CDK stacks.
Two paths through the serviceUpdates run off the request path so answers stay fast.
Event path — keeping each facility’s processing path current
Dispensing eventsRecent operational history
→
SQS queueAsynchronous ingestion
→
Lambda handlerProcessing-path update
Stored statePath per medication + facility
Request path — answering a cost query
Cost requestMedications + one facility
→
API on ECSJava service
→
Parallel readsPath + cost config via DAX
→
Lookup or fallbackExplicit unknown path
ResponsePer-medication cost + path
Engineering problems worth naming
Two reads, in parallel, behind a cache. A cost answer needs both the processing path for that medication at that facility and the cost configuration for that path. Issuing those reads in parallel and serving them through DAX brought DynamoDB read latency from roughly 400 ms to roughly 10 ms — the difference between a service you call inside a routing decision and one you call ahead of it.
Unknown stays unknown. When a facility has no recent dispensing history for a medication, the service returns an explicit unknown-path result with a fallback estimate instead of silently substituting a default. Downstream routing can then treat “we do not know” differently from “we know it is cheap,” which is the whole point of making the gap visible.
Say what the estimate actually is. Processing paths are updated from recent dispensing history through a rule applied off the request path. That is a rule-based selection over operational history, not a trained forecasting model, and the service is described that way here because it is what the code does.
Scale and results
2,000+ requests / secondSustained request rate served by the cost API.
20+ fulfillment facilitiesFacilities covered, each with its own processing-path records.
~400 ms → ~10 msDynamoDB read latency on the serving path after parallel reads and DAX. This is the database read, not end-to-end service latency.
$9M+ annual savingsReported for the cost-aware routing decisions this service feeds. The service is one input to that routing, not the sole cause.
Why it matters for research
Estimates become decisions here. What a downstream system does when an estimate is missing, stale, or uncertain is a design question with a direct research analogue: how uncertainty should be represented to whoever — or whatever — acts on it.
Douxingma
A bilingual VPN product I designed, built, deployed, and still operate — the whole thing, from the signup form to the certificate renewals.
Role: Sole developer and operatorContext: Independent product, in productionStack: React · TypeScript · Python / FastAPI · DynamoDB · AWS Lightsail · CDK
What I built
Douxingma brings account registration and recovery, subscription redemption, connection setup, and traffic usage into one bilingual website. A React frontend talks to a FastAPI backend for email verification, account access, subscription redemption, and administration; DynamoDB holds the application records; payment callbacks record order status and apply subscription upgrades.
My ownership
All of it, and it is the part of my portfolio with no one else to blame: product decisions, frontend, backend, data model, infrastructure in AWS CDK on Lightsail, deployment, and the ongoing operations. The source repository is private, so the link below goes to the running product.
Douxingma architectureOne application path, plus a scheduled reconciliation against the running service.
Application path
React frontendBilingual site
→
FastAPI backendAuth, subscriptions, admin
→
DynamoDBAccounts, orders, subscriptions
ApplicationAccount + subscription state
Reconciliation path
Scheduled jobRuns on a fixed cadence
→
VPN management dataTraffic usage read
→
Usage recordsWritten to application store
Reconciled viewUsage shown per account
Engineering problems worth naming
Two systems, one subscriber. The VPN management system and the product’s own records each know something different about an account. A scheduled job reconciles traffic usage from the management database into the application records, so what a user sees matches what the service is enforcing. That makes the display eventually consistent — a tradeoff I took deliberately rather than coupling every page load to the management system.
Payment callbacks arrive whenever they arrive. Order status and subscription upgrades are applied when the payment provider calls back, which may be late or repeated relative to what the user is doing in the browser. The order record is the authority for what was paid for, and account state is derived from it.
Operations are part of the product. Account recovery, bilingual onboarding, deploys, and certificate renewals are all mine too. The failure mode I care about most is the one that locks a paying user out of their own account.
Where it stands
In production and actively maintained. It is not research, and I do not present it as such: it is the clearest evidence I have that I can take a system from an idea to something strangers rely on, and keep it running.