Writing Prompts Like Type Signatures: SEO Master Agent Turns the LLM Into a "Typed Function"

SEO Master Agent enforces fixed-key JSON from the LLM via "prompts as type signatures," turning model calls into dependable functions.
The article analyzes the core design of the open-source project SEO Master Agent: writing prompts as type signatures, using get_system_prompt() to force each SEO workflow (onpage, offpage, gmb, content, geo) to return pure JSON with fixed keys, combined with input validation (missing input returns 422) to form a dual contract, giving LLM calls the dependability of functions. The project uses LiteLLM prefix routing to support OpenAI, Anthropic, Gemini, GLM, and Ollama, with single-request keys taking priority and never persisted, and the README candidly explains the limitations of key storage. The task chain is an async queue plus a state machine, the fetcher has SSRF protection, and authentication is fail-closed. The smoke test bootstraps uvicorn and asserts the interface contract in a closed environment stripped of keys. The author also proactively flags two incomplete items: missing rank tracking and GMB generating but not publishing.
The output of large language models is inherently free-form text, which is both its flexibility and the biggest obstacle to engineering it into production. The GitHub project SEO Master Agent solves this with a simple yet critical design: treat prompts as type signatures, forcing the model to return a pure JSON object with fixed keys.

The Problem: When the LLM Is Still a "Chatbot"
Most LLM applications output a chunk of text. That's fine for chat scenarios, but in systems that need to pass data downstream, text is a burden—you have to write parsers, error handling, and fallbacks. The designer of SEO Master Agent realized that as long as prompts consistently output structured data, the downstream code and frontend can render each workflow's output as real UI components, rather than dumping a conversation transcript onto the screen.
The project runs five SEO workflows: on-page optimization (onpage), off-page link building (offpage), Google Business Profile copy (gmb), content repurposing (content), and local GEO optimization (geo). It uses a multi-LLM gateway to support OpenAI, Anthropic, Gemini, GLM, and Ollama simultaneously, with a FastAPI backend, a Next.js frontend, and asynchronous task execution.
Core Design: Prompts as Type Signatures
The key code lives in get_system_prompt(). Every task type is forced to return only a raw JSON object with fixed keys, with no markdown code fences allowed. In the author's words, this is "writing prompts as type signatures."
The code defines it with extreme clarity:
TASK_TYPES = ("onpage", "offpage", "gmb", "content", "geo")
REQUIRED_INPUTS = {
"onpage": ("url", "keyword"),
"offpage": ("niche",),
"gmb": ("business_name", "services"),
"content": ("blog_url",),
"geo": ("city", "service"),
}
Missing inputs return a 422 directly, rather than letting the model guess. Each task's system prompt then pins down the output shape. For example, the geo prompt requires returning geo_title, geo_meta_description, local_landmarks_to_mention, localized_body_copy; onpage requires optimized_title, optimized_meta_description, h1_suggestion, keyword_density_analysis, content_recommendations.
The insight here isn't just format constraints. When input validation and output contracts are both hard-coded at the prompt layer, the LLM call becomes a genuine function: inputs have preconditions, outputs have types, and the caller can rely on it. This is the dividing line between LLM applications moving from "demo" to "system."
On JSON Schema and Structured Output
Writing prompts as type signatures is essentially using natural language to simulate the constraint capabilities of JSON Schema. JSON Schema is a specification described in JSON for validating the structure and types of JSON documents; it can declare which properties an object must contain, what types those properties are, and which are required. In LLM engineering, OpenAI's Structured Outputs, Anthropic's tool use, and the response_format parameter supported by some frameworks all use JSON Schema as the contract input. For compatibility with multiple models (whose support for GLM, Ollama, etc. varies), SEO Master Agent chose to hard-code key names in the system prompt rather than rely on each vendor's Schema feature. The trade-off of this path is: better generality and portability, but no server-side strict validation, so the model may occasionally add fields or wrap code fences—meaning the caller side needs to retain a layer of lenient parsing and retry fallback.
Multi-LLM Routing and Honest Key Handling
The resolve_llm_config() function handles model selection logic: keys in the request header take priority, with environment variables as a fallback; keys from a single request are used only in memory and never persisted. Routing itself is based on the model name prefix, dispatching via LiteLLM to gpt, claude, gemini, zhipu/glm, or ollama.
Worth mentioning separately is the project's candor in its documentation. The README states plainly that POST /api/settings/keys stores keys in a local SQLite, that this is just a self-hosted convenience feature rather than a key manager, and that environment variables are recommended in production. This practice of writing limitations out in the open is rarer in open-source projects than one might imagine.
LiteLLM and Model-Prefix Routing
LiteLLM is a Python library that unifies the APIs of hundreds of model providers into an OpenAI-compatible call format. By inputting a model name (such as claude-3-5-sonnet, gemini-1.5-pro, ollama/llama3), it automatically dispatches to the corresponding SDK or HTTP endpoint, and uniformly handles cross-cutting concerns like retries, timeouts, and usage statistics. SEO Master Agent's routing strategy leverages exactly this naming convention: parse the model name prefix, determine which provider to use, then hand it off to LiteLLM to make the actual call. This way, business code doesn't need branches for each vendor's auth headers or request body formats, and adding a new model only requires confirming the prefix mapping. A pitfall to note: different models vary in their adherence to the system prompt, JSON faithfulness, and max_tokens semantics, so output contract stability must be re-validated when switching models.
Task Lifecycle and Security Boundaries
A single call goes through the full chain: POST /api/execute validates the parameters and enqueues the task, returning 202 and a task_id; a background worker fetches the site (needed for onpage and content tasks), builds the task prompt, and calls the LLM; ExecutionLog status moves from pending to running, then to success or error; GET /api/tasks/{task_id} returns the structured result.
There are several security implementations worth noting: the fetcher has SSRF protection; X-API-Key authentication is fail-closed—without a SEO_API_KEY the server simply refuses to start; errors return structured JSON without exposing stack traces.
SSRF Protection and Fail-Closed Authentication
SSRF (Server-Side Request Forgery) refers to an attacker making the server initiate requests to access internal network addresses or cloud metadata endpoints (such as 169.254.169.254), thereby bypassing the network boundary. Since the onpage and content tasks need to fetch user-provided URLs, the fetcher must include protections: resolve the target domain, reject private network ranges and loopback addresses, and limit the number of redirect hops and protocols. Fail-closed, meanwhile, is a design orientation for authentication—when SEO_API_KEY is not configured, the server simply refuses to start, rather than allowing all requests by default. Its opposite, fail-open, seems convenient for development but easily leaves the interface exposed to the public internet when configuration is overlooked. Together, the two form the most basic security bottom line for this type of self-hosted tool.
The Smoke Test Starts the Server Itself
The design of tests/smoke_test.py is quite unusual. It spins up uvicorn itself on port 18099, runs in a closed environment stripped of inherited LLM keys, waits for /health to become ready, and then asserts the contract one by one: no key returns 401, an invalid task type, input, or URL returns 422, a valid request gets a task_id, the task is queryable, and errors are structured. Run python tests/smoke_test.py, and an exit code of 0 means it passed—no manual environment setup needed.
Fully isolating and self-bootstrapping the test environment means anyone can verify the interface contract on their own machine, rather than just trusting the README's description.
Getting It Running
cp .env.example .env # Set SEO_API_KEY to a generated key
pip install -r backend/requirements.txt
uvicorn backend.server:app --port 8000
# Trigger a task:
curl -X POST localhost:8000/api/execute -H "X-API-Key: $SEO_API_KEY" \
-d '{"task_type":"geo","inputs":{"city":"Chennai","service":"Dentist"},"model_id":"gpt-4o"}'
Two Honest Omissions
In the code review, the author generously lists two points: the "rank tracking" mentioned in the repository description currently has no corresponding workflow; the GMB task only generates copy and does not actually publish to Google. Both are written out rather than hidden.
For developers who want to build LLM applications, the most worthwhile thing to learn from this project isn't the SEO functionality itself, but the idea of "prompts as contracts"—define the input and output clearly first, then let the model fill in the blanks, rather than letting the model improvise freely first and then figuring out how to catch it. The workflows are runnable and self-hostable, and it's worth a look.
One-line Summary
SEO Master Agent uses "prompts as type signatures" to force the LLM to return fixed-key JSON, turning model calls into dependable functions.
Paragraph Summary
This article analyzes the core design of the open-source project SEO Master Agent: writing prompts as type signatures, with get_system_prompt() forcing each SEO workflow (onpage, offpage, gmb, content, geo) to return pure JSON with fixed keys, combined with input validation (missing input returns 422) to form a dual contract, giving LLM calls the dependability of functions. The project uses LiteLLM prefix routing to support OpenAI, Anthropic, Gemini, GLM, and Ollama, with single-request keys taking priority and never persisted, and the README candidly explaining the limitations of key storage. The task chain is an async queue plus a state machine, the fetcher has SSRF protection, and authentication is fail-closed. The smoke test bootstraps uvicorn and asserts the interface contract in a closed environment stripped of keys. The author also proactively flags two incomplete items: missing rank tracking and GMB generating but not publishing.
Related articles

How Big Can Stars Really Get? The Physical Limits of the Largest Stars
Red supergiants are the largest stars by volume, with radii up to 2,000 times the Sun, engulfing the inner solar system. But there is no precise upper limit on stellar size—models predict up to 15,000 solar radii. The most massive blue stars are far smaller than red supergiants—biggest and heaviest are two different things.

Apple HomeView Smart Display Full Feature Breakdown: Wall-Mount or Desktop, $299
Apple launches HomeView, a $299 smart display with wall-mount and desktop options, 12MP camera for facial recognition, smart home control, music playback, and recipe step-by-step cooking. Full breakdown of watch faces, apps, privacy, and cooking integration.

Where Did the Promised New Studio Go? Craft Computing Answers After Five Years
Craft Computing's host responds to viewer questions: the new studio promised five years ago was shelved as construction costs soared from $30,000 to $75,000, and was replaced by a 168-square-foot garage conversion.