Building a Two-Way MITM Audit Ledger for AI Agents
The Mission: A Tamper-Proof System of Record
AI agents are unpredictable. When an agent interacts with a financial API like Alpaca.markets, you need more than just a “feeling” that it’s working—you need an immutable, two-way audit trail. I set out on a learning adventure to build a platform that securely encloses the agent and logs every single byte of data in both directions. I am incredibly proud to share that the enclosure is secure, and the ledger is live. It logs everything!

OpenResty Web Server?
To capture full request and response bodies at the network layer without slowing down the system, I chose OpenResty.
OpenResty is Nginx bundled with LuaJIT. A blazingly fast, lightweight, and highly embeddable compiled scripting language. It allows me to write high-performance scripts that run inside the Nginx event loop. By using the lua_need_request_body on; directive, I force the proxy to hold the entire JSON payload in memory. This allows my Lua scripts to “sniff” the data and write it to a structured JSON log before the request ever moves to the next hop. It is the perfect tool for high-concurrency financial logging.
The 4 Magic Points of the MITM Ledger
Achieving a 100% visible audit trail required four specific architectural successes.
1. The Secure Enclosure (Isolation)
In docker-compose.yml, the Alpaca MCP container is stripped of its direct internet access. It lives on an internal-only network. This ensures the agent cannot “leak” data or talk to Wall Street behind my back. Its only path to the world is through the Provost.
2. The Surgical Patch (Redirection)
The Alpaca Python SDK is hardcoded to connect to https://paper-api.alpaca.markets. Even in an enclosure, the SDK will try to reach that URL and fail if ti doesnt.
The Genius Code Change: I wrote an entrypoint.sh script that performs “brain surgery” on the container at startup. It finds the SDK’s source code and surgically rewrites the base URL functions to point to http://agent-provost:8081. The SDK now natively routes its traffic to my sniffer.
3. The Inbound Ledger (Intent)
On Port 8000, OpenResty intercepts the Intent (the LLM speaking to the MCP server). The proxy captures the JSON-RPC tool calls, telling me exactly what the AI thinks it is doing.
Here is the actual, unredacted log entry of the LLM asking for the account balance:
{
"time_local": "25/Mar/2026:18:18:48 +0000",
"remote_addr": "172.18.0.1",
"request": "POST /mcp HTTP/1.1",
"status": "200",
"body_bytes_sent": "1011",
"request_time": "0.381",
"upstream_response_time": "0.381",
"request_body": "{\"jsonrpc\": \"2.0\", \"method\": \"tools/call\", \"id\": 2, \"params\": {\"name\": \"get_account_info\", \"arguments\": {}}}",
"resp_body": "id: 2\nevent: message\ndata: {\"jsonrpc\": \"2.0\", \"id\": 2, \"result\": {\"content\": [{\"type\": \"text\", \"text\": \"{\\n \\\"id\\\": \\\"21111111-1111-1111-1111-111111111111\\\",\\n \\\"admin_configurations\\\": {},\\n \\\"user_configurations\\\": null,\\n \\\"account_number\\\": \\\"PA38GU7HYAWQ\\\",\\n \\\"status\\\": \\\"ACTIVE\\\",\\n \\\"crypto_status\\\": \\\"ACTIVE\\\",\\n \\\"options_approved_level\\\": 2,\\n \\\"options_trading_level\\\": 2,\\n \\\"currency\\\": \\\"USD\\\",\\n \\\"buying_power\\\": \\\"200000\\\",\\n \\\"regt_buying_power\\\": \\\"200000\\\",\\n \\\"daytrading_buying_power\\\": \\\"0\\\",\\n \\\"effective_buying_power\\\": \\\"200000\\\",\\n \\\"non_marginable_buying_power\\\": \\\"100000\\\",\\n \\\"bod_dtbp\\\": \\\"0\\\",\\n \\\"cash\\\": \\\"100000\\\",\\n \\\"accrued_fees\\\": \\\"0\\\",\\n \\\"portfolio_value\\\": \\\"100000\\\",\\n \\\"pattern_day_trader\\\": false,\\n \\\"trading_blocked\\\": false,\\n \\\"transfers_blocked\\\": false,\\n \\\"account_blocked\\\": false,\\n \\\"created_at\\\": \\\"2025-03-24T16:21:11.481111Z\\\",\\n \\\"trade_suspended_by_user\\\": false,\\n \\\"multiplier\\\": \\\"2\\\",\\n \\\"shorting_enabled\\\": true,\\n \\\"equity\\\": \\\"100000\\\",\\n \\\"last_equity\\\": \\\"100000\\\",\\n \\\"long_market_value\\\": \\\"0\\\",\\n \\\"short_market_value\\\": \\\"0\\\",\\n \\\"position_market_value\\\": \\\"0\\\",\\n \\\"initial_margin\\\": \\\"0\\\",\\n \\\"maintenance_margin\\\": \\\"0\\\",\\n \\\"last_maintenance_margin\\\": \\\"0\\\",\\n \\\"sma\\\": \\\"100000\\\",\\n \\\"daytrade_count\\\": 0,\\n \\\"balance_asof\\\": \\\"2025-03-24\\\",\\n \\\"crypto_tier\\\": 1,\\n \\\"intraday_adjustments\\\": \\\"0\\\",\\n \\\"pending_reg_taf_fees\\\": \\\"0\\\"\\n}\"}], \"isError\": false}}\n\n"
}
4. The Outbound Ledger (Execution)
On Port 8081, OpenResty intercepts the Execution (the MCP server speaking to Wall Street). It receives the plain HTTP request from my patched SDK, logs the full body, wraps it in a secure SSL/TLS blanket, and forwards it to the real Alpaca API.
Here is the exact corresponding log entry showing the MCP server fetching the real data from Wall Street:
{
"time_local": "25/Mar/2026:18:18:48 +0000",
"remote_addr": "172.18.0.3",
"request": "GET /v2/account HTTP/1.1",
"status": "200",
"body_bytes_sent": "861",
"request_time": "0.374",
"upstream_response_time": "0.374",
"request_body": "",
"resp_body": "{\"id\":\"21111111-1111-1111-1111-111111111111\",\"admin_configurations\":{},\"user_configurations\":null,\"account_number\":\"PA38GU7HYAWQ\",\"status\":\"ACTIVE\",\"crypto_status\":\"ACTIVE\",\"options_approved_level\":2,\"options_trading_level\":2,\"currency\":\"USD\",\"buying_power\":\"200000\",\"regt_buying_power\":\"200000\",\"daytrading_buying_power\":\"0\",\"effective_buying_power\":\"200000\",\"non_marginable_buying_power\":\"100000\",\"bod_dtbp\":\"0\",\"cash\":\"100000\",\"accrued_fees\":\"0\",\"portfolio_value\":\"100000\",\"pattern_day_trader\":false,\"trading_blocked\":false,\"transfers_blocked\":false,\"account_blocked\":false,\"created_at\":\"2025-03-24T16:21:11.481111Z\",\"trade_suspended_by_user\":false,\"multiplier\":\"2\",\"shorting_enabled\":true,\"equity\":\"100000\",\"last_equity\":\"100000\",\"long_market_value\":\"0\",\"short_market_value\":\"0\",\"position_market_value\":\"0\",\"initial_margin\":\"0\",\"maintenance_margin\":\"0\",\"last_maintenance_margin\":\"0\",\"sma\":\"100000\",\"daytrade_count\":0,\"balance_asof\":\"2025-03-24\",\"crypto_tier\":1,\"intraday_adjustments\":\"0\",\"pending_reg_taf_fees\":\"0\"}"
}
The Alpaca Case Study: Re-Mapping the SDK
Most developers struggle with SSL certificate errors when trying to proxy HTTPS traffic. By surgically patching the Python SDK to use http locally, I bypassed the need for fake certificates and complex kernel routing.
I didn’t fight the SDK; I re-mapped its internal world. This ensures that the high-level “Trading API” logic remains intact while the low-level “Network” logic is governed by the Provost.
Pondering the Universal MCP Pattern
The success of this prototype proves a universal pattern for any MCP server in a container: Enclose -> Patch -> Proxy.
- Enclose: Use Docker networks to isolate the container.
- Patch: Use an entrypoint script to find and replace the API URLs inside the container’s code.
- Proxy: Use OpenResty to provide the “Smart Pipe” that handles the logging.
By standardizing this “Sidecar” architecture, we can turn any black-box AI agent into a transparent, governed financial tool. I can now prove exactly what the AI intended to do and exactly what the exchange actually executed.
The Agent Provost Project demonstrates how to build a transparent, auditable AI agent stack for research and production. By logging every tool call and HTTP body, we gain deep insight into agent behavior, system health, and user experience. This approach is recommended for any AI system where trust, reproducibility, and auditability are priorities.
Get the Code
You can clone the full Agent Provost Project from GitHub: https://github.com/CharmingSteve/agent-provost
All logs shown are real, unredacted entries from a live session. For more details, see the project README or the full logs in the repository.
