← Back to ElementELEMENT / Lab notes

Technology

A technical write-up of Element: what we built, how each part works, what we measured and where it falls short. Each tool is listed with the reason we chose it and what that choice costs us. Bracketed numbers point to the references at the end.

1. Methods: from a lid photo to a perfumer's bench

One test, followed from start to finish. Steps marked AI call a model while the test runs; every other step is ordinary code that we can test and read. Where a model is used, its answer is checked, and a plain fallback is ready if the check fails.

YOUR DATA PATH01 Scan the lidQRqr-code-styling02 Guided testvinext · React ·WorkersAI03 Lidphotos, readon the phonejs-aruco2 ·photo-mathAI04 Adaptivesurvey +voiceLlama 3.3 70B ·Whisper · bge-m305 Nose modelscikit-learn → JSON06 Note-graphtuningTypeScript rulesAI07ExplanationLlama 3.3 70B +validator08 Share +compareD1 · Email Service09 PerfumerreviewPeopleBUILD AND RELEASE01 PythonreferenceOpenCV02 Browser portjs-aruco2 ·TypeScript03 Testsnode --test04 D1migrationsSQL05 Review, thendeployWrangler → WorkersYOUR DATA PATH01 Scan the lid QRqr-code-styling02 Guided testvinext · React · WorkersAI03 Lid photos,read on the phonejs-aruco2 · photo-mathAI04 Adaptivesurvey + voiceLlama 3.3 70B · Whisper ·bge-m305 Nose modelscikit-learn → JSON06 Note-graphtuningTypeScript rulesAI07 ExplanationLlama 3.3 70B + validator08 Share + compareD1 · Email Service09 Perfumer reviewPeopleBUILD AND RELEASE01 Python referenceOpenCV02 Browser portjs-aruco2 · TypeScript03 Testsnode --test04 D1 migrationsSQL05 Review, thendeployWrangler → Workers
Figure 1. Top lane: what happens to one person's data. Bottom lane: how the code gets there. Gold outlines mark steps that call an AI model while the test runs.
  1. 01

    Scan the QR on the printed lid

    The kit lid carries a QR code that opens the guided test at yatlifestyle.com/element/demo. Every QR the site draws, including share and compare links, uses the same rounded gold style with the Element mark in the centre and error-correction level H, so the logo does not stop phones from reading it [28].

    qr-code-styling
  2. 02

    A guided test on the phone

    The test is one page that walks through consent, setup, a four-vial sensitivity ladder, scent ratings, the lid photos and the survey. The ladder hides a blank, a weak, a medium and a strong vial behind numbers: reporting the blank is treated as a false alarm and missing the strong vial as a miss, borrowing the logic of signal detection [8] and, loosely, of staircase threshold tests [7]. Either outcome halves the weight given to the person's traits [26]. Between scents the test asks for a short pause and a sniff of the inner elbow; recovery time depends on the odour and the person [9], and coffee beans showed no advantage in the one exploratory test we found [10]. A session is created after the person confirms they are 18 or over and gives consent [24].

    vinext · React · Workers
  3. 03

    Lid photos, read on the phoneAI

    The phone finds the printed ArUco fiducial markers (4x4 dictionary) [11] with js-aruco2, uses the four markers around each zone to solve a four-point homography [12], and straightens the zone to 10 pixels per millimetre. White and black patches printed on the lid normalise the colour. The blot is scored against its own dry border and a six-step printed scale, and each pH pad is converted to CIELAB and matched to the printed chart by colour difference, deltaE [13]. Blur, glare and shadow become quality flags instead of silent errors [21]. The server receives the straightened zone and the numbers; a vision model then checks placement and glare, and never produces a measurement [24].

    js-aruco2 · photo-math
  4. 04

    An adaptive survey, with optional voiceAI

    A bank of 27 questions in three languages covers routine, climate, activity, social settings, memories and dislikes [23]. After the opening question, Llama 3.3 70B on Workers AI, called through AI Gateway, either picks the next bank question or writes a short follow-up. Its answer is validated: English and Chinese ban lists, at most 120 characters, two to five answer chips, the right script for the chosen language, and no more than three AI follow-ups in a test. Anything that fails, or arrives too late, is replaced by the next bank question [23, 24]. People can also answer by voice: Whisper large v3 turbo transcribes, bge-m3 embeds the transcript, and Vectorize returns the closest of 71 lifestyle tags [24].

    Llama 3.3 70B · Whisper · bge-m3
  5. 05

    A nose model trained on real smell data

    Scent-family ratings go into a small model trained offline on Keller and Vosshall (2016): 55 people rating 480 molecules, published under CC BY 4.0 [6]. k-means clustering [14] with k = 3, chosen by silhouette score [15], groups people into three nose types (Discerning, Open and Balanced), and ridge regression [16] fills in the families a person did not rate. The trained model ships as a JSON file inside the Worker, so inference is a few lines of arithmetic and needs no model call [25].

    scikit-learn → JSON
  6. 06

    Deterministic note-graph tuning

    A rule engine adjusts one of six base formulas. Traits such as oil level, routine tags and the musk flag push notes and tiers by fixed amounts. Each note moves by at most 20% of its weight or 6 points, whichever is larger, and at most two harmonious accents can be added, along curated pairs of notes that sit well together [26]. Skin properties have been linked to fragrance evaporation in a laboratory study, but that study does not validate a consumer blot photo [19], so oil changes are small and bounded. pH is recorded but never changes the formula [18, 19, 26]. Each base lists the real perfumer oils it is made from, from our supplier Candle Element [26].

    TypeScript rules
  7. 07

    An explanation that cannot change the formulaAI

    Llama 3.3 70B rewrites the computed changes as plain sentences, one per change, in the person's language. A validator checks every sentence against the change list: the right note, no other notes, no numbers except that change's before and after values, and no pH. If any line fails, or the model is slow, the page uses fixed templates that say the same thing [27].

    Llama 3.3 70B + validator
  8. 08

    Share and compare, on your terms

    Results and profiles are stored in D1. Profiles are private by default; sharing creates an unlisted link and a styled QR, and a shared profile carries family likes, nose type and tags, never skin, oil or pH. Accounts use a six-digit code sent by Cloudflare Email Service. Two profiles can be compared with a score built from family likes and shared tags [28].

    D1 · Email Service
  9. 09

    A perfumer reviews before mixing

    No formula is mixed straight from the screen. A perfumer reviews each tuned formula and its materials list before anything is blended, and has the final word on every change [27].

    People

