I design the architecture, build it, and publish how I measured it.
I build systems where a failure costs money or costs a fine: electronic invoicing to the Peruvian tax authority, payment gateways, accounting ledgers and reconciliation. On that base I add AI where the improvement is measurable, and I publish the conditions each figure was measured under.
I design and build systems where a failure carries fiscal or legal consequence: electronic invoicing, payment gateways, ledgers and reconciliation.
I work from Lima and lead the technical team of a nine-person agency.
Every figure on this page carries the conditions it was measured under and a link to its source.
Four rules I do not negotiate
Measure before claiming
Every figure I publish carries the conditions it was measured under. A recall@10 without naming the gold set is not data, it is an opinion.
Document where it breaks
Every system has a failure point. The forecasting one is at 510 products, and it is calculated, not guessed. I would rather say it than have a client find it.
Leave my mistakes written down
I estimated 25 seconds and measured 39.17. I bet on gradient boosting and AR(1) won. The gap between estimating and measuring is the work.
Choose on cost, not fashion
I dropped CloudWatch because at that cardinality it cost 44 USD a month. Concurrency is 8 because the account quota was 10, not because 8 was optimal.
I would rather be filtered out for what is missing than be caught out in a technical interview.
- AWS
- Python, TypeScript
- FastAPI, Next.js, React
- BM25 and vector search, RRF
- Step Functions, Lambda, S3
- DynamoDB, PostgreSQL, Prisma
- CDK
- pytest, vitest
- TensorFlow, MediaPipe
- Azure, Google Cloud
- Databricks
- Snowflake
- Microsoft Fabric
- Unity Catalog
- MLflow
- OCR and document extraction
- Go
- Formal Spec Driven Development
Eight figures, each with the conditions it was taken under and the source that backs it, or the one it is still missing.
Recall of the search isolated from the deterministic floor, with the real embedder. The toy embedder gave 0.759: that difference is the only thing proving the semantic half is not decoration ↗
With the real embedder. Asking for papaya, which is not in the catalogue, returns potato records: 0.6444 against 0.6656, two hundredths apart. No threshold separates them without killing the aggregate questionsunsolved in retrieval ↗
MCP server in production for the RAG system ↗
The same surface, exposed over HTTP ↗
435 in pytest, 77 in vitestcoverage missing ↗
Same walk-forward validation. Lost to AR(1)name the metric ↗
Before moving it out. After: 1.78 ms ↗
The account quota was 10 ↗
Per month at that cardinality. DynamoDB chosen instead ↗
Over 543 tests and 29 domain services ↗
Limits carry the same weight as results.
Papaya returns potato, and I did not fix it in retrieval
Asking for a product that is not in the catalogue returns the closest one by name. Papaya scores 0.6444 and potato 0.6656: two hundredths apart. I tried the obvious threshold and it does not work, because any cut that separates them kills the aggregate questions, which score 0.5151. I solved it by warning the model from the lexical filter, which already knew no word matched the catalogue. The evaluation still reports both violations: I did not relax the gold set. ↗
It breaks at 510 products
514 bytes per Map element against the 256 KB Step Functions state ceiling. Past that number the Map has to be paginated or the payload moved to S3. ↗
recall@10 holds inside the indexed domain
The gold set is 32 cases I wrote myself over 73 products and 537 dates. It is a small, single-author sample: it catches regressions, it does not prove the system generalises. ↗
SUNAT between 100 and 1999 is inconclusive
There is no way to know whether the document was registered. The rule is not to reissue and to wait for the status query, because reissuing duplicates the counter. ↗
The deployment package, unpublished
207.6 MB against a 250 MB limit. Held back until I confirm whether that figure is the zipped or the unzipped package, because the Lambda limit applies to the unzipped one. It gets corrected or it leaves the list, but it does not get published half-right. unverified ↗