About

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.

6years of software
2years of AI in production
50+projects delivered
45+clients
20+systems in production
9people on the team
How I work

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.

What I cannot do

I would rather be filtered out for what is missing than be caught out in a technical interview.

What I use
  • AWS
  • Python, TypeScript
  • FastAPI, Next.js, React
  • BM25 and vector search, RRF
  • Step Functions, Lambda, S3
  • DynamoDB, PostgreSQL, Prisma
  • CDK
  • pytest, vitest
  • TensorFlow, MediaPipe
Never used it
  • Azure, Google Cloud
  • Databricks
  • Snowflake
  • Microsoft Fabric
  • Unity Catalog
  • MLflow
  • OCR and document extraction
  • Go
  • Formal Spec Driven Development
Every measurement

Eight figures, each with the conditions it was taken under and the source that backs it, or the one it is still missing.

Measured, source unpublished07
Measured, source unpublished: the evaluation run with jina-embeddings-v30.862What the vector half earns
Measured, source unpublished: tool definitions8MCP tools
Measured, source unpublished: OpenAPI schema13FastAPI routes
Measured, source unpublished: before and after logs707.5 msSnapshot in handler
Measured, source unpublished: Map state definition8Map concurrency
Measured, source unpublished: the cost calculation44 USDCloudWatch rejected
Measured, source unpublished: E2E suite output53ADM end to end
Unverified03
Unverified: the evaluation run2Gold set violations
Unverified: output of both suites392 + 77RAG tests
Unverified: validation notebook0.1711Gradient boosting
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 ↗
Where it breaks

Limits carry the same weight as results.

Measured, source unpublished: the evaluation run with the real embedderRAG system

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.

Measured, source unpublished: the state size calculationForecasting on AWS

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.

Measured, source unpublished: the full gold setRAG system

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.

Measured, source unpublished: code table and its handlingOrquestadorADM

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.

Unverified: nothing until it is verifiedDeployment package

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.