Build a Two-Agent FX Stack: Research Agent + Execution Gatekeeper
Author: FXMacroData Team
Published: May 21, 2026
Single-agent FX bots fail for a simple reason: the same model that generates ideas is also allowed to approve them. When volatility spikes around events like US Non-Farm Payrolls, one reasoning error can jump straight into position risk.
This guide shows a safer architecture: split responsibilities between two agents. The first agent does market research and proposes setups. The second agent is a strict gatekeeper that can only approve, resize, or reject proposals based on hard risk rules.
By the end, you will have a practical two-agent workflow for EUR/USD and GBP/USD that supports both direct REST API integration and MCP-based tool integration.
Prerequisites
- Python 3.10+.
- An FXMacroData API key from API Management.
- An LLM endpoint for the research and gatekeeper agents.
- Basic familiarity with JSON and HTTP APIs.
Install dependencies:
pip install requests python-dotenv pydantic
Create a .env file:
FXMD_API_KEY=your_fxmacrodata_key
RESEARCH_MODEL=claude-or-hermes
GATEKEEPER_MODEL=claude-or-hermes
MAX_RISK_PCT=0.50
Step 1: Define strict roles for both agents
What to do: lock responsibilities before writing code.
- Research agent: reads macro + market context and proposes candidate setups.
- Gatekeeper agent: validates constraints only. It cannot invent new trades, only approve/reject/resize.
Why it matters: this separation prevents a single model from bypassing risk controls when confidence is high but evidence is weak.
Step 2: Pull structured context with direct REST calls
What to do: fetch releases and spot data from FXMacroData so the research agent gets clean inputs instead of unstructured headlines.
curl "https://api.fxmacrodata.com/v1/calendar/usd?api_key=YOUR_API_KEY"
curl "https://api.fxmacrodata.com/v1/announcements/eur/inflation?api_key=YOUR_API_KEY"
curl "https://api.fxmacrodata.com/v1/announcements/gbp/unemployment?api_key=YOUR_API_KEY"
curl "https://api.fxmacrodata.com/v1/forex?base=EUR"e=USD&api_key=YOUR_API_KEY"
Why it matters: consistent, structured fields give both agents the same source of truth and make validation deterministic.
Minimal Python collector:
import os
import requests
from datetime import datetime, timezone
API = "https://api.fxmacrodata.com/v1"
KEY = os.environ["FXMD_API_KEY"]
def fxmd_get(path, **params):
r = requests.get(
f"{API}{path}",
params={"api_key": KEY, **params},
timeout=25,
)
r.raise_for_status()
return r.json()
def build_market_context():
return {
"asof_utc": datetime.now(timezone.utc).isoformat(),
"calendar_usd": fxmd_get("/calendar/usd").get("data", [])[:8],
"calendar_eur": fxmd_get("/calendar/eur").get("data", [])[:8],
"eur_inflation": fxmd_get("/announcements/eur/inflation").get("data", [])[-1:],
"gbp_unemployment": fxmd_get("/announcements/gbp/unemployment").get("data", [])[-1:],
"eurusd": fxmd_get("/forex", base="EUR", quote="USD").get("data", [])[-48:],
"gbpusd": fxmd_get("/forex", base="GBP", quote="USD").get("data", [])[-48:],
}
Step 3: Generate trade candidates with the research agent
What to do: ask the research agent for structured trade candidates only. Do not let it send execution-ready instructions.
{
"pair": "EUR/USD",
"bias": "long|short|flat",
"thesis": "string",
"confidence": 0.0,
"entry_zone": "string",
"invalidation": "string",
"event_risks": ["string"]
}
Why it matters: a fixed schema lets the gatekeeper enforce rules on predictable fields instead of trying to parse free-form text.
Step 4: Enforce hard risk rules in the gatekeeper agent
What to do: run every candidate through a second model or rule-first validator with strict limits.
Example policy:
- Maximum risk per trade: 0.50% of equity.
- No new trades inside 15 minutes of high-impact releases from the release calendar.
- Mandatory invalidation level.
- Reject if confidence < 0.60.
Pydantic gate and decision output:
from pydantic import BaseModel, Field
class Candidate(BaseModel):
pair: str
bias: str
thesis: str
confidence: float = Field(ge=0.0, le=1.0)
entry_zone: str
invalidation: str
event_risks: list[str]
class GateDecision(BaseModel):
status: str # approve, resize, reject
approved_size_pct: float
reason: str
def gate(candidate: Candidate, max_risk_pct: float = 0.50) -> GateDecision:
if candidate.confidence < 0.60:
return GateDecision(status="reject", approved_size_pct=0.0, reason="Low confidence")
if not candidate.invalidation.strip():
return GateDecision(status="reject", approved_size_pct=0.0, reason="Missing invalidation")
proposed = 0.50 if candidate.confidence >= 0.75 else 0.30
size = min(proposed, max_risk_pct)
return GateDecision(status="approve", approved_size_pct=size, reason="Within policy")
Why it matters: even if the research agent has a bad read, the gatekeeper can still block oversized or weak setups.
Step 5: Add MCP integration path (tool-native agent workflow)
What to do: expose FXMacroData through MCP so agent frameworks can call tools natively instead of building custom REST glue for each bot.
Start a Python-based MCP server via uvx:
uvx fxmacrodata-mcp --transport http --server-url https://mcp.fxmacrodata.com
Client config example:
{
"mcpServers": {
"fxmacrodata": {
"command": "uvx",
"args": [
"fxmacrodata-mcp",
"--transport",
"http",
"--server-url",
"https://mcp.fxmacrodata.com"
]
}
}
}
Example MCP tool-call schema for your research agent:
{
"name": "get_fx_calendar_and_spot",
"description": "Get upcoming macro events and current spot context for selected pairs",
"input_schema": {
"type": "object",
"properties": {
"currencies": {
"type": "array",
"items": { "type": "string" }
},
"pairs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"base": { "type": "string" },
"quote": { "type": "string" }
},
"required": ["base", "quote"]
}
}
},
"required": ["currencies", "pairs"]
}
}
Agent invocation example (MCP path):
Research Agent:
"Using the fxmacrodata MCP tools, pull today's USD/EUR/GBP calendar and spot for EUR/USD and GBP/USD.
Return up to 3 candidate setups in strict JSON schema."
Gatekeeper Agent:
"Validate each candidate against risk policy v1. Reject anything violating confidence,
size, invalidation, or event-window constraints. Return approve/resize/reject with reason."
Why it matters: MCP reduces integration drift as your stack grows, especially when you run multiple agents or swap model providers.
Step 6: Route outputs to human review, then broker API
What to do: send the gatekeeper decision to Slack/Telegram first. Only send approved trades to execution systems after human confirmation in early versions.
Why it matters: this creates an auditable loop and protects you during prompt or model changes.
[FX Two-Agent Candidate]
Pair: EUR/USD
Research Bias: Long
Gatekeeper: Approve
Approved Size: 0.30%
Reason: Confidence 0.71, invalidation present, no high-impact event in 15m window.
What you built
You now have a two-agent FX architecture that separates idea generation from risk approval, supports direct REST integration, and also supports MCP-based tool orchestration through uvx. This is a stronger foundation than single-agent bots because trade candidates must survive an independent policy gate before execution.
Next step: add a kill-switch layer that automatically forces reject after unusual latency, missing data fields, or repeated schema failures during high-volatility sessions. You can also add session-aware constraints from FX sessions and positioning context from COT.