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
Why
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
Why
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
Why
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
Why
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.
Each sounding's condition
- What the vector half earns
- 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 ↗
- Gold set violations
- 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 tools
- MCP server in production for the RAG system ↗
- FastAPI routes
- The same surface, exposed over HTTP ↗
- RAG tests
- 392 in pytest, 77 in vitestcoverage missing ↗
- Gradient boosting
- Same walk-forward validation. Lost to AR(1)name the metric ↗
- Snapshot in handler
- Before moving it out. After: 1.78 ms ↗
- Map concurrency
- The account quota was 10 ↗
- CloudWatch rejected
- Per month at that cardinality. DynamoDB chosen instead ↗
- ADM end to end
- 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
Where and why
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
Where and why
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
Where and why
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
Where and why
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
Where and why
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.