Getting started

VERDANT SDK

A lightweight, NDPR-native SDK that wraps any AI integration and returns a structured reasoning chain — bias flags, plain-language explanations, and a trust score.

Overview

VERDANT is designed for Nigerian and African developers shipping AI into high-stakes contexts. Every AI call passes through a 5-stage reasoning pipeline that produces structured, auditable outputs — no freestyle AI responses, no hidden reasoning.

Drop-in wrapper

Wrap OpenAI, Anthropic, or Gemini calls without rewriting your product flow.

Nigerian baselines

Fairness baselines tuned to local demographic reality, not imported Western defaults.

Compliance trail

Persist audit payloads, export NDPR-ready reports, and send webhook alerts.

Readable by anyone

Give non-technical reviewers a plain-language explanation instead of model internals.

Installation

Install the VERDANT Python SDK using pip. Requires Python 3.11+.

$pip install verdant

Dependencies: httpx, pydantic, anthropic.

API keys

Your VERDANT API key authenticates every SDK and REST call. Generate one from the dashboard — open Settings → API keys and click Generate key. Pass it to the client as api_key or set VERDANT_API_KEY.

Shown once

The full vd_live_ key is displayed a single time at creation. Copy it immediately.

Hashed at rest

Only a prefix and a SHA-256 hash are stored — VERDANT can never show the key again.

Regenerate anytime

Regenerating revokes the old key immediately and issues a fresh one. Update your env after.

Lost your key?

Keys can't be recovered — only regenerated. Use Regenerate key in Settings, then update VERDANT_API_KEY everywhere it's used. The previous key stops working the moment you regenerate.

Quick start

Wrap your existing AI call. Your model runs where it always has; VERDANT runs the 5-stage pipeline on its output and returns the clean result alongside a full audit payload. Your VERDANT key is all you need — provider keys live in the dashboard.

Pythonverdant_example.py
from verdant import VerdantClient
import openai

client = VerdantClient(api_key="vd_live_...")

result = await client.wrap(
    fn=openai.chat.completions.create,
    context_type="hiring",
    input_text=user_prompt,
    model="gpt-4o",
    messages=[{"role": "user", "content": user_prompt}],
)

# Pass to your product
print(result.output)        # Clean AI response for your user
print(result.trust_score)   # 0–100 composite score
print(result.flags)         # ["proxy_language_detected"]
print(result.explanation)   # "Role phrasing may disadvantage..."
print(result.audit)         # Full reasoning chain JSON

Hosted mode (SaaS)

The SDK always talks to the hosted VERDANT API — your VERDANT_API_KEY is all you need. Provider keys (Claude, Gemini) stay server-side, managed from the dashboard.

Pythonhosted_example.py
from verdant import VerdantClient

# Just your VERDANT key. Provider keys (Claude, Gemini) live server-side —
# managed from the dashboard Settings page.
client = VerdantClient(api_key="vd_live_...")

# run() executes the pipeline on the hosted API over HTTP and returns the
# same result object as wrap().
result = await client.run(
    context_type="hiring",
    input_text="Should we shortlist this candidate from Kano?",
)

print(result.output)        # Response generated server-side
print(result.trust_score)   # 0–100 composite score
print(result.flags)         # ["proxy_language_detected"]
print(result.explanation)   # Plain-language rationale

run() — VERDANT does everything

Send the input; the server generates the output, runs the full pipeline with dashboard-managed provider keys, and logs the audit. The simplest setup.

wrap(fn=…) — call your own model

