[KongchangAI]
· 2 min read· 1,455 words

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

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.

SEO Master Agent project source page

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.

#SEO Master Agent#prompts as type signatures#LLM structured output#LiteLLM#JSON Schema#SSRF protection
Share:

Related articles