Build and release

  1. 01

    Python reference reader

    The lid reader was written in Python with OpenCV, tested on real lid photos for marker detection and on a rendered board, flat and in perspective, for full readings [22].

    OpenCV
  2. 02

    Browser port

    The same steps and thresholds were ported to TypeScript, with a development page that runs the browser reader on the same test images as the Python tests [21].

    js-aruco2 · TypeScript
  3. 03

    Automated tests

    45 Element unit tests (51 in the whole suite) run with Node's built-in test runner [31].

    node --test
  4. 04

    Database migrations

    Five SQL migrations define the newsletter and Element tables [29].

    SQL
  5. 05

    Review, then deploy

    Each task was reviewed before merging; the site is built with vinext and deployed with Wrangler to Cloudflare Workers [29].

    Wrangler → Workers

2. Results

Every value below was measured, and each gives its source; nothing here is an estimate.

2.1

Nose model

Silhouette scores favoured k = 3 (0.311, against 0.257, 0.243 and 0.201 for k = 4, 5 and 6) [15, 25]. In leave-one-person-out evaluation over 55 folds and 330 held-out person and family targets, the ridge model's mean absolute error was 0.178 on a 1 to 5 scale, against 0.300 for predicting the training mean. The model beat the baseline in all six families, with errors from 0.147 (floral) to 0.213 (woody) [25].

