SMBs handling customer financial data must avoid exposing PII or sensitive information when using AI for support or analysis. Off-the-shelf chatbots lack guardrails for finance-specific compliance risks.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
This recipe builds a security-guardrail layer around Perplexity AI for SMB financial queries. You’ll create a Next.js API endpoint that routes every user message through a pipeline of input guardrails (PII redaction, prompt injection detection, topic enforcement, cost precheck, and rate limiting), then through Perplexity’s search-augmented model, and finally through output guardrails (PII scanning, hallucination detection, and toxicity filtering). Every incident is logged to an audit trail, and LLM costs are tracked per request. By the end, you’ll have a functioning chat gateway that prevents sensitive financial data from leaving your application.
Prerequisites
Node.js >= 22 and pnpm 10 installed on your machine
A Perplexity API key — sign up at perplexity.ai and create an API key
Familiarity with TypeScript, Next.js App Router, and basic Zod schema validation
Step 1: Scaffold the project and review dependencies
Start from an empty directory. The scaffold has already been set up with pnpm create next-app and all dependencies installed. Verify the project structure:
terminal
ls
text
app/node_modules/
packages/
src/
tests/
.env.example
.gitignore
LICENSE
eslint.config.mjs
next.config.ts
package.json
pnpm-lock.yaml
tsconfig.json
vitest.config.ts
Open package.json to see the exact-pinned dependencies:
@reaatech/guardrail-chain-config — configuration loader from JSON, YAML, and environment variables
@reaatech/guardrail-chain-observability — pluggable logging, metrics, and tracing
@reaatech/llm-cost-telemetry — cost calculation utilities and domain types
Expected output: No errors from the terminal — just the directory listing.
Step 2: Configure environment variables
The .env.example file lists every environment variable the application reads. Copy it to .env and fill in your Perplexity API key:
terminal
cp .env.example .env
The final .env should look like this:
env
# Env vars used by perplexity-security-guardrails-for-smb-financial-queries.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=development# PerplexityPERPLEXITY_API_KEY=<your-perplexity-api-key>PERPLEXITY_MODEL=pplx-70b-online# Guardrail Chain budget (env-driven, adjustable without code)GUARDRAIL_CHAIN_BUDGET_MAX_LATENCY_MS=3000GUARDRAIL_CHAIN_BUDGET_MAX_TOKENS=8000GUARDRAIL_CHAIN_BUDGET_SKIP_SLOW=true
Replace <your-perplexity-api-key> with a real key from your Perplexity account. The GUARDRAIL_CHAIN_* variables let you tune latency and token budgets without touching code — the config loader from @reaatech/guardrail-chain-config reads them automatically.
Expected output: A .env file present in the project root with all values filled in.
Step 3: Define domain types with Zod
Create the foundation — the types and Zod schemas that every module will import. Write src/types/guardrails.ts:
ChatRequestSchema uses z.enum to restrict message roles to exactly "user", "assistant", and "system" — invalid roles are caught at the API boundary before reaching any business logic.
IncidentRecord carries a severity field ranging from "low" to "critical" so your operations team can triage incidents by impact.
The TypeScript interfaces (ChatRequest, ChatResponse, etc.) are the compile-time contract; the Zod schemas (ChatRequestSchema, IncidentRecordSchema) enforce it at runtime.
Expected output: A new file at src/types/guardrails.ts with no TypeScript errors.
Step 4: Load application configuration from the environment
Create src/lib/config.ts to wire up the @reaatech/guardrail-chain-config package:
The loadChainConfig function from @reaatech/guardrail-chain-config reads GUARDRAIL_CHAIN_* environment variables and deep-merges them into the returned config object — so you can set GUARDRAIL_CHAIN_BUDGET_MAX_LATENCY_MS=1500 in .env to tighten the latency budget without deploying new code.
Expected output:src/lib/config.ts created with no type errors. Run pnpm typecheck to confirm.
Step 5: Create the Perplexity SDK wrapper
Next, build the perplexity-sdk adapter. Write src/lib/perplexity.ts:
The perplexity-sdk uses OpenAPI-generated classes: createConfiguration() accepts authMethods with a tokenProvider for Bearer token auth, and returns a configuration object used to instantiate DefaultApi. The ChatCompletionsPostRequestModelEnum provides typed model constants. The response structure is result.choices[0].message.content for the text, and result.usage with promptTokens/completionTokens for token counts.
Expected output:src/lib/perplexity.ts with no type errors. Run pnpm typecheck again.
Step 6: Track LLM costs with telemetry
Create src/lib/cost-tracker.ts to record every Perplexity API call’s cost:
calculateCostFromTokens from @reaatech/llm-cost-telemetry takes token count and price-per-million-tokens and returns the fractional USD cost. The pricing map uses estimated Perplexity rates — check current pricing at perplexity.ai/pricing and update these values. Unknown models fall back to the pplx-70b-online tier with a warning.
This is the heart of the recipe. Create src/lib/guardrails.ts that assembles a pipeline of input guardrails (run before the Perplexity call) and output guardrails (run after):
ts
import { GuardrailChain, setLogger, ConsoleLogger, generateCorrelationId, type Guardrail, type GuardrailResult, type ChainContext, type ExecutionOptions, type ChainResult,} from "@reaatech/guardrail-chain";import { PIIRedaction, PromptInjection, TopicBoundary, ToxicityFilter, PIIScan, HallucinationCheck, CostPrecheck, RateLimiter, CachedGuardrail,} from "@reaatech/guardrail-chain-guardrails";import { injectionGuard, piiGuard, secretGuard, GuardrailsEngine,
Here’s what each input guardrail does:
Guardrail
Purpose
PresidioAdapter
Wraps @presidio-dev/hai-guardrails — checks for prompt injection, PII, and secrets using Presidio’s heuristic engine with a 0.7 threshold
The incident store is a simple in-memory array. inputSnippet is truncated to 200 characters to prevent log bloat. Each incident is also emitted through the structured logger (which writes to console in development via ConsoleLogger).
Expected output:src/services/incident-logger.ts with no type errors.
Step 9: Wire up the chat orchestrator
The main orchestrator lives in src/services/chat-service.ts — it ties together guardrails, Perplexity, cost tracking, and incident logging:
ts
import { getLogger } from "@reaatech/guardrail-chain";import { executeInputGuardrails, executeOutputGuardrails, generateCorrelationId, type ChainResult,} from "../lib/guardrails.js";import { createPerplexityClient, queryPerplexity, PerplexityApiError,} from "../lib/perplexity.js";import { logIncident } from "./incident-logger.js";import { recordCostSpan } from "../lib/cost-tracker.js";import { getAppConfig } from "../lib/config.js";import type { ChatRequest, ChatResponse,} from "../types/guardrails.js"
The flow is:
Validate the incoming request with ChatRequestSchema — returns early on invalid format
Extract the last user message from the messages array
Block on failure — log the incident and return a 403-equivalent response
Call Perplexity — send the (potentially redacted) input to the search-augmented model
Run output guardrails — PII scan, hallucination check, toxicity filter
Block on failure — log the incident with severity derived from the guardrail type
Record cost — emit a telemetry span with token counts and model pricing
Return success — response text, passed flag, and cost breakdown
Expected output:src/services/chat-service.ts typechecks. Note the .js extensions in imports — this is required by NodeNext module resolution in Next.js.
Step 10: Set up the API route handler
Create the Next.js App Router route at app/api/chat/route.ts:
Sets runtime = "nodejs" because the guardrail chain and Perplexity SDK pull in Node-only modules (file system, network) that won’t work in Edge Runtime
Exports a named POST function — App Router requires named exports for HTTP methods, not default exports
Returns 200 with the response body on success, 403 when a guardrail blocks the request, 400 for malformed JSON, and 500 for unexpected errors
Replace the scaffolded app/page.tsx with a landing page that documents the API:
tsx
export default function Home() { return ( <main style={{ maxWidth: 720, margin: "0 auto", padding: "2rem 1rem", fontFamily: "system-ui, sans-serif" }}> <h1>Perplexity Security Guardrails</h1> <p style={{ fontSize: "1.125rem", color: "#374151" }}> Safely use Perplexity’s search-augmented AI for financial Q&A with real-time PII redaction, harmful-content blocking, and audit logging. </p> <section style={{ marginTop: "2rem" }}> <h2>API Reference</h2> <p>Send a POST request to <code>/api/chat</code> with this JSON body:</p> <pre style={{ background: "#f3f4f6", padding: "1rem", borderRadius: 8, overflow: "auto" }}>{`{ "messages": [{ "role": "user", "content": "..." }], "userId": "optional-user-id", "sessionId": "optional-session-id", "model": "optional-model-override"}`} </pre> <p> Responses include <code>reply</code>, <code>passed</code>, and when a guardrail triggers, an <code>incident</code> record with the reason. </p> </section> <footer style={{ marginTop: "3rem", color: "#6b7280", fontSize: "0.875rem" }}> <p> Built with Next.js, <code>@reaatech/guardrail-chain</code>, and Perplexity AI. </p> </footer> </main> );}
Expected output: Both app/api/chat/route.ts and app/page.tsx typecheck. Run pnpm typecheck to confirm zero errors.
Step 11: Configure server-side instrumentation
Create src/instrumentation.ts for server startup initialization:
ts
import { setLogger, ConsoleLogger } from "@reaatech/guardrail-chain-observability";export function register() { if (process.env.NEXT_RUNTIME === "nodejs") { setLogger(new ConsoleLogger()); }}
This ensures the ConsoleLogger is set once at server startup rather than on every request. The NEXT_RUNTIME === "nodejs" guard prevents the import from failing in Edge contexts. For this to work in Next.js, you need to enable instrumentation in next.config.ts. Open the file and add the experimental.instrumentationHook option:
ts
import type { NextConfig } from "next";const nextConfig: NextConfig = { experimental: { instrumentationHook: true, },};export default nextConfig;
Expected output:src/instrumentation.ts and the updated next.config.ts typecheck successfully.
Step 12: Run the test suite
Finally, run the full test suite to verify everything works:
terminal
pnpm typecheck && pnpm lint && pnpm test
Expected output: All three commands exit with code 0. The typechecker reports zero errors, the linter finds no violations, and vitest reports:
Configuration loader — getAppConfig() reads PERPLEXITY_API_KEY and PERPLEXITY_MODEL from env, falls back to defaults, and delegates to @reaatech/guardrail-chain-config
Perplexity SDK wrapper — createPerplexityClient returns the DefaultApi client; queryPerplexity sends the correct payload and extracts the response; errors throw PerplexityApiError with appropriate status codes
Guardrail chain — buildInputGuardrails() returns 6 guardrails in the correct order; buildOutputGuardrails() returns 3; executeInputGuardrails blocks PII, prompt injections, and off-topic queries; executeOutputGuardrails blocks hallucination patterns and PII leaks; CachedGuardrail caches results correctly
Incident logger — logIncident stamps entries with IDs and timestamps; truncates inputSnippet to 200 chars; queryIncidents filters by userId and severity; clearIncidents empties the store
Chat orchestrator — happy path returns the Perplexity response with cost data; blocked input guardrails return { passed: false, incident } with phase: "input"; blocked output guardrails return { passed: false, incident } with phase: "output"; API errors are caught and logged; Zod validation failures return early
Route handler — POST /api/chat returns 200 on pass, 403 on guardrail block, 400 on bad JSON, 500 on crashes
Next steps
Add a database-backed incident store — replace the in-memory array in incident-logger.ts with PostgreSQL or SQLite so incidents persist across server restarts. The queryIncidents signature already supports this without changing callers.
Wire up remote observability — replace ConsoleLogger with a pino or winston transport that sends logs to your observability platform (Datadog, Grafana, etc.). The setLogger function accepts any object matching the Logger interface.
Extend guardrails with an external verifier — pass an API-based fact-checking function to HallucinationCheck for verified financial data lookups (e.g., tax rates, IRS publication references) instead of relying on the heuristic “I think” / “probably” pattern detection.
SelectionType,
} from "@presidio-dev/hai-guardrails";
class PresidioAdapter implements Guardrail<string, string> {