# booklint > Deterministic spending guardrails for agents. Set limits; booklint checks every action against them and answers FLAGGED, CLEAN or NOT_VERIFIED, naming each limit an action breaks. Hotel bookings, purchases, subscriptions, invoices, quotes, budgets and data quality. Trading books against rulebooks too. 8 MCP tools. Free key in one call, no card. ## Try it now 1. Sample result, no key, never counted against the free caps: curl -sS -X POST https://booklint.com/v1/check -H 'content-type: application/json' -d '{"sample":true}' It runs every check on the public sample book and answers FLAGGED with 2 flags: a stock position on the wrong side of the broker and a journal entry with no broker fill. 2. Free key (no card, no e-mail; shown once): curl -sS -X POST https://booklint.com/v1/keys -H 'content-type: application/json' -d '{"plan":"free"}' 3. Check your own book with the key (edit the example book from GET https://booklint.com/v1/sample?name=trading): curl -sS -X POST https://booklint.com/v1/check -H "authorization: Bearer YOUR_KEY" -H 'content-type: application/json' -d @book.json 4. Any agent's actions (sample 2, no key, never counted against the free caps): curl -sS -X POST https://booklint.com/v1/check -H 'content-type: application/json' -d '{"sample":"spend"}' The spend sample, a made-up purchasing and booking agent's day, answers FLAGGED with 3 flags: a 640 USD hotel booking above the 600 USD per-action cap, a UTC day totalling 1620 USD against the 1500 USD daily cap, and a rail booking that is not refundable. Your own: POST https://booklint.com/v1/check with {"actions": [{"id", "time" (ISO 8601, UTC day boundaries), "amount", "currency", "merchant", "refundable", "kind"}], "limits": {"version", "declared", "rules"}}; edit GET https://booklint.com/v1/sample?name=spend. Rule kinds: per_transaction_cap and daily_spend_cap (free), merchant_allowlist, merchant_denylist and refundable_required (Desk). A missing amount, currency, merchant or refundable is reported as not checked, never assumed; another currency is not converted. 5. Desk plan: on the free plan, rules outside the plan report not_in_plan and the result carries an "upgrade" object (price, the rules Desk runs on this book, available true or false, and the existing route POST https://booklint.com/v1/keys {"plan":"desk"}). Call that route only when upgrade.available is true. ## Fine-Print Check and Terms & fee card (before your agent books or subscribes) - Samples 3 and 4, no key, never counted against the free caps: curl -sS -X POST https://booklint.com/v1/check -H 'content-type: application/json' -d '{"sample":"booking"}' Sample 3, a made-up hotel offer, answers FLAGGED with 3 flags: STOP, mandatory line items total 636.6 USD against a 600 USD cap; WARN, a resort fee and a tax put it 17.89% over the 540 USD headline price; STOP, free cancellation ends 20 hours after as_of against a 48-hour minimum. Sample 4 ("subscription") answers FLAGGED with 4 flags: it renews on its own, at 29.99 USD against a 15 USD cap, after a 3-day trial against a 7-day minimum, and its page text says non-refundable where the offer states refundable true. - Your own: POST https://booklint.com/v1/check with {"offer": {"merchant", "currency", "headline_price", "line_items": [{"label", "amount", "mandatory"}], "refund": {"refundable", "free_cancel_until"}, "subscription": {"auto_renews", "renewal_price", "renewal_interval", "trial_ends"}}, "limits": {...}, "as_of": "", "page_text": ""}; edit GET https://booklint.com/v1/sample?name=booking or ?name=subscription. Send plain text you already have. Do not send a URL; booklint will not fetch one. booklint never fetches a URL. Each rule answers MATCH, WARN, STOP or NOT_CHECKED with the figure found and the field it came from. Rule kinds: total_price_cap and fees_above_headline (free), refundable_required, free_cancellation_min_hours, no_auto_renew, renewal_price_cap, trial_end_min_days and page_text_consistency (Desk); page_text_consistency answers WARN at most, never STOP. Time rules use your as_of, never the server clock. Facts only: it does not judge whether terms are fair. ## Use it from code Each example connects to https://booklint.com/mcp with no key and asks the agent to run the booking sample, which returns real STOP results and is never counted against the free caps. Without a key, an agent gets 5 own checks per client address per UTC day. A free key (get_free_key, no card) gives 20 a day; send it as Authorization: Bearer vdk_.... - LangChain (pip install langchain langchain-anthropic langchain-mcp-adapters): import asyncio from langchain.agents import create_agent from langchain_mcp_adapters.client import MultiServerMCPClient async def main(): client = MultiServerMCPClient( {"booklint": {"transport": "http", "url": "https://booklint.com/mcp"}} ) tools = await client.get_tools() agent = create_agent("anthropic:claude-opus-5-5", tools) result = await agent.ainvoke( {"messages": "Call check_book with sample='booking' and list every STOP result."} ) print(result["messages"][-1].content) asyncio.run(main()) - Anthropic SDK, MCP connector (pip install anthropic): import anthropic client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5-5", max_tokens=4096, betas=["mcp-client-2025-11-20"], mcp_servers=[{"type": "url", "url": "https://booklint.com/mcp", "name": "booklint"}], tools=[{"type": "mcp_toolset", "mcp_server_name": "booklint"}], messages=[{"role": "user", "content": "Call check_book with sample='booking' and list every STOP result."}], ) for block in response.content: if block.type == "text": print(block.text) The connector calls booklint from Anthropic's servers, so keyless calls share one client address with other connector users. For your own cap, put a free key in "authorization_token" on the mcp_servers entry. - CrewAI (pip install 'crewai[anthropic]' 'crewai-tools[mcp]'): from crewai import LLM, Agent, Crew, Task agent = Agent( llm=LLM(model="anthropic/claude-opus-5-5", max_tokens=4096), role="Booking checker", goal="Check every booking against the owner's limits before it is confirmed", backstory="Calls booklint before confirming a booking, spend or subscription.", mcps=["https://booklint.com/mcp"], ) task = Task( description="Call check_book with sample='booking' and list every STOP result.", expected_output="Each STOP result with its rule and the figure found.", agent=agent, ) print(Crew(agents=[agent], tasks=[task]).kickoff()) ## Machine surfaces - [OpenAPI 3.1](https://booklint.com/openapi.json): every public route - [MCP](https://booklint.com/mcp): POST, Streamable HTTP, JSON-RPC 2.0, stateless; tools get_sample (optional name: booking, subscription, trading or spend; runs that sample and returns the issues found plus the next step, run check_book on your data; start with booking, which finds real STOP results), check_book (a book, an actions list or an offer; the same checks and free caps as POST /v1/check; without api_key the keyless free plan applies; with no arguments it runs the booking sample), the finance tools check_invoice, check_quote_math, check_budget_variance, check_accounts_tieout and check_data_quality, and get_free_key (a free key, the same as POST /v1/keys {"plan":"free"}). Every check_* tool runs its own sample with sample=true or with no arguments, never counted against the free caps; a sample result ends with "next", the fields to send for your own data - [Example book](https://booklint.com/v1/sample?name=trading): edit and POST to /v1/check - [Example actions](https://booklint.com/v1/sample): edit and POST to /v1/check - [Example offers](https://booklint.com/v1/sample?name=booking) and (https://booklint.com/v1/sample?name=subscription): edit and POST to /v1/check - [Plans](https://booklint.com/v1/plans) ## Policies - [Terms](https://booklint.com/terms) - [Privacy](https://booklint.com/privacy) ## Notes - Send the key as Authorization: Bearer vdk_... on POST https://booklint.com/v1/check, or as api_key in the MCP check_book tool. - Free use is capped: 20 own-book checks a day and 200 in all per free key, 5 a day without a key; the sample is never counted against the caps; over a cap the answer is 429 or 402 with the upgrade path. - Every refusal, on POST /v1/check, POST /v1/keys and the MCP tools, carries a "next" object: the missing field names, the tool or route to call next (for example get_free_key) and, at a daily cap, resets_at (the next 00:00 UTC). - Every response starts with verdict (FLAGGED, CLEAN, NOT_VERIFIED, REFUSED), coverage and coverage_complete. - A zero-flag result with verdict NOT_VERIFIED is not a pass, and a response with coverage_complete false is not full coverage under any verdict; read coverage.not_ran before the flag count. - GET /terms is the Terms of Service. GET /privacy is the Privacy Notice. - Flags test the customer's stated rules only and are never a trade instruction. The operator does not give advice, does not execute and holds no discretion. - Raw books are processed and discarded; the customer book is not kept as a stored book. - Run it locally: the free rule set as an MIT Python package (booklint check, booklint mcp), offline, no key, no caps: https://github.com/booklint/booklint/releases/latest