2.2

Browser reader against the Python reference

We ran both readers on five images: a rendered board, a real photo of an earlier lid layout, and three real photos of the current printed lid. They found the same markers, matched the same scale step and made the same accept or refuse decisions on every image. Oil readings differed by at most 0.002 and pH readings by at most 0.008. On the rendered board, both recovered the painted pH values of 5.5 and 8.0 [21, 22, 30].

2.3

Real photos of the printed lid

Both readers detected all 8 markers in 3 of 3 photos of the current lid. Oil read 0.019 for a clean sheet and 0.085 (Python) or 0.087 (browser) for an oily sheet, on a 0 to 1 scale, matching scale steps 1 and 2. With no sheet in place, the reader returned 0.54; telling a missing sheet from an oily one is left to the placement check. With dry strips in the slots, pH confidence was between 0.02 and 0.08, too low to report a pH [30].

2.4

Models and tests

In live use the 70B model answered survey requests in about 3.5 seconds, inside the 4.5 second budget [24, 32]. The test suite ran 51 tests, all passing, of which 45 cover Element across 9 files [31].

3. Limitations

  • The nose model was trained on single laboratory molecules rated by 55 people, not on blended perfumes rated by our customers [6]. The six-family mapping is our heuristic, and with fewer known families the error rises (0.259 with one known, 0.190 with three) [25]. Transfer to blended perfumes is untested.
  • The browser reader has no subpixel corner refinement, which the Python reference uses, so its warp is slightly less precise [21, 22]. Agreement between blot readings and laboratory measurement of skin oil has not been tested, and the study linking skin properties to evaporation does not validate consumer photos [19].
  • pH confidence is low with dry strips, and skin pH varies with washing and products [18, 30]. pH is recorded with its confidence and never used for tuning [26].
  • The sample sizes are small: three photos of the current lid, one older photo and one rendered board [30]. The four-vial ladder is far simpler than a validated staircase test [7] and is a sensitivity cue, not a clinical threshold.
  • Odorant receptors vary between people, including for particular musks [4, 5, 17]. Our ratings explore preference; they do not identify genes, and the musk flag is a product heuristic.
  • The test is for adults aged 18 or over, with consent recorded per session [24]. We make no claim that a tuned perfume changes confidence or attractiveness; the related study used a deodorant that also contained antimicrobial agents [20].

4. Technology stack

Each layer of the system, the tools in it, why we chose them and what each choice costs us.

4.1 The web app

One codebase serves the brand site, the guided test and the API. We kept the stack small so a small team could build it, test it and explain it in the time available.

vinext ↗

vinext is Cloudflare's implementation of the Next.js App Router on Vite. It lets us write pages and API routes in the familiar App Router layout and deploy them straight to Workers, without a separate adapter or a second host.

React 19 ↗

The guided test is a long flow with a lot of state: consent, the ladder, ratings, the camera, the survey and the result. React keeps that state in one place and renders it on the phone. Heavier pieces, such as the marker detector and the QR library, load when a screen needs them, so the opening screen stays light.

TypeScript ↗

The domain logic (photo maths, survey rules, tuning, nose inference, compare) lives in plain TypeScript modules with no framework imports. Node runs them directly in tests, and the same files run in the browser and in the Worker.

4.2 Hosting, data & storage

Everything runs on one Cloudflare account. Each service has one job, and each binding is declared in wrangler.jsonc, so the Worker cannot reach anything it was not given [29].

Cloudflare Workers ↗

The Worker serves the pages, the static assets and the /api/element routes. It checks the request origin, applies rate limits, validates every body and caps uploads at 16 KB for JSON, 1.5 MB for a photo and 2 MB for audio. Errors never log bodies, photos or readings [24].

D1 ↗

D1 is Cloudflare's SQLite database. It holds sessions, photo readings, survey answers, results, profiles, sign-in codes and logins across seven Element tables. Every query is plain SQL with bound parameters [24, 29].

R2 ↗

