Innkeeper
An autopilot night auditor for small hotels. Every night at 2 AM it reconciles the property-management ledger against the card processor's settlements and the OTA's PDF statement, adjudicates every mismatch with cited evidence and competing hypotheses, auto-clears the confident ones inside an expected-loss policy gate, and queues only the material discrepancies for the owner's coffee — then signs the night's books cryptographically.
A husband-and-wife inn runs 14 rooms; every night one of them stays up past midnight squinting at three statements that disagree by $6.67 — because the last time they let it slide, it was $2,300 by month-end.
Innkeeper is a CLI + MCP servers, not a hosted web app — but the audit engine is deployed live on Alibaba Function Compute (
/health·/verify·/run, offline, zero keys — see ☁️ Deployed). Everything above also runs locally, offline, with zero API keys; seeDEMO.mdfor the exact copy-paste script.
⚠️ MOCK SYSTEMS — read this first
The three source systems (PMS, card processor, OTA) are mocks, shipped in this repo as three MCP-compatible servers (
mcp/) that read from a committed, deterministically seeded 14-room month. This is the honest path to realism: the data is a coherent month with planted, ground-truth-labeled discrepancy archetypes and real reportlab-rendered OTA PDFs.Which transport runs, precisely: the offline demo and every test run on
FakeQwen— a deterministic stand-in that computes each verdict from the mismatch's own arithmetic and memos (the same signalsqwen3.7-maxreasons over) and parses the committed statement sidecar; it makes no VL call and never reads the ground-truth labels. The realqwen3-vl-plustwo-pass read of the 8-pt PDF andqwen3.7-maxadjudication run only under--livewith aDASHSCOPE_API_KEY— a real, key-gated path you can run yourself (see the Run the real Qwen path (--live) section below); the reportlab PDFs exist so that path has a genuine document to read.No live PMS/processor/OTA is contacted, and Innkeeper is decision support with signed evidence, not an accounting system of record. The graded core runs offline with zero API keys.
🚀 Quickstart (offline, no keys)
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
innkeeper seed --nights 30 # deterministic month + rendered OTA statement PDFs
innkeeper run --night 2026-07-04 # fetch → extract → match → adjudicate → gate → signed close
innkeeper replay --night 2026-07-04 # re-derive byte-identical, zero keys (invariant I4)
innkeeper verify-chain # recompute every root, check every signature
innkeeper bench # 277/281 auto-cleared · 0 false clears · accuracy 0.9964
pytest -q # 404 passedThe full demo script is in DEMO.md.
☁️ Deployed on Alibaba Function Compute
Innkeeper is deployed live on Alibaba Function Compute (FC 3.0, managed
python3.10 runtime — no container, no ACR) at
https://innkeeper-temfmzpqug.ap-southeast-1.fcapp.run. Every endpoint runs
offline on the committed fixtures/ + ledger/ — zero keys, no network, no
vision model — so a judge can verify the signed books straight from a browser:
Endpoint | What it does |
liveness | |
re-verifies the 30 signed closes in the cloud — roots, Ed25519 signatures, evidence sha256 bindings, a byte-identical replay of 2026-07-04, and a one-byte tamper caught (I2 / I3 / I4) | |
reproduces one deterministic FakeQwen night audit — 39 txns, 12 mismatches, 11 auto-cleared, 1 queued, Merkle root |
The live qwen3-vl-plus / qwen3.7-max path is wired and verified with a real
DashScope smoke call, and stays key-gated behind --live (next section); the
deployed endpoints are the offline-deterministic engine, byte-for-byte
replayable. Full request/response transcript:
docs/proof/DEPLOY_PROOF.md. The 02:00 timer
trigger is configured in infra/fc/s.yaml; a captured
console recording of the cron firing on its own is the one remaining artifact.
🔑 Run the real Qwen path (--live)
The offline demo above proves the whole pipeline with zero keys. To exercise
the actual Qwen Cloud models on the real reportlab-rendered statement — the one
step this project can't fake — add your key and the live extra:
pip install -e ".[live]" # openai + pypdfium2 rasterizer
export DASHSCOPE_API_KEY=sk-… # dashscope.console.aliyun.com/apiKey
innkeeper run --night 2026-07-04 --live # SAME pipeline, real modelsUnder --live the extractor is LiveQwen: it rasterizes the committed 8-pt
OTA PDF and calls qwen3-vl-plus twice (temperatures 0.0 / 0.4) — agreement
becomes confidence, disagreement escalates (I5) — and the mismatch residue is
adjudicated by qwen3.7-max + thinking returning typed JSON the gate
computes over. The evidence citations are bound to the on-disk sha256 hashes
either way, so a live verdict can never cite a hash that doesn't resolve. This
is the single key-gated path; nothing else needs a network.
🏗️ The pipeline
flowchart LR
TMR["Function Compute<br/>(deployed · 02:00 timer configured)"] --> RUN["audit run"]
RUN -->|"MCP tools"| M["3 mock servers — PMS · processor · OTA-PDF"]
M --> EX["extract: qwen3-vl-plus two-pass + bbox (I5)"]
RUN --> MT["deterministic 3-tier matcher<br/>clears ~77% — zero model calls"]
EX --> MT
MT -->|"mismatch residue"| ADJ["adjudicate: qwen3.7-max + thinking → typed Verdict"]
ADJ <--> T{{"Qwen transport"}}
T --> FQ["FakeQwen — default, no key"]
T --> LQ["LiveQwen — qwen3.7-max · qwen3-vl-plus"]
ADJ --> G["E-loss gate: conf ≥ .85 ∧ ≤ $50 ∧ ≠ true_error"]
G --> CL["signed Merkle night-close → prev_root chain (I1–I4)"]As built = the three source systems are in-repo mocks (disclosed above); everything is green on FakeQwen, keyless. Live qwen3-vl-plus / qwen3.7-max sit behind DASHSCOPE_API_KEY; the app is deployed live on Function Compute (managed python3.10 — /health · /verify · /run, offline) and the 02:00 timer trigger is configured in infra/fc/s.yaml. Plain-text view below.
fetch (3× MCP) → extract (qwen3-vl-plus, two-pass + bbox) → deterministic match
→ adjudicate the residue (qwen3.7-max + thinking) → E[loss] policy gate
→ signed Merkle night-close → chainA deterministic three-tier matcher (ref_exact → fuzzy-ref → amount+date) clears
~77% of transactions with zero model calls. Only the mismatch residue reaches
the language model. Every verdict is a typed Verdict:
a classification, evidence citations from ≥2 systems (each bound by sha256),
competing hypotheses, and a confidence — so the gate is math over typed fields,
not vibes.
The policy gate is expected-loss math, not a button:
auto_clear ⟺ confidence ≥ 0.85 ∧ materiality ≤ $50 ∧ class ≠ true_error
generalised: E[loss] = amount × (1 − confidence) ≤ τTwo hard constraints can only ever queue: a true_error classification (I1)
and a two-pass extraction disagreement (I5).
🔒 Invariants (tested, not promised)
Invariant | Test | |
I1 | Zero false auto-clears on the planted true errors across all 30 nights — the load-bearing one |
|
I2 | Every verdict cites ≥2 systems with resolvable sha256 hashes |
|
I3 | The signed close chain verifies; a one-byte tamper (verdict / evidence / root) fails |
|
I4 |
|
|
I5 | A two-pass extraction disagreement always escalates, never averages |
|
📈 Benchmark
Seeded month, 30 nights, 1199 transactions, FakeQwen (deterministic, offline).
Regenerate with python scripts/bench.py → docs/BENCH.md.
metric | value | target |
classification accuracy | 0.9964 | ≥ 0.92 |
HITL action accuracy | 1.0000 | — |
false auto-clears on true errors | 0 | 0 (invariant) |
auto-cleared / mismatches | 277/281 (98.58%) | — |
queue precision / recall | 1.00 / 1.00 | — |
residue fraction (LLM-touched) | 23.44% | small |
runtime (30 nights, offline) | 0.16s | < 5 min/night |
modelled cost / night | $0.0327 | ~$0.15 |
The single class disagreement is the page-broken row correctly declining to
classify (unknown) while still queueing — the right call under I5. Across the
whole τ-sweep the false-clear column stays 0: the true-error and
escalation constraints hard-queue at every threshold, so the risk knob trades
automation against review load without ever touching the safety floor.
🧩 Why only Qwen Cloud
Qwen surface | What it does here | Without it |
| reads the 8-pt OTA statement table, bbox-cited per figure | a document-AI vendor bill + losing the evidence-highlight |
two-pass VL agreement | agreement = confidence, disagreement = escalation (I5) | single-pass misreads corrupt money decisions silently |
| ranks competing hypotheses over evidence | plus-tier collapses to the first plausible story |
structured output | the E[loss] gate has typed fields to compute over | free-text verdicts can't be gated, benched, or replayed |
function calling / 3× MCP | typed, logged tool calls = the replayable decision log | bespoke connectors; forfeits the rubric's named example |
Remove Qwen Cloud and a 14-room inn needs a document-AI vendor, a frontier LLM, and a tool-orchestration framework — three bills it will never pay, and no single-vendor trail to sign into the books.
🗂️ Layout
src/innkeeper_audit/ amounts · config · schemas · crypto · matcher · policy
pipeline · report · verify · benchmark · cli
qwen/ fake (offline) · live (DashScope) · base
mcp/ server (JSON-RPC) · tools · pms/processor/ota
mcp/ pms_server.py · processor_server.py · ota_server.py (stdio)
scripts/ bench.py · verify_offline.py · check_submission_readiness.py
infra/fc/ s.yaml · wsgi.py (deployed HTTP handler) · audit_handler.py · PROOF.md
tests/ 404 tests
docs/ BENCH.md · friction-log.md · proof/DEPLOY_PROOF.md✅ Testing & CI
5-stage pipeline: Quality → Security → Build → Offline Proof → Deploy Gate.
# ── Code Quality ────────────────────────────
ruff check . # lint
mypy src # type check (advisory on qwen/live.py)
pytest --cov=innkeeper_audit --cov-report=term # 404 tests, 100% coverage
# ── Offline Proof (the invariants, live) ────
innkeeper seed --nights 30
innkeeper run --night 2026-07-04
innkeeper replay --night 2026-07-04 # I4: byte-identical re-derivation
innkeeper verify-chain # I3: every root + signature checked
python scripts/verify_offline.py # socket-guarded tamper proof
innkeeper bench # I1: 0 false auto-clears, always
# ── Security ────────────────────────────────
pip-audit # dependency vulnerability scanLayer | Tool | Status |
Code Quality | ruff + mypy | ✅ |
Unit Testing | pytest, 404 tests, 100% coverage | ✅ |
Invariant Testing | I1–I5, parametrised × 30 nights | ✅ |
Security (SAST) | CodeQL ( | ✅ |
Security (SCA) | Dependabot ( | ✅ |
Secret Scanning | TruffleHog | ✅ |
Build Verification | sdist/wheel + CLI entrypoint smoke test | ✅ |
CI runs on every push/PR to main — see .github/workflows/ci.yml.
📋 Status — honest
✅ Offline core is complete and green: seed, matcher, adjudication, gate, crypto, MCP, CLI,
bench,verify_offline, 404 passing tests.🔑 Live Qwen path is wired but behind
DASHSCOPE_API_KEY(--live):qwen3-vl-plustwo-pass extraction andqwen3.7-maxadjudication via the DashScope OpenAI-compatible endpoint. Not exercised in the offline suite by design; the deterministicFakeQwencomputes verdicts from the same signals.☁️ Deployed live on Alibaba Function Compute (managed
python3.10) —/health·/verify·/runrun offline on the committed ledger (/verifyre-verifies the signed-close chain in the cloud;/runreproduces a night audit). Transcript indocs/proof/DEPLOY_PROOF.md. The02:00timer trigger is configured (infra/fc/s.yaml); a captured console recording of the cron firing on its own is the one remaining step.○
report.htmlis optional (single-page render viainnkeeper report --night N --html); the primary report is Markdown. No Next.js UI shipped — the CLI + signed ledger are the product surface.
Built solo in a five-project sprint. The night auditor finally sleeps.
🤝 Contributing & Security
Issue/PR templates, Code of Conduct, and a security policy live under
.github/ — see CONTRIBUTING.md
and SECURITY.md.
📄 License
MIT © 2026 Edy Cu
🏷️ Versioning
This project uses Semantic Versioning with fully automated version management driven by Conventional Commits — the version is never edited by hand.
Commit type | Bump | Example |
| patch | 1.0.0 → 1.0.1 |
| minor | 1.0.0 → 1.1.0 |
| major | 1.0.0 → 2.0.0 |
python-semantic-release keeps the version in sync
across pyproject.toml and src/innkeeper_audit/__init__.py.
In CI/CD: Stage 6 of the pipeline (
.github/workflows/ci.yml) runs on every push tomain, computes the next version from the commits since the last tag, then commits + tags it automatically.Locally:
pip install -e ".[release]" semantic-release version # compute + apply the next version and tag
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/edycutjong/innkeeper'
If you have feedback or need assistance with the MCP directory API, please join our Discord server