Build an AI trading agent
Give your own AI agent a scoped agent key, connect it to the Tickwire MCP server or the agent REST routes, and handle held, rejected and unknown outcomes safely.
An AI agent is a program where a language model decides which tools to call. This guide shows how to give one access to a MetaTrader 4 or 5 account through Tickwire, so that it can look, and trade only if you allow it, with every order checked and recorded.
There are two ways in. Both use the same guarded path and the same agent key.
| Route | Best for | How the key is sent |
|---|---|---|
| The Tickwire MCP server | Agent frameworks that speak MCP (Claude Agent SDK, OpenAI Agents SDK, Claude Code, Cursor) | Authorization: Bearer twag_… |
The agent REST routes, /v1/agent/… | Your own tool-calling loop, or any language | X-Tickwire-Agent-Key: twag_… |
AI apps such as Claude and ChatGPT don’t need a key at all: they add the MCP address and sign in with Tickwire. See AI agents for those steps.
1. Create an agent key
Create the key in the dashboard under AI agents > Keys, or with the API using your own API key:
curl -s -X POST https://api.tickwire.io/v1/agent-keys \
-H "Authorization: Bearer $TICKWIRE_API_KEY" \
-H "content-type: application/json" -H "X-Reason: research agent" \
-d '{"name": "research-agent", "account_ids": ["<ACCOUNT_ID>"], "read_only": true}'
- The answer’s
key(twag_…) is shown once. Keep it in a secret store or an environment variable, never in source control. account_idslimits the key to those accounts. The agent can’t see any others.read_only: trueis the safe default. A read-only key that tries to trade gets403 read_only_keyand nothing is sent. Change it later withPATCH /v1/agent-keys/{id}or in the dashboard.- Agent keys can’t call the direct terminal routes (they get
401 invalid_token), so an agent can’t go around the checks below. See Authentication.
Check what the key may do:
curl -s -H "X-Tickwire-Agent-Key: $AGENT_KEY" https://api.tickwire.io/v1/agent/me
2. What happens to every agent order
Every trade from an agent passes the same checks, in this order:
- Scope. The account must be one the key may use.
- Read-only. A read-only key stops here with
read_only_key. - Kill switches. One per agent key and one per account. Engaged means
kill_switch_engaged, and nothing is sent. - Your pre-trade hook, if you set one. Tickwire posts the order, who asked, why, and a fresh
read of the account to your HTTPS endpoint, signed with HMAC-SHA256. Your hook answers
allow,denyorhold. If it can’t be reached or its answer can’t be verified, the order is not sent. See the hook. - Sent once. The order reaches the terminal once per idempotency key, and the answer says what actually happened.
Tickwire has no built-in size or loss limits and doesn’t judge whether a trade is a good idea. Rules like a maximum volume, a daily loss limit or trading hours belong in your hook.
3. Read the outcome correctly
Every trading answer has an outcome. Teach your agent what each one means, in its instructions
and in your code:
outcome | Meaning | What the agent should do |
|---|---|---|
applied | The terminal confirmed it (evidence says how) | Carry on |
no_change | Nothing needed doing, for example it was already closed | Carry on |
rejected | Not executed. sent: false means it never left Tickwire | Read error; don’t loop on it |
held | Your hook is holding it for approval. Nothing is sent yet | Wait. Check later; never resend |
unknown | It may have executed | Check before doing anything else |
After unknown, never retry with a new idempotency key. Ask what happened first:
GET /v1/agent/requests/{idempotency_key} (the MCP tool get_trade_status), then the positions
and orders. Repeating a call with the same key is always safe: you get the first answer back
with replayed: true, never a second order. More in Idempotency.
hook_denied, kill_switch_engaged and read_only_key are the account owner’s decisions.
The agent should report them, not try to work around them.
4. Connect an agent through MCP
The MCP server address is https://api.tickwire.io/mcp. Its tools include list_accounts,
get_agent_permissions, get_account_health, get_account_info, get_positions,
get_orders, get_quote, get_symbol_spec, get_candles, get_history, place_order,
modify_position, close_position, modify_order, cancel_order, get_trade_status and
get_audit_log. Every trading tool needs a reason, which goes into the audit log. The server
adds an idempotency key if the agent doesn’t give one (client_request_id).
Both examples below give the model only the tools it needs. That is a good habit: an agent that
only reads should not even see place_order.
Claude Agent SDK (Python)
pip install claude-agent-sdk
import asyncio, os
from claude_agent_sdk import query, ClaudeAgentOptions
options = ClaudeAgentOptions(
mcp_servers={
"tickwire": {
"type": "http",
"url": "https://api.tickwire.io/mcp",
"headers": {"Authorization": f"Bearer {os.environ['AGENT_KEY']}"},
}
},
allowed_tools=[
"mcp__tickwire__list_accounts",
"mcp__tickwire__get_account_health",
"mcp__tickwire__get_positions",
"mcp__tickwire__get_quote",
],
system_prompt=(
"You read MetaTrader accounts through Tickwire. Check account health first. "
"Times are UTC unless a field says broker time."
),
)
async def main():
async for message in query(prompt="Show the health and open positions of my first account.",
options=options):
print(message)
asyncio.run(main())
OpenAI Agents SDK (Python)
pip install openai-agents
import asyncio, os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp, create_static_tool_filter
async def main():
async with MCPServerStreamableHttp(
name="tickwire",
params={
"url": "https://api.tickwire.io/mcp",
"headers": {"Authorization": f"Bearer {os.environ['AGENT_KEY']}"},
},
tool_filter=create_static_tool_filter(
allowed_tool_names=["list_accounts", "get_account_health", "get_positions", "get_quote"]
),
) as tickwire:
agent = Agent(
name="Account reader",
instructions="You read MetaTrader accounts through Tickwire. Check account health first.",
mcp_servers=[tickwire],
)
result = await Runner.run(agent, "Show the health and open positions of my first account.")
print(result.final_output)
asyncio.run(main())
Option names in agent frameworks change between versions. If one of these fails, check the framework’s own documentation for remote MCP servers with custom headers.
5. Or call the agent REST routes yourself
If you run your own tool-calling loop, map each tool to a plain HTTP call. The reads:
| Route | What it returns |
|---|---|
GET /v1/agent/accounts | Accounts in the key’s scope |
GET /v1/agent/accounts/{id}/health | connected, disconnected or unknown, with evidence |
GET /v1/agent/accounts/{id}/account-information | Balance, equity, margin, currency |
GET /v1/agent/accounts/{id}/positions and …/orders | Open positions and pending orders |
GET /v1/agent/accounts/{id}/symbols/{symbol}/current-price | Bid and ask |
GET /v1/agent/accounts/{id}/history-deals | Deals, 24 hours by default, at most 31 days |
Trading is one route, POST /v1/agent/accounts/{id}/trade, and it needs an Idempotency-Key
and an X-Reason header. Here is a trade function that handles every outcome:
import os, uuid, requests
API = "https://api.tickwire.io"
HEADERS = {"X-Tickwire-Agent-Key": os.environ["AGENT_KEY"]}
def trade(account_id: str, order: dict, reason: str, key: str | None = None) -> dict:
"""Send one order. Reuse `key` to repeat the same order safely."""
key = key or str(uuid.uuid4())
try:
r = requests.post(f"{API}/v1/agent/accounts/{account_id}/trade", json=order, timeout=30,
headers={**HEADERS, "Idempotency-Key": key, "X-Reason": reason})
except requests.RequestException:
# No answer: the order may have gone. Ask, with the same key.
return status(key)
body = r.json()
body["idempotency_key"] = key
return body
def status(key: str) -> dict:
r = requests.get(f"{API}/v1/agent/requests/{key}", headers=HEADERS, timeout=30)
return r.json()
result = trade("<ACCOUNT_ID>",
{"actionType": "ORDER_TYPE_BUY", "symbol": "EURUSD", "volume": 0.01},
reason="example order from the guide")
match result.get("outcome"):
case "applied" | "no_change":
print("done", result.get("evidence"))
case "held":
print("waiting for approval, hold", result.get("hold_id"))
case "unknown":
print("may have executed: check", status(result["idempotency_key"]))
case _:
print("not executed:", result.get("error"), result.get("message"))
The order uses absolute prices only, in lots. actionType covers market, limit, stop and
stop-limit orders, POSITION_MODIFY, POSITION_PARTIAL, POSITION_CLOSE_ID, ORDER_MODIFY
and ORDER_CANCEL. MT4 has no stop-limit orders, and those answer not_supported_on_mt4. See
Platforms.
A 409 idempotency_in_flight answer means an earlier request with the same key is still running.
Wait for Retry-After and repeat with the same key.
6. Stop an agent, and see what it did
Engage a kill switch to stop an agent at once. Nothing else it sends will reach the broker:
curl -s -X PUT https://api.tickwire.io/v1/agent-keys/<KEY_ID>/kill-switch \
-H "Authorization: Bearer $TICKWIRE_API_KEY" \
-H "content-type: application/json" -d '{"engaged": true, "reason": "stop the agent"}'
The per-account switch is PUT /v1/agent-kill-switches/accounts/{account_id}, and both are in
the dashboard under AI agents. Everything an agent did, with its reasons and outcomes, is in
GET /v1/agent-audit (yours, as the owner) and GET /v1/agent/audit (the agent’s own log).
Checklist
- One agent key per agent, scoped to the accounts it needs, read-only until you’re sure.
- Only the tools it needs.
- Size, loss and hours rules in your pre-trade hook, not in the prompt.
- Every trade with a reason and an idempotency key.
unknownmeans check, never resend with a new key.- A kill switch you know how to reach.
- Practise against the sandbox first.
Further reading: Which AI apps can place trades on MT4 and MT5?, Risk controls for AI trading agents and Safe prompts for an AI trading assistant.