R2 stores the straightened lid photos and the generated label art, under keys tied to the session. Photos show the lid, not the person. The API serves label art back to the browser and never serves lid photos [24].

Vectorize ↗

Vectorize holds one embedding for each of the 71 lifestyle tags, built from the survey bank's own question and chip text in English and Chinese. A spoken answer is embedded and matched to its three nearest tags [23, 24].

Rate Limiting ↗

Workers Rate Limiting caps each IP address at 300 Element API requests a minute, and sign-in requests at 10 a minute, before any database or model work starts [24, 29].

AI Gateway ↗

Every Workers AI call goes through AI Gateway, which gives one place to see requests, latency and errors for all five models. Each call also has its own time budget in our code, from 4 seconds for the photo check to 10 seconds for label art [24].

4.3 Reading the lid

The lid is a measurement surface. We designed it so a phone photo taken in an ordinary room can be straightened and colour-corrected using marks printed on the lid itself, with no special app or lighting.

The printed lid board

The board is 160 x 130 mm so it fits inside a 17 x 14 cm box lid. It has two zones, each photographed on its own: an oil-blot zone with an 80 x 50 mm blotting sheet and a six-step blue scale, and a pH zone with slots for a skin strip and a control strip beside a printed pH chart. Each zone has four corner markers and its own white, grey and black patches. A Python script generates the board, its print PDF and the JSON layout the reader uses [22].

js-aruco2 ↗

js-aruco2 detects ArUco markers [11] in plain JavaScript, with no WebAssembly download. We load its 4x4 dictionary, accept exact codes with no bit errors, and try each photo at full, three-quarter and half size, because its fixed threshold window can miss markers on large, unevenly lit photos [21].

photo-math

photo-math is our own module: a homography solver [12], a bilinear warp, a CIELAB conversion that matches OpenCV, CIE76 deltaE [13], a blur check, colour normalisation from the printed patches, the blot score and the pH chart match. It mirrors the Python reference step by step, with the same thresholds. pH confidence is one minus the ratio of the best chart distance to the second best [21].

4.4 Understanding you

This is where most of the AI runs. Each model has a narrow task, a time limit and a check on its output. When a check fails, the person still gets a sensible next step from ordinary code.

Workers AI ↗

We use five models on Workers AI: Llama 3.3 70B (fp8 fast) for survey choices, explanations and compare sentences; Llama 4 Scout 17B to check photo placement and glare; Whisper large v3 turbo for speech; bge-m3 for multilingual embeddings; and FLUX.1 schnell for abstract label art. They run through a binding inside the Worker, so there are no API keys in the code and no separate inference server [24].

The survey engine

The bank has 27 questions across six topics, each written in English, Traditional Chinese and Simplified Chinese, with answer chips that carry lifestyle tags. Without AI, the engine asks follow-ups triggered by earlier answers, then the least-covered topic, and stops at 12 answers. The model sees the remaining valid candidates and may pick one or write a follow-up; the validator rejects anything outside those rules [23].

Voice answers

The voice button records a short clip on the phone and sends it to the Worker, up to 2 MB. Whisper transcribes it in English or Chinese, bge-m3 turns the transcript into a vector, and Vectorize returns the three nearest tags. The transcript is trimmed to 500 characters and the audio is not stored [24].

4.5 The nose model

We wanted nose types to come from real smell data rather than invented personas. The model is small on purpose: it is trained offline, evaluated honestly, and published with its limits [25].

Keller & Vosshall 2016 ↗

55 people rated 480 molecules at two concentrations for intensity, pleasantness, familiarity and 20 odour descriptors [6]. The data is published under CC BY 4.0, and we use the CSV version from Pyrfume. We map descriptors to our six scent families and keep the 191 molecules whose strongest descriptor is at or above the 60th percentile [25].

scikit-learn ↗

A Python script standardises each person's six family averages, fits k-means [14] for k from 3 to 6 with a fixed seed, and chooses k by silhouette score [15]. It then fits a ridge regression [16] per family that predicts it from the other five, and exports the means, centroids and coefficients to JSON [25].