Your model call runs locally (keep custom prompts, streaming, or a provider VERDANT doesn't host); its output is scored on the server. Still just your VERDANT key.

Result object

client.wrap() returns a structured VerdantResult object. All fields are always present.

FieldTypeDescription
outputstrClean AI response — pass this to your user.
trust_scoreint0–100 composite trust score.
flagslist[str]Detected bias flags, e.g. proxy_language_detected.
explanationstrPlain-language rationale for the output.
auditdictFull reasoning chain JSON with all 5 stage outputs.
context_typestrActive context: hiring, lending, content, or healthcare.

Context types

VERDANT uses context types to load the correct Nigerian demographic baseline for each decision. Pass context_type when wrapping a call. If omitted, the pipeline infers it from the input text.

TypeUse caseAliases
hiringRecruitment, candidate evaluation, job screening.—
lendingLoan applications, credit scoring, financial assessment.—
contentContent moderation, text review, publishing decisions.content_moderation, moderation
healthcarePatient triage, diagnosis support, treatment recommendations.—

Alias resolution

You can pass aliases like content_moderation or moderation and they'll resolve to content. Unsupported values raise a ValueError.

Pipeline stages

Every AI call passes through 5 stages. Each stage returns structured JSON — no freestyle outputs. If a trust score looks wrong, the stage outputs tell you exactly where the signal came from.

1

Intent extraction

Detects what the model is trying to do, the context type (hiring, lending, etc.), and which demographic signals matter for this decision.

2

Fairness baseline load

Loads the versioned Nigerian demographic baseline for the active context. Baselines account for ethnic diversity, regional economic disparity, and gender dynamics in Nigerian professional contexts.

3

Bias pattern match

Checks the response against known bias patterns, proxy terms, and explanation confidence gaps. Flags are added to the result if patterns match.

4

Explanation generation

Produces a plain-language explanation using Claude. This explanation is what your stakeholders will read — it needs to be coherent, specific, and defensible.

5

Trust synthesis

Computes the 0–100 trust score from all stage signals. Weighted: bias (40%), explanation confidence (30%), intent alignment (20%), historical consistency (10%).

Audit payload

Every pipeline run produces an AuditPayload accessible via result.audit. This is the full reasoning chain that gets persisted to Supabase and powers the dashboard.

FieldTypeDescription
request_idUUIDUnique identifier for this pipeline run.
created_atdatetimeUTC timestamp of when the pipeline executed.
context_typestrThe active context: hiring, lending, content, or healthcare.
input_textstrThe input text passed to the pipeline.
output_textstrStringified version of the AI output.
stagesobjectFull per-stage structured outputs (intent, baseline, bias, explanation, trust).
trust_scoreintThe composite 0–100 trust score.
flagslist[str]All bias flags triggered during the pipeline.
explanationstrPlain-language explanation string.
model_namestrAI model used (default: claude-sonnet-4-6).
duration_msintPipeline execution time in milliseconds.
errorstr | nullError message if the wrapped function failed, null otherwise.
Pythonaudit_fields.py
result = await client.wrap(fn=my_ai_call, context_type="hiring", ...)
audit = result.audit

# Top-level audit fields
audit.request_id       # UUID — unique per pipeline run
audit.created_at       # UTC timestamp
audit.context_type     # "hiring", "lending", etc.
audit.input_text       # The input you provided
audit.output_text      # Stringified AI output
audit.model_name       # "claude-sonnet-4-6"
audit.duration_ms      # Pipeline execution time
audit.error            # None if successful, error string if failed

# Per-stage outputs
audit.stages.intent.detected_intent     # e.g. "candidate_evaluation"
audit.stages.intent.confidence          # 0.0–1.0
audit.stages.baseline.baseline_name     # e.g. "ng_hiring_v3"
audit.stages.baseline.baseline_version  # e.g. "3.1"
audit.stages.bias.severity              # "low" | "medium" | "high" | "critical"
audit.stages.bias.matched_patterns      # ["proxy_language", ...]
audit.stages.explanation.caveats        # ["Limited context provided", ...]
audit.stages.trust.score_breakdown      # {"bias": 38.0, "explanation": 28.5, ...}
audit.stages.trust.risk_level           # "low" | "medium" | "high" | "critical"

Trust score breakdown

Access audit.stages.trust.score_breakdown to see the per-signal scores. The trust score is a weighted composite: bias signal strength (40%), explanation confidence (30%), intent alignment (20%), and historical pattern consistency (10%).

Wrapping non-AI functions

VERDANT doesn't care how the output was generated. You can wrap legacy rules engines, deterministic scoring functions, or any callable that makes a high-stakes decision. The pipeline evaluates the relationship between input and output — not the source.

Pythonnon_ai_example.py
from verdant import VerdantClient

client = VerdantClient(api_key="vd_live_...")

# Your existing rules-based function — no AI involved
def legacy_lending_rules(applicant_data: dict) -> dict:
    if applicant_data["location"] == "Rural" and applicant_data["gender"] == "Female":
        return {"status": "rejected", "reason": "High risk demographic profile"}
    return {"status": "approved", "limit": 50000}

applicant = {"name": "Amina", "location": "Rural", "gender": "Female"}

# Wrap it exactly like an AI call
result = await client.wrap(
    fn=legacy_lending_rules,
    context_type="lending",
    input_text=str(applicant),
    applicant_data=applicant,
)

print(result.output)       # {"status": "rejected", ...}
print(result.trust_score)  # Low — bias detected
print(result.flags)        # ["demographic_bias", "gender_proxy"]

Why wrap non-AI logic?

Many high-stakes systems in Nigerian fintech, HR-tech, and health-tech still use deterministic rules or heuristics. These can encode demographic bias just like an LLM can. VERDANT gives you the same inspectability, audit trail, and NDPR compliance for any decision-making function.

Wrapping existing values

If you already have the AI output as a string or object and just want VERDANT to audit it, wrap it in a lambda or a simple function. Don't pass a raw value directly to fn — it expects a callable.

Pythonwrap_existing_value.py
from verdant import VerdantClient

client = VerdantClient(api_key="vd_live_...")

# You already have the output from somewhere else
my_existing_output = "The candidate is rejected due to cultural fit concerns."

# Option 1: Lambda wrapper (quick one-liner)
result = await client.wrap(
    fn=lambda **kwargs: my_existing_output,
    context_type="hiring",
    input_text="Evaluate this candidate for the analyst role.",
)

# Option 2: Async function wrapper (if you prefer explicit code)
async def existing_output(**kwargs):
    return my_existing_output

result = await client.wrap(
    fn=existing_output,
    context_type="hiring",
    input_text="Evaluate this candidate for the analyst role.",
)

print(result.trust_score)  # Pipeline runs on the value as-is
print(result.flags)        # Any bias flags detected in the text
print(result.explanation)  # Plain-language reasoning

Do this

fn=lambda **kwargs: my_value

Don't do this

fn=my_value — crashes with TypeError

Error handling

If the wrapped function throws an exception, VERDANT catches it gracefully. Your application stays up. The pipeline still runs and records a full audit — but the trust score is capped and the risk level is set to critical.

Pythonerror_handling.py
from verdant import VerdantClient

client = VerdantClient(api_key="vd_live_...")

async def flaky_ai_call(**kwargs):
    raise ConnectionError("API timeout")

# VERDANT catches the crash — your app stays up
result = await client.wrap(
    fn=flaky_ai_call,
    context_type="lending",
    input_text="Evaluate this loan application.",
)

print(result.output)       # "" (empty — no upstream output)
print(result.trust_score)  # ≤ 15 (capped to critical)
print(result.flags)        # []
print(result.explanation)  # "...The wrapped AI call failed: API timeout."

# The audit still records everything
print(result.audit.stages.trust.risk_level)  # "critical"
print(result.audit.stages.trust.alerts)      # ["Wrapped function call failed"]
print(result.audit.error)                    # "API timeout"

Trust score

≤ 15

Capped to critical threshold

Risk level

critical

Always set on failure

Output

Empty string

No upstream data available

REST API reference

If you're integrating via HTTP instead of the Python SDK, the FastAPI backend exposes these endpoints. Each one requires your VERDANT API key as a Authorization: Bearer header. All responses use a consistent envelope: { data, meta, error }

MethodEndpointDescription
POST/pipeline/runGenerate an output for the input and run the 5-stage pipeline. Returns full audit payload with trust score, flags, and explanation.
POST/pipeline/analyzeAnalyze an output you already produced (input_text + output_text). Runs the pipeline stages server-side without re-generating. Powers wrap().
GET/auditsList audit logs with pagination. Supports filtering by context_type. Query params: limit, offset, context_type.
GET/audits/{audit_id}Retrieve a single audit record by ID. Returns the full stage breakdown and metadata.
GET/reports/ndprGenerate an NDPR compliance report. Aggregates trust scores, flag counts, and context breakdown. Query param: days (default 30).
POST/webhooks/dispatchManually dispatch webhook alerts for a specific audit. Query params: audit_id, force (bypass threshold check).
cURLexamples
# Every SDK-facing endpoint requires your VERDANT API key as a Bearer token.
# Generate one from the dashboard: Settings -> API keys -> Generate key.

# Run pipeline via REST API
curl -X POST https://verdant-be.onrender.com/pipeline/run \
  -H "Authorization: Bearer vd_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "input_text": "Evaluate this candidate",
    "context_type": "hiring",
    "metadata": {}
  }'

