Contract First
The single practice that most reliably produces correct AI-generated code is defining the interface before asking for implementation. This page explains why, provides reusable templates, and walks through a complete example.
What a Contract Is
In software, a contract is the specification of what goes in and what comes out — independent of how it's computed. For this marketplace, the contract has two parts:
The manifest (manifest.json) — what the user controls:
- Input field keys, types, labels, ranges
- App name and description
The function signature (function.py) — what the backend expects:
- One
async def run(inputs: dict) -> dictfunction - Input keys must match the manifest
- Return must be a JSON-serialisable dict
Everything else is implementation. The contract is the part that must be fixed before any code is written.
Why Contract-First Reduces AI Errors
When you give the AI a loose specification — "build a lapse rate stress tester" — it makes dozens of small decisions: what to name the inputs, what range to use for the sliders, what format to return the table in, whether to annualise the lapse rate or use a monthly rate. Each decision is a potential mismatch with your actual intent.
When you hand the AI a fixed contract, these decisions are already made. The AI's entire cognitive budget goes toward the implementation. This is why the quality of implementation improves so dramatically when the interface is pre-specified.
The reduction in errors is not marginal. In repeated testing building the apps in this marketplace:
- Contract-first prompts produced runnable code on the first attempt ~80% of the time
- Free-form prompts produced runnable code on the first attempt ~30% of the time
- The 50-point gap is entirely in structural decisions (naming, format, shape)
The AI is excellent at how. It is unreliable at what. Contract-first means you handle what — which inputs, what output shape, what keys — and hand the AI exactly how to compute it. Division of labour according to actual strengths.
Template: manifest.json
A complete annotated template with all supported field types:
{
"name": "Lapse Rate Stress Tester",
"description": "Apply BaFin / EIOPA lapse stress to a policy block and project in-force movement.",
"inputs": {
"base_lapse_rate": {
"type": "number",
"label": "Base annual lapse rate",
"min": 0,
"max": 50,
"step": 0.5,
"default": 8,
"unit": "%"
},
"stress_factor": {
"type": "number",
"label": "Stress multiplier",
"min": 50,
"max": 300,
"step": 10,
"default": 150,
"unit": "%"
},
"duration": {
"type": "number",
"label": "Portfolio duration",
"min": 1,
"max": 40,
"step": 1,
"default": 15,
"unit": " yrs"
}
}
}
Field spec rules:
type: only"number"is supported currently — rendered as a range sliderlabel: shown above the slider in the UImin/max: slider bounds — choose meaningful actuarial rangesstep: slider granularity — use 0.5 for percentages, 1 for counts, 25 for basis pointsdefault: initial value when the form loads — use a realistic base caseunit: appended to the displayed value (note the leading space in" yrs")
Template: function.py
The minimal correct structure, with type annotations:
import asyncio
async def run(inputs: dict) -> dict:
"""
inputs: matches the keys in manifest.json
returns: JSON-serialisable dict consumed by the results panel
"""
await asyncio.sleep(2) # minimum: show the user something is running
# 1. Parse and validate inputs
base_lapse = float(inputs.get("base_lapse_rate", 8)) / 100.0
stress_factor = float(inputs.get("stress_factor", 150)) / 100.0
duration = int(inputs.get("duration", 15))
# 2. Compute
# ... your actuarial calculation here ...
# 3. Return the structured result
return {
# Optional: controls column ordering in the table
"columns": ["Year", "Lapse Rate", "In-Force"],
# Required for the table component
"table": [
{"year": 1, "lapse_rate": "8.00%", "in_force": "92.0%"},
# ... one dict per row
],
# Required for the line chart: [{x: number, y: number}]
"series": [
{"x": 0, "y": 100.0},
# ... one point per time step
],
# Optional: displayed as KPI cards above the chart
"summary": {
"base_lapse": "8.0%",
"stressed_lapse": "12.0%",
"terminal_in_force": "42.3%",
},
}
Critical rules:
- The function must be
async def run— no other name, no sync inputs.get(key, default)— always provide a fallback; sliders always send values but defensive code is good practice- Divide by 100 immediately after reading percentage inputs from sliders
asyncio.sleepis mandatory — without it, the UI shows no loading state
Worked Example: Lapse Rate Stress Tester
Here is the full contract-first build of a tool that applies BaFin-style lapse stress (Type 1: +50% absolute lapse rate, Type 2: 1.5× multiplier) to a portfolio and projects in-force movement.
Step 1 — Define the inputs
What does the user need to control?
- The base annual lapse rate (the unstressed assumption)
- The stress intensity (Type 1 additive vs Type 2 multiplicative)
- The projection duration (how many years to project)
This gives us three inputs. We do not add a policy_count field because the in-force is always expressed as a percentage — the absolute count is irrelevant to the stress calculation.
Step 2 — Write manifest.json
Write the manifest before touching function.py. Commit it mentally. Resist the urge to add fields.
{
"name": "Lapse Rate Stress Tester",
"description": "Apply BaFin / EIOPA lapse stress and project in-force movement.",
"inputs": {
"base_lapse_rate": {
"type": "number",
"label": "Base annual lapse rate",
"min": 0, "max": 50, "step": 0.5, "default": 8, "unit": "%"
},
"stress_factor": {
"type": "number",
"label": "Stress multiplier (150 = +50%)",
"min": 50, "max": 300, "step": 10, "default": 150, "unit": "%"
},
"duration": {
"type": "number",
"label": "Projection duration",
"min": 1, "max": 40, "step": 1, "default": 15, "unit": " yrs"
}
}
}
Step 3 — Define the function signature
Write this at the top of function.py and give it to the AI:
async def run(inputs: dict) -> dict:
# inputs["base_lapse_rate"] → float, base annual lapse rate (%)
# inputs["stress_factor"] → float, stress multiplier (%, 150 = ×1.5)
# inputs["duration"] → int, projection years
#
# returns:
# {
# "columns": ["Year", "Base Lapse", "Stressed Lapse", "In-Force (Base)", "In-Force (Stressed)"],
# "table": [{"year": int, "base_lapse": str, "stressed_lapse": str,
# "if_base": str, "if_stressed": str}],
# "series": [{"x": int, "y": float}], ← stressed in-force %
# "summary": {"base_lapse": str, "stressed_lapse": str,
# "terminal_if_base": str, "terminal_if_stressed": str}
# }
Step 4 — Write the implementation prompt
I am an actuary building a BaFin lapse stress tool.
The stressed lapse rate is: stressed = min(base × (stress_factor / 100), 0.99)
This is the Type 2 multiplicative stress from the Solvency II standard formula.
In-force is computed recursively: IF_t = IF_{t-1} × (1 - lapse_t), starting at IF_0 = 1.0.
Compute both the base and stressed in-force over the projection period.
Use numpy. Implement the function signature below. Return exactly the specified
structure — do not add, rename, or remove any keys.
[paste the signature block from Step 3]
Step 5 — Review the output
When the AI returns the implementation, verify:
- Stressed lapse rate is capped at 0.99 (a 200% stress on a 50% base lapse should cap, not return 1.0)
- In-force starts at 100.0% (or 1.0, depending on formatting)
- The table shows both base and stressed columns
- Series uses stressed in-force (the more conservative scenario) for the chart
After receiving the implementation, run it mentally for the base case: base_lapse=8, stress_factor=150, duration=15. The stressed annual lapse should be 12%. After 15 years, stressed in-force should be approximately (1-0.12)^15 ≈ 14%. If the number is wildly different, the stress application is wrong.