Leave-one-person-out evaluation

Each of 55 rounds holds one person out of every step, including descriptor averaging and family assignment, then predicts each of their families from the other five. This keeps the held-out person's ratings from leaking into the model that predicts them [25].

A JSON model in the Worker

Inference is a TypeScript function: fill missing families with the ridge formulas, standardise, and pick the nearest centroid. It needs no network call, so it is fast, free to run and covered by unit tests [25].

4.6 From profile to formula

The formula is computed by rules a perfumer can read and edit, not generated by a language model. The language model then explains the result, under checks that stop it from changing it.

The note graph

Six base formulas use 24 notes, each assigned to the top, heart or base tier. A short table of harmony pairs says which notes sit well together, and a trait table says how far each trait moves a tier or a note. Both tables are plain data a perfumer can change [26].

Tuning rules

The tuner applies each active trait, caps every change at 20% of the note's weight or 6 points, whichever is larger, adds at most two accents along harmony pairs, and rebalances the formula to 100% without reversing any trait's direction. When the sensitivity ladder looks unreliable, trait effects are halved. Oil and sensitivity choose EDT or EDP; routine tags and sensitivity set how far the scent projects [26].

Explanation guardrails

The model receives the computed changes and their reasons and must return one sentence per change, in order, plus a summary. The validator rejects a line that names another note, uses a number other than that change's before and after values, mentions pH, uses a banned word or an em dash, or runs past 200 characters. Rejected or late answers are replaced by templates written in all three languages [27].

Candle Element materials

Each base lists the perfumer oils it is made from, supplied by Candle Element and mixed into CW Perfume Base. This keeps what the screen shows tied to bottles that exist on the bench [26].

4.7 Accounts and sharing

Nobody needs an account to take the test. Accounts exist so people can come back to a profile, share it on their own terms and compare with friends.

Cloudflare Email Service ↗

Sign-in uses a six-digit code sent through the Worker's send_email binding from codes@yatlifestyle.com, in the person's language. There are no passwords to store or leak [24, 28].

Hashed codes and cookies

Codes are hashed with SHA-256 and a secret pepper before they reach D1, expire after 10 minutes, allow 5 attempts and can be re-sent once a minute. A successful sign-in sets a 30-day HttpOnly, Secure, SameSite=Lax cookie; the database stores a hash of the session ID, never the ID itself [28].

Private profiles and compare

A profile is private until its owner shares it. Sharing creates an unlisted 12-character link and a styled QR. The public view is built field by field, so skin, oil and pH can never leave the server. Compare scores two profiles: 85% from the cosine similarity of their family likes and 15% from shared tags [24, 28].

qr-code-styling ↗

It draws the rounded, dark-gold QR codes with the Element mark in the centre, at error-correction level H so the logo does not break scanning. It loads when a QR is on screen, so pages without one do not download it [28].

4.8 Building & checking

We moved fast, but we also wanted to be able to say why every number on the site is there. These are the habits and tools that let us do both.

node --test ↗

The suite uses Node's built-in test runner and needs no extra framework. 45 Element tests across 9 files cover the homography, colour maths, blot and pH scoring, survey validation and ban lists, tuning caps and direction, nose inference, explanation checks, sign-in, the API routes with stubbed bindings, and the citations on this page [31].

Python reference reader ↗

Before writing the browser reader we built it in Python with OpenCV, where every intermediate image is easy to inspect. Its plain-assert tests check marker detection on real photos of earlier lid layouts and full readings on a rendered board, flat and in perspective. A development page runs the browser port on the same images [21, 22].

D1 migrations and Wrangler ↗

Five SQL migrations define the newsletter and Element tables, and Wrangler applies them and deploys the built Worker. Bindings, rate limits and the AI, email and Vectorize connections are all declared in one wrangler.jsonc file [29].

Per-task code review