# Fetch audit logs
curl https://verdant-be.onrender.com/audits?limit=10&context_type=hiring \
  -H "Authorization: Bearer vd_live_..."

# Get a specific audit
curl https://verdant-be.onrender.com/audits/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer vd_live_..."

# Generate NDPR compliance report
curl https://verdant-be.onrender.com/reports/ndpr?days=30 \
  -H "Authorization: Bearer vd_live_..."

# Manually dispatch webhooks for an audit
curl -X POST "https://verdant-be.onrender.com/webhooks/dispatch?audit_id=550e8400-...&force=true" \
  -H "Authorization: Bearer vd_live_..."

Webhooks

When a pipeline result falls below your configured trust score threshold (default: 40), VERDANT automatically dispatches a signed webhook POST to all active endpoints. Each webhook is HMAC-SHA256 signed so you can verify authenticity.

JSONwebhook_payload.json
# Webhook POST payload (sent automatically on low trust)
{
  "event": "verdant.audit.low_trust",
  "timestamp": "2026-07-08T22:30:00+00:00",
  "audit": {
    "audit_id": "550e8400-e29b-41d4-a716-446655440000",
    "context_type": "hiring",
    "trust_score": 28,
    "flags": ["proxy_language_detected", "demographic_bias"],
    "explanation": "The output uses language proxies...",
    "stages": { ... }
  }
}

