Where Prompts Stop: The Runtime Architecture of a Paid LLM Agent on Post Fiat

Where Prompts Stop: The Runtime Architecture of a Paid LLM Agent on Post Fiat

By walkonwayvs | Crypto Related Reviews | 20 May 2026


You can pass every function-calling test, write a careful system prompt, and ship a niche agent that breaks the moment a real user sends it a real paid query. The gap isn't in the prompt. It isn't in the function-calling protocol. It's in the runtime architecture wrapped around them — and most of that architecture is invisible until you wire the agent end-to-end and watch what users actually trigger.

This memo documents the LLM-integration gap: what surfaces only once a real LLM is connected to real tools, real pricing tiers, and real user messages — and why the defense is layered runtime architecture, not prompt engineering alone.

The setup

I'm building The Validator Oracle (TVO) — a Post Fiat Task Node agent that answers questions about validators. Three paid tiers (Glance, Reading, Vision) plus a free Preview. By the time LLM-integration started, the agent already had a working data backend, a reference document explaining domain mechanics, a passed function-calling test on the chosen LLM (DeepSeek), and a topic-gate system prompt.

Every component a function-calling tutorial would say you need was in place. The agent still broke under real conditions in six distinct ways.

What broke (and why prompts alone couldn't catch it)

1. The model ignored hard length caps

The first paid tier — Glance — was specified as "one validator, point-in-time, 80 words max." The system prompt said HARD CAP: 80 words in capital letters. The model produced 155-word answers with tables, headers, and section breakdowns. Three rounds of prompt iteration — NO TABLES, NO HEADERS, PLAIN PROSE ONLY — got output into range.

This matters because tier pricing assumes tier output discipline. A Glance-priced answer that sprawls into Reading-shape output undermines the whole tier economy. The runtime layer that fixes it: rebuild the system prompt per query based on which command was invoked, so each tier gets its own length and format constraints at dispatch time. Same agent, different prompt per query.

2. Mid-answer self-correction visible to users

Real output from a live test:

"That historical blip is the main reason it's below the UNL eligibility cutoff of 40? Actually, its overall score of 65 is well above 40..."

The model corrected its own reasoning mid-sentence and shipped the correction to the user. The user paid for this answer.

This is documented LLM behavior — reasoning artifacts leaking into final output — but it's invisible until your agent is generating real customer-facing text. The fix is an explicit prompt rule banning specific phrases ("wait, actually", "X? Actually Y", "I now have a complete picture", "here's the answer", or any visible self-correction). You can only write that rule after you've seen it in the wild.

3. Silent invention of comparison numbers with tools available

The model called getValidatorByDomain for a Glance query, got the validator data back, then wrote:

"...the network average is roughly in the low 50s..."

The real network average was 80.36. The getNetworkSummary tool was in the schema. The model knew the tool existed. It chose to invent a number instead of fetching one.

This is the worst failure mode in the list, because it's silent — the user has no way to know the comparison is fabricated, and the agent has no way to flag it. Standard prompt guidance ("use the tools rather than guessing") is a suggestion. The model treats it as one.

The fix is a strict tool-grounding layer in the system prompt naming the specific failure mode in operator terms:

Every specific number in your answer (score, rank, percentage, count, average) MUST come from a tool result you received in this conversation. If you didn't call the tool, you don't know the number.

Network-level comparisons REQUIRE getNetworkSummary. If your answer mentions "network average", "compared to other validators", or any aggregate stat — getNetworkSummary MUST appear in your tool calls. No exceptions.

After these rules, the model stopped inventing numbers. These rules are domain-specific, observation-derived, and had to be written for this agent, after watching it fail. No generic LLM-best-practices doc would have produced them.

4. Hardcoded refusal fast-path beats LLM-only gating

Some refusal categories cluster around predictable phrasings. "Are X and Y run by the same operator?" "Is X a Sybil of Y?" "How do I send PFT to my wallet?" These should be refused 100% of the time, identically, with zero variance.

Routing every refusal through the LLM means:

  • Paying for an LLM call to produce a refusal that doesn't need reasoning
  • Accepting variance in refusal wording (which the user perceives as inconsistency)
  • Risking the LLM's empathy training overriding the refusal in edge cases

The runtime layer that fixes this is a hardcoded regex fast-path running before any LLM call. Two or three lines per category. Instant. Free. Identical wording every time. The LLM gate becomes the safety net for novel phrasings, not the primary refusal mechanism.

Here's the layer in code, from the live agent:

const SYBIL_PATTERNS = [
  /\bsame\s+(operator|person|owner|entity|company|guy|individual)\b/i,
  /\bsybil\b/i,
  /\brun\s+by\s+the\s+same\b/i,
  /\boperated\s+by\s+the\s+same\b/i,
  /\bowned\s+by\s+the\s+same\b/i,
];

const GENERAL_PFT_PATTERNS = [
  /\bmy\s+(wallet|balance|account|tokens?|pft)\b/i,
  /\bsend\s+pft\b/i,
  /\bbuy\s+pft\b/i,
  /\bwithdraw/i,
  /\btransfer\s+pft\b/i,
];

function checkHardcodedRefusal(query) {
  for (const p of SYBIL_PATTERNS) {
    if (p.test(query)) return { refused: true, reason: 'sybil' };
  }
  for (const p of GENERAL_PFT_PATTERNS) {
    if (p.test(query)) return { refused: true, reason: 'general_pft' };
  }
  return { refused: false };
}

// In the tier classifier — runs BEFORE the LLM call:
export async function classifyTier(userQuery) {
  const hardcoded = checkHardcodedRefusal(userQuery);
  if (hardcoded.refused) {
    return {
      tier: 'refuse',
      refusalText: hardcoded.reason === 'sybil' ? SYBIL_REFUSAL : GENERAL_PFT_REFUSAL,
      via: 'hardcoded'
    };
  }
  // Otherwise: call DeepSeek for tier classification
  // ...
}

That's the architectural pattern in roughly 20 lines. Hardcoded layer first, LLM gate second. Anything stable enough to regex doesn't need an LLM.

5. Pricing UI doesn't activate until registration

The Post Fiat MCP package exposes a register_bot call where each command can declare a min_cost_drops field — the messenger UI reads this and auto-attaches the right PFT amount when a user types a slash command.

The trap: this only activates after the bot is registered. A bot running before register_bot has been called has no declared command prices, and the messenger UI sends each command with whatever the user manually types — typically 1 drop. The pricing mechanic and the registration step are two separate gates.

6. No server-side enforcement of paid-tier amounts

This is the worst trap, because it's invisible until exploited:

min_cost_drops configures the messenger UI's send flow. It does NOT enforce minimum payment on the receiving side.

Live test: I sent the agent a /glance query with only 1 drop attached. The agent received the message, classified it as Glance, called the data tools, generated a full Glance-tier answer using paid LLM tokens, and sent the answer back. Total cost to me: 1 drop. Total cost to the agent operator (DeepSeek API + tokens): real money.

Anyone who bypasses the UI — by crafting the payment transaction directly, or by editing the amount before sending — can run paid commands at chain-floor price. The agent will happily process and answer because the bot code has no gate on incoming amount_drops.

The runtime layer that fixes this is a check in the message handler, before any paid-tier work begins:

const TIER_MIN_DROPS = {
  '/glance': 50_000_000,
  '/reading': 150_000_000,
  '/vision': 300_000_000,
};
if (TIER_MIN_DROPS[command] && envelope.amount_drops < TIER_MIN_DROPS[command]) {
  await sendReply(envelope.sender,
    `That command requires ${TIER_MIN_DROPS[command]/1_000_000} PFT. You sent ${envelope.amount_drops} drops.`);
  return;
}

Without that gate, the entire pricing model is honor-system. With that gate, the bot self-enforces.

The architectural insight: layered runtime defense

The pattern across all six frictions is the same: no single layer catches them. The defense is built into the agent's runtime in distinct layers, each catching what the others miss.

  • Layer 1 — Hardcoded fast-path. Regex matches predictable refusal categories before any LLM call. Cheap, fast, deterministic.
  • Layer 2 — LLM-based classifier. A small free LLM call decides which paid tier a free-form question fits, without committing user PFT. Safety net for cases the regex misses.
  • Layer 3 — Tool-grounded system prompt. The paid query runs against a system prompt with observation-derived anti-hallucination rules, tier-specific length caps, anti-self-correction rules, and explicit topic-gating.
  • Layer 4 — Server-side payment enforcement. Before any paid-tier work begins, the bot checks the incoming payment amount against the declared minimum and refuses underpaid queries.
  • Layer 5 — Meta-knowledge injection. A static markdown reference document is injected into the system prompt on every query, giving the model the domain knowledge to interpret tool results.

Function-calling tutorials mostly cover Layer 3. Everything else is operator-discovered architecture. None of it appears in vendor docs.

What another niche-agent builder would get wrong

If you ship a paid niche agent assuming function-calling tests, prompt instructions, and UI pricing settings are enough, the failure modes are predictable:

  • You set per-tier length caps in your system prompt and trust the model to honor them. Users get inconsistent output. Tier pricing loses meaning.
  • You write a careful topic-gate prompt and route every refusal through the LLM. You pay for refusals. Refusal wording drifts. Edge cases slip through.
  • You add getNetworkSummary to your tool schema and tell the model to use it. The model sometimes uses it, sometimes invents the number. Silently. Your paying users see hallucinated comparisons they have no way to detect.
  • You declare min_cost_drops and assume the messenger handles the rest. Anyone bypassing the UI runs your premium commands at chain-floor price. Your operating costs eat your margins until you notice — and you might not notice for a long time.
  • You write the system prompt once at agent boot and reuse it for every query. You can't enforce tier-specific behavior because every query gets the same prompt. The model has no way to distinguish a 50 PFT request from a 300 PFT request.

Each of these is a real, specific way to operate a paid agent at a loss — sometimes for hallucinated value, sometimes for actual unpriced computation. None of them surface in isolated tests. All of them surface in week one of real users.

Closing

The function-calling protocol is solved. The prompt engineering is well-documented. The hard part isn't either of those — it's the runtime layer where prompts end and architecture begins. That layer is operator-discovered, agent-specific, and absent from every getting-started guide.

If you're building a paid niche agent on Post Fiat — or anywhere — design the runtime layers before you design the prompts. The prompts go inside the layers. The layers are the agent.

How do you rate this article?

1


walkonwayvs
walkonwayvs

Professional artist. Part-time cryptocurrency trader. Semi-retired napper.


Crypto Related Reviews
Crypto Related Reviews

Is the juice worth the squeeze? Reviews for different crypto projects, apps, protocols, platforms, dapps, faucets, websites, airdrops, and fucking everything else related to crypto.

Publish0x

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.