The build was split into numbered tasks, from domain logic to the landing page. Each task was reviewed before it was merged, and fixes from review were committed separately, so the history shows what changed and why.

Claude Code and Codex ↗

We built Element with two AI coding assistants: Claude Code for planning, implementation and review, and Codex for tasks run in parallel, such as the photo reader, the nose model and the landing pages. The plan assigned each task to a tool, and commits record AI co-authorship.

References

Literature [1] to [20]; Element code, data and measurements [21] to [32]. Where the lab notes review a source, a link leads to that review.

  1. [1]

    Courtiol E, Wilson DA. 2015. The olfactory thalamus: unanswered questions about the role of the mediodorsal thalamic nucleus in olfaction. Frontiers in Neural Circuits 9:49. doi:10.3389/fncir.2015.00049 doi.org ↗ Lab notes review ↗

  2. [2]

    Herz RS, Eliassen J, Beland S, Souza T. 2004. Neuroimaging evidence for the emotional potency of odor-evoked memory. Neuropsychologia 42(3):371-378. doi:10.1016/j.neuropsychologia.2003.08.009 pubmed.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  3. [3]

    Willander J, Larsson M. 2006. Smell your way back to childhood: autobiographical odor memory. Psychonomic Bulletin & Review 13(2):240-244. doi:10.3758/bf03193837 pubmed.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  4. [4]

    Mainland JD, Keller A, Li YR, et al. 2014. The missense of smell: functional variability in the human odorant receptor repertoire. Nature Neuroscience 17(1):114-120. doi:10.1038/nn.3598 pmc.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  5. [5]

    Keller A, Zhuang H, Chi Q, Vosshall LB, Matsunami H. 2007. Genetic variation in a human odorant receptor alters odour perception. Nature 449(7161):468-472. doi:10.1038/nature06162 doi.org ↗ Lab notes review ↗

  6. [6]

    Keller A, Vosshall LB. 2016. Olfactory perception of chemically diverse molecules. BMC Neuroscience 17:55. doi:10.1186/s12868-016-0287-2. Dataset CC BY 4.0; CSV via Pyrfume (keller_2016). doi.org ↗ Lab notes review ↗

  7. [7]

    Hummel T, Sekinger B, Wolf SR, Pauli E, Kobal G. 1997. 'Sniffin' Sticks': olfactory performance assessed by the combined testing of odor identification, odor discrimination and olfactory threshold. Chemical Senses 22(1):39-52. doi:10.1093/chemse/22.1.39 pubmed.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  8. [8]

    Green DM, Swets JA. 1966. Signal Detection Theory and Psychophysics. New York: Wiley.

  9. [9]

    Philpott CM, Wolstenholme CR, Goodenough PC, Clark A, Murty GE. 2008. Olfactory clearance: what time is needed in clinical practice? Journal of Laryngology & Otology 122(9):912-917. doi:10.1017/S0022215107000977 pubmed.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  10. [10]

    Grosofsky A, Haupert ML, Versteeg SW. 2011. An exploratory investigation of coffee and lemon scents and odor identification. Perceptual and Motor Skills 112(2):536-538. doi:10.2466/24.PMS.112.2.536-538 pubmed.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  11. [11]

    Garrido-Jurado S, Muñoz-Salinas R, Madrid-Cuevas FJ, Marín-Jiménez MJ. 2014. Automatic generation and detection of highly reliable fiducial markers under occlusion. Pattern Recognition 47(6):2280-2292. doi:10.1016/j.patcog.2014.01.005 doi.org ↗

  12. [12]

    Hartley R, Zisserman A. 2004. Multiple View Geometry in Computer Vision, 2nd ed. Cambridge University Press. doi:10.1017/CBO9780511811685 doi.org ↗

  13. [13]

    CIE. 2004. CIE 15:2004 Colorimetry, 3rd ed. Vienna: Commission Internationale de l'Eclairage. (CIE 1976 L*a*b* space and colour difference.)

  14. [14]

    Lloyd SP. 1982. Least squares quantization in PCM. IEEE Transactions on Information Theory 28(2):129-137. doi:10.1109/TIT.1982.1056489 doi.org ↗

  15. [15]

    Rousseeuw PJ. 1987. Silhouettes: a graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics 20:53-65. doi:10.1016/0377-0427(87)90125-7 doi.org ↗

  16. [16]

    Hoerl AE, Kennard RW. 1970. Ridge regression: biased estimation for nonorthogonal problems. Technometrics 12(1):55-67. doi:10.1080/00401706.1970.10488634 doi.org ↗

  17. [17]

    Li B, Kamarck ML, Peng Q, et al. 2022. From musk to body odor: decoding olfaction through genetic variation. PLoS Genetics 18(2):e1009564. doi:10.1371/journal.pgen.1009564 pmc.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  18. [18]

    Lambers H, Piessens S, Bloem A, Pronk H, Finkel P. 2006. Natural skin surface pH is on average below 5, which is beneficial for its resident flora. International Journal of Cosmetic Science 28(5):359-370. doi:10.1111/j.1467-2494.2006.00344.x doi.org ↗ Lab notes review ↗

  19. [19]

    Hadjiefstathiou E, Savary G, Malhiac C, Terescenco D, Picard C. 2025. Exploring the impact of fragrance molecular and skin properties on the evaporation profile of fragrances. International Journal of Cosmetic Science 47(6):981-995. doi:10.1111/ics.13085 doi.org ↗ Lab notes review ↗

  20. [20]

    Roberts SC, Little AC, Lyndon A, Roberts J, Havlicek J, Wright RL. 2009. Manipulation of body odour alters men's self-confidence and judgements of their visual attractiveness by women. International Journal of Cosmetic Science 31(1):47-54. doi:10.1111/j.1468-2494.2008.00477.x pubmed.ncbi.nlm.nih.gov ↗ Lab notes review ↗

  21. [21]

    Element source: browser lid reader. lib/element/photo-math.ts; app/element/demo/photo-reader.ts; public/kit/lid-board.json.

  22. [22]

    Element source: Python reference reader and board generator. scripts/photo_reader.py; scripts/test_photo_reader.py; scripts/make-lid-board.py.

  23. [23]

    Element source: survey bank and engine. lib/element/survey-bank.ts; lib/element/survey.ts.

  24. [24]

    Element source: API, model calls, time budgets, validation and upload limits. lib/element/api.server.ts; app/api/element/[action]/route.ts.

  25. [25]

    Element nose model: training, evaluation and inference. docs/element-nose-model.md; scripts/train-nose-model.py; lib/element/nose.ts; lib/element/nose-model.json.

  26. [26]

    Element source: note graph, tuning rules, bases and sensitivity ladder. lib/element/graph.ts; lib/element/tune.ts; lib/element/bases.ts; lib/element/ladder.ts.

  27. [27]

    Element source: explanation templates and validator. lib/element/explain.ts.

  28. [28]

    Element source: accounts, profiles and compare. lib/element/auth.server.ts; lib/element/compare.ts; app/element/styled-qr.tsx.

  29. [29]

    Element configuration: Cloudflare bindings and rate limits, database schema. wrangler.jsonc; migrations/0001-0005.

  30. [30]

    Reader measurements, 4 October 2026: scripts/photo_reader.py and the browser reader (dev page /element/demo/photo-reader-check) run on public/kit/test-photos (synthetic-board-test.png, blot-dark.webp) and scripts/kit-assets/test-photos (v12-clean-blot, v12-oily-blot, v12-no-blot).

  31. [31]

    Test run, 4 October 2026: npm test (node --test "lib/**/*.test.ts").

  32. [32]

    Element commit history: b40f05d (survey model budget after about 3.5 s live answers); cede9f6 (AI Gateway routing).

A perfumer reviews every formula before mixing.
Not a medical or diagnostic device.

Try the demo ↗