# Headers included with every webhook
# X-Verdant-Event: verdant.audit.low_trust
# X-Verdant-Timestamp: 2026-07-08T22:30:00+00:00
# X-Verdant-Audit-Id: 550e8400-...
# X-Verdant-Signature: sha256=abc123...  (HMAC-SHA256)

HMAC-SHA256 signing

Every webhook payload is signed with your WEBHOOK_SECRET. Verify the X-Verdant-Signature header to confirm the request came from VERDANT.

Per-webhook thresholds

Each webhook endpoint can have its own min_trust_score threshold. A webhook with threshold 60 will fire more often than one set to 20.

Manual dispatch

Use POST /webhooks/dispatch?audit_id=...&force=true to re-send webhooks for any audit, even if the trust score is above threshold.

Environment variables

These are the only variables the SDK reads. You just need your VERDANT_API_KEY — provider keys are managed in the dashboard, not your app.

.env
# Your VERDANT API key — from the dashboard: Settings → API keys.
# This is all the SDK needs.
VERDANT_API_KEY=vd_live_...

# Provider keys (Claude, Gemini) are NOT set here — they live in the
# dashboard (Settings → Provider Keys) and are used server-side.

Configuration

The client reads VERDANT_API_KEY from the environment, or you can pass it as a constructor argument.

Python
from verdant import VerdantClient

# Just pass your key — that's it.
client = VerdantClient(api_key="vd_live_...")

Trust score alert threshold

Outputs scoring below the alert threshold (default 40) trigger a webhook POST to your configured endpoint. This threshold is set on your VERDANT deployment (via the dashboard), not in the SDK.