Pass an evaluation, mint a scoped API key, and let your bot trade a funded Ferm account over a clean REST API, with the same risk rails as manual trading and revocation in one click.
https://api.ferm.tradeClear a Ferm challenge to unlock a funded account backed by our capital, the same account your agent will trade.
From any page, open your profile menu and choose API keys. Pick read-only or trading scopes and grant the key exactly the accounts it may touch.
Point your bot at our REST API to read state and place orders, then reconcile with the ordered event feed. Revoke access anytime.
Six steps from a fresh account to a bot placing orders. Every sample is copy-paste ready in curl, Python, or JavaScript. Pick a language once and the whole page follows.
Agent keys attach to real Ferm accounts, so you need at least one account first. Any account in an evaluation (active), or already funded (funded / live), can trade through the API. You can develop your bot against an evaluation account and let it earn the funded one.
Log in on the web, open your profile menu (top right) and choose API keys, then Mint a key. You will pick four things:
account:read to read, order:write to trade. Start read-only if you are still testing.The full key is displayed once. Store it in an environment variable or a secrets manager, never in code or version control. Lost keys cannot be recovered, only replaced.
# The full key is shown exactly once, at creation. Copy it then.
# Anatomy: frm_agent_<key id: 16 hex>_<secret: 64 hex>
export FERM_AGENT_KEY="frm_agent_1a2b3c4d5e6f7a8b_<64-hex-secret>"
List the accounts your key was granted. If this returns your account, auth, scopes, and grants are all wired up correctly.
curl https://api.ferm.trade/v1/agent/accounts \
-H "Authorization: Bearer $FERM_AGENT_KEY"
{
"status": true,
"data": {
"accounts": [
{
"id": "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10",
"status": "funded",
"balanceCents": 10000000,
"highWaterMarkCents": 10250000,
"profitShareBps": 9000,
"createdAt": "2026-06-02T15:04:05.000Z"
}
]
}
}
import os, requests
API = "https://api.ferm.trade"
HEADERS = {"Authorization": f"Bearer {os.environ['FERM_AGENT_KEY']}"}
res = requests.get(f"{API}/v1/agent/accounts", headers=HEADERS)
print(res.json())
{
"status": true,
"data": {
"accounts": [
{
"id": "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10",
"status": "funded",
"balanceCents": 10000000,
"highWaterMarkCents": 10250000,
"profitShareBps": 9000,
"createdAt": "2026-06-02T15:04:05.000Z"
}
]
}
}
const API = "https://api.ferm.trade";
const HEADERS = { Authorization: `Bearer ${process.env.FERM_AGENT_KEY}` };
const res = await fetch(`${API}/v1/agent/accounts`, { headers: HEADERS });
console.log(await res.json());
{
"status": true,
"data": {
"accounts": [
{
"id": "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10",
"status": "funded",
"balanceCents": 10000000,
"highWaterMarkCents": 10250000,
"profitShareBps": 9000,
"createdAt": "2026-06-02T15:04:05.000Z"
}
]
}
}
10000000 is $100,000.00.401? Check the Authorization header made it through your HTTP client. Getting an empty list? The key has no account grants yet.One call returns everything your strategy needs to make a decision: equity, buying power, open positions with live mark prices, working limit orders, and how far the account is from each risk limit.
export ACCOUNT_ID="1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10"
curl https://api.ferm.trade/v1/agent/accounts/$ACCOUNT_ID/state \
-H "Authorization: Bearer $FERM_AGENT_KEY"
{
"status": true,
"data": {
"tradingState": {
"account": { "id": "1f0c9c7e-…", "status": "funded", "balanceCents": 10000000 },
"currentEquityCents": 10038200,
"buyingPowerCents": 9820000,
"positions": [
{
"id": "6c1d1b2e-4a3f-4f7e-8a9b-2c3d4e5f6a7b",
"symbol": "BTC",
"side": "buy",
"quantity": "0.05",
"entryPrice": 67210.5,
"markPrice": 67480.0,
"unrealizedPnlCents": 1348
}
],
"pendingOrders": [],
"riskThresholds": { "maxDrawdownCents": 800000, "dailyLossCents": 400000 },
"breached": false,
"progress": { "profitBps": 382, "targetBps": 1000 }
}
}
}
ACCOUNT_ID = "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10"
res = requests.get(f"{API}/v1/agent/accounts/{ACCOUNT_ID}/state", headers=HEADERS)
print(res.json())
{
"status": true,
"data": {
"tradingState": {
"account": { "id": "1f0c9c7e-…", "status": "funded", "balanceCents": 10000000 },
"currentEquityCents": 10038200,
"buyingPowerCents": 9820000,
"positions": [
{
"id": "6c1d1b2e-4a3f-4f7e-8a9b-2c3d4e5f6a7b",
"symbol": "BTC",
"side": "buy",
"quantity": "0.05",
"entryPrice": 67210.5,
"markPrice": 67480.0,
"unrealizedPnlCents": 1348
}
],
"pendingOrders": [],
"riskThresholds": { "maxDrawdownCents": 800000, "dailyLossCents": 400000 },
"breached": false,
"progress": { "profitBps": 382, "targetBps": 1000 }
}
}
}
const ACCOUNT_ID = "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10";
const res = await fetch(`${API}/v1/agent/accounts/${ACCOUNT_ID}/state`, { headers: HEADERS });
console.log(await res.json());
{
"status": true,
"data": {
"tradingState": {
"account": { "id": "1f0c9c7e-…", "status": "funded", "balanceCents": 10000000 },
"currentEquityCents": 10038200,
"buyingPowerCents": 9820000,
"positions": [
{
"id": "6c1d1b2e-4a3f-4f7e-8a9b-2c3d4e5f6a7b",
"symbol": "BTC",
"side": "buy",
"quantity": "0.05",
"entryPrice": 67210.5,
"markPrice": 67480.0,
"unrealizedPnlCents": 1348
}
],
"pendingOrders": [],
"riskThresholds": { "maxDrawdownCents": 800000, "dailyLossCents": 400000 },
"breached": false,
"progress": { "profitBps": 382, "targetBps": 1000 }
}
}
}
positions[].id is the id you use to close a position or update its brackets; pendingOrders[].id is the id you use to cancel a working order.riskThresholds and progress track the same numbers that score your evaluation. Use them to size positions defensively.Generate a fresh UUID as clientOrderId, then send the order. If your connection drops mid-request, retry with the same UUID. You will get the original result back, never a second fill.
curl -X POST https://api.ferm.trade/v1/agent/accounts/$ACCOUNT_ID/orders \
-H "Authorization: Bearer $FERM_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientOrderId": "8f2b7c1e-0d4a-4c9b-9a1e-1c2d3e4f5a6b",
"symbol": "BTC",
"side": "buy",
"volume": "0.05",
"orderType": "market",
"takeProfitPercent": 5,
"stopLossPercent": 3
}'
{
"status": true,
"data": {
"order": {
"status": "filled",
"fillPrice": 67480.0,
"position": {
"id": "6c1d1b2e-4a3f-4f7e-8a9b-2c3d4e5f6a7b",
"symbol": "BTC",
"side": "buy",
"quantity": "0.05"
}
}
}
}
import uuid
order = {
"clientOrderId": str(uuid.uuid4()), # retry with the same id - never double-fills
"symbol": "BTC",
"side": "buy",
"volume": "0.05",
"orderType": "market",
"takeProfitPercent": 5,
"stopLossPercent": 3,
}
res = requests.post(f"{API}/v1/agent/accounts/{ACCOUNT_ID}/orders",
headers=HEADERS, json=order)
print(res.json())
{
"status": true,
"data": {
"order": {
"status": "filled",
"fillPrice": 67480.0,
"position": {
"id": "6c1d1b2e-4a3f-4f7e-8a9b-2c3d4e5f6a7b",
"symbol": "BTC",
"side": "buy",
"quantity": "0.05"
}
}
}
}
const order = {
clientOrderId: crypto.randomUUID(), // retry with the same id - never double-fills
symbol: "BTC",
side: "buy",
volume: "0.05",
orderType: "market",
takeProfitPercent: 5,
stopLossPercent: 3,
};
const res = await fetch(`${API}/v1/agent/accounts/${ACCOUNT_ID}/orders`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify(order),
});
console.log(await res.json());
{
"status": true,
"data": {
"order": {
"status": "filled",
"fillPrice": 67480.0,
"position": {
"id": "6c1d1b2e-4a3f-4f7e-8a9b-2c3d4e5f6a7b",
"symbol": "BTC",
"side": "buy",
"quantity": "0.05"
}
}
}
}
status: "filled" with the new position; limit orders return status: "pending" with the working order.Fills, cancels, bracket hits, and breaches all land on an ordered event feed. Persist the last sequence you processed and poll with sinceSequence. After a crash or disconnect you replay exactly what you missed.
# Fetch everything after the last sequence you processed
curl "https://api.ferm.trade/v1/agent/events?accountId=$ACCOUNT_ID&sinceSequence=10431&limit=100" \
-H "Authorization: Bearer $FERM_AGENT_KEY"
{
"status": true,
"data": {
"events": [
{
"eventId": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"sequence": 10432,
"type": "order_filled",
"occurredAt": "2026-07-12T08:14:22.101Z",
"accountId": "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10",
"clientOrderId": "8f2b7c1e-0d4a-4c9b-9a1e-1c2d3e4f5a6b",
"data": { }
}
]
}
}
# Fetch everything after the last sequence you processed
params = {"accountId": ACCOUNT_ID, "sinceSequence": 10431, "limit": 100}
res = requests.get(f"{API}/v1/agent/events", headers=HEADERS, params=params)
print(res.json())
{
"status": true,
"data": {
"events": [
{
"eventId": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"sequence": 10432,
"type": "order_filled",
"occurredAt": "2026-07-12T08:14:22.101Z",
"accountId": "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10",
"clientOrderId": "8f2b7c1e-0d4a-4c9b-9a1e-1c2d3e4f5a6b",
"data": { }
}
]
}
}
// Fetch everything after the last sequence you processed
const params = new URLSearchParams({
accountId: ACCOUNT_ID,
sinceSequence: "10431",
limit: "100",
});
const res = await fetch(`${API}/v1/agent/events?${params}`, { headers: HEADERS });
console.log(await res.json());
{
"status": true,
"data": {
"events": [
{
"eventId": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"sequence": 10432,
"type": "order_filled",
"occurredAt": "2026-07-12T08:14:22.101Z",
"accountId": "1f0c9c7e-8f4b-4c6e-9d2a-7b1e3a5c9d10",
"clientOrderId": "8f2b7c1e-0d4a-4c9b-9a1e-1c2d3e4f5a6b",
"data": { }
}
]
}
}
eventId before acting.One small Python file that proves the whole loop: it finds a granted account, checks it is healthy, places one bracketed order idempotently, then tails the event feed. Copy it, export your key, run it.
#!/usr/bin/env python3
"""Ferm starter bot: reads state, places one bracketed order, tails events.
Run it: export FERM_AGENT_KEY="frm_agent_..." && python3 bot.py
Needs: pip install requests
"""
import os, time, uuid, requests
API = "https://api.ferm.trade"
HEADERS = {"Authorization": f"Bearer {os.environ['FERM_AGENT_KEY']}"}
def get(path, **params):
res = requests.get(f"{API}{path}", headers=HEADERS, params=params, timeout=10)
res.raise_for_status()
return res.json()["data"]
# 1. Find an account this key can trade
account = get("/v1/agent/accounts")["accounts"][0]
account_id = account["id"]
print(f"trading account {account_id} ({account['status']})")
# 2. Look before you leap: never trade a breached account
state = get(f"/v1/agent/accounts/{account_id}/state")["tradingState"]
if state["breached"]:
raise SystemExit("account is breached - read-only until further notice")
print(f"equity ${state['currentEquityCents'] / 100:,.2f}")
# 3. Place one small bracketed market order, idempotently
order = {
"clientOrderId": str(uuid.uuid4()), # reuse on retry: never double-fills
"symbol": "BTC",
"side": "buy",
"volume": "0.01",
"orderType": "market",
"takeProfitPercent": 5,
"stopLossPercent": 3,
}
res = requests.post(f"{API}/v1/agent/accounts/{account_id}/orders",
headers=HEADERS, json=order, timeout=10)
res.raise_for_status()
print("order:", res.json()["data"]["order"]["status"])
# 4. Tail the event feed - real bots persist `since` across restarts
since, seen = 0, set()
while True:
feed = get("/v1/agent/events", accountId=account_id,
sinceSequence=since, limit=100)
for event in feed["events"]:
since = event["sequence"]
if event["eventId"] in seen:
continue # delivery is at-least-once - dedupe by eventId
seen.add(event["eventId"])
print(f"[{event['sequence']}] {event['type']}")
time.sleep(2)
requests. No SDK, no framework. The whole API fits in plain HTTP.sinceSequence somewhere durable so a restart replays exactly the events they missed.Everything above gets you trading; everything below is what you come back for: exact fields, exact errors, exact limits.
Every request carries a bearer token. Agent endpoints are header-only: they never read your browser session, so they sit entirely outside cookie and CSRF machinery.
# format: frm_agent_<key id>_<secret>
#
# key id 16 hex chars - identifies the key; visible in your dashboard
# secret 64 hex chars - stored only as a hash; revealed exactly once
frm_agent_1a2b3c4d5e6f7a8b_9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b
curl https://api.ferm.trade/v1/agent/accounts \
-H "Authorization: Bearer frm_agent_<keyId>_<secret>"
401 unauthorized; wrong or unknown token → 401 invalid_api_key; revoked → 401 api_key_revoked; expired → 401 api_key_expired.| Scope | Unlocks | Description |
|---|---|---|
account:read | All read endpoints | Read account state, positions, trade history, and the event feed. |
order:write | All trading endpoints | Place and cancel orders, close positions, and adjust brackets. |
stream:subscribe | Coming soon | Reserved for the low-latency WebSocket event stream. Selectable soon; not yet active. |
Keys are default-deny: a key can only see and trade the accounts you explicitly grant it.
404 account_not_found. The API never confirms whether an account exists to a key that cannot access it.Base URL https://api.ferm.trade. All requests and responses are JSON; request bodies are capped at 100 KB; timestamps are ISO 8601 UTC.
| Method | Path & description | Scope |
|---|---|---|
| GET | /v1/agent/accountsList the accounts this key has been granted. | account:read |
| GET | /v1/agent/accounts/:id/stateLive snapshot: balance, equity, buying power, open positions, working orders, risk thresholds, and evaluation progress. | account:read |
| GET | /v1/agent/accounts/:id/tradesPaginated closed-trade history (limit 1–500, cursor). | account:read |
| GET | /v1/agent/eventsOrdered, replayable event feed (accountId, sinceSequence, limit). | account:read |
| POST | /v1/agent/accounts/:id/ordersPlace a market or limit order with optional take-profit / stop-loss brackets. | order:write |
| DELETE | /v1/agent/accounts/:id/orders/:orderIdCancel a working limit order. :orderId comes from state.pendingOrders[].id. | order:write |
| POST | /v1/agent/accounts/:id/positions/:tradeId/closeClose an open position at market. :tradeId comes from state.positions[].id. Optional body { "closePercent": 1–99 } closes only part of the position. | order:write |
| PATCH | /v1/agent/accounts/:id/positions/:tradeId/bracketsUpdate take-profit / stop-loss on an open position. | order:write |
POST /v1/agent/accounts/:id/orders places a market or limit order. The schema is strict: unknown fields are rejected rather than ignored.
| Field | Type | Required | Notes |
|---|---|---|---|
clientOrderId | string (UUID) | Required | Your idempotency token. Generate a fresh UUID per order; retries with the same id return the original result instead of filling twice. |
symbol | string | Required | Instrument symbol, e.g. BTC, EURUSD, US500, XAUUSD. Common aliases (BTCUSD, SPY, GOLD) are normalized automatically. Unknown symbols return 400 unsupported_symbol. |
side | "buy" | "sell" | Required | Direction of the order. |
volume | string (decimal) | Required | Quantity as a decimal string with up to 8 decimal places, e.g. "0.05". Checked against the instrument’s min / max / step; out of range returns 400 invalid_order_size. |
orderType | "market" | "limit" | Optional | Defaults to "market". |
limitPrice | string (decimal) | Limit only | Required when orderType is "limit"; not allowed on market orders. |
takeProfitPrice | string (decimal) | Optional | Absolute take-profit price. Use this or takeProfitPercent, not both. |
takeProfitPercent | integer 1–100 | Optional | Take-profit as a percent distance from entry. |
stopLossPrice | string (decimal) | Optional | Absolute stop-loss price. Use this or stopLossPercent, not both. |
stopLossPercent | integer 1–100 | Optional | Stop-loss as a percent distance from entry. |
Tradable instruments span crypto (BTC, ETH, SOL, …), 25+ FX pairs (EURUSD, GBPJPY, …), and index / commodity CFDs (US500, NAS100, XAUUSD, USOIL, …). See the full list on the markets page.
Before an order reaches the matching engine it must clear the same rails as a manual trade: the account must be in a tradable state (active, funded, or live, otherwise 409 account_not_tradable), have no withdrawal in flight (409 trading_withdrawal_pending), and pass sizing, margin, and risk checks in the engine itself.
clientOrderId is your safety net for retries. What happens when the same UUID is sent twice:
| Existing state | Result of the retry |
|---|---|
| Same clientOrderId, position already open | 200: returns the existing position. No second fill. |
| Same clientOrderId, limit order still working | 200: returns the existing pending order. |
| Same clientOrderId, order already closed or cancelled | 400: "client order id has already been used". Generate a new UUID. |
Cancel, close, and bracket updates address server-side ids, so read them from the state endpoint first. These calls are not idempotent; check state before retrying.
# Cancel a working limit order - id from state.pendingOrders[].id
curl -X DELETE https://api.ferm.trade/v1/agent/accounts/$ACCOUNT_ID/orders/$ORDER_ID \
-H "Authorization: Bearer $FERM_AGENT_KEY"
# Close an open position at market - id from state.positions[].id
# Add -d '{ "closePercent": 50 }' (with Content-Type: application/json) for a partial close
curl -X POST https://api.ferm.trade/v1/agent/accounts/$ACCOUNT_ID/positions/$TRADE_ID/close \
-H "Authorization: Bearer $FERM_AGENT_KEY"
# Move the stop to break-even on an open position
curl -X PATCH https://api.ferm.trade/v1/agent/accounts/$ACCOUNT_ID/positions/$TRADE_ID/brackets \
-H "Authorization: Bearer $FERM_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{ "stopLossPrice": "67210.50" }'
# Cancel a working limit order - id from state.pendingOrders[].id
requests.delete(f"{API}/v1/agent/accounts/{ACCOUNT_ID}/orders/{order_id}",
headers=HEADERS)
# Close an open position at market - id from state.positions[].id
requests.post(f"{API}/v1/agent/accounts/{ACCOUNT_ID}/positions/{trade_id}/close",
headers=HEADERS)
# Move the stop to break-even on an open position
requests.patch(f"{API}/v1/agent/accounts/{ACCOUNT_ID}/positions/{trade_id}/brackets",
headers=HEADERS, json={"stopLossPrice": "67210.50"})
// Cancel a working limit order - id from state.pendingOrders[].id
await fetch(`${API}/v1/agent/accounts/${ACCOUNT_ID}/orders/${orderId}`, {
method: "DELETE",
headers: HEADERS,
});
// Close an open position at market - id from state.positions[].id
await fetch(`${API}/v1/agent/accounts/${ACCOUNT_ID}/positions/${tradeId}/close`, {
method: "POST",
headers: HEADERS,
});
// Move the stop to break-even on an open position
await fetch(`${API}/v1/agent/accounts/${ACCOUNT_ID}/positions/${tradeId}/brackets`, {
method: "PATCH",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ stopLossPrice: "67210.50" }),
});
takeProfitPrice / stopLossPrice (decimal strings) or takeProfitPercent / stopLossPercent (integers 1–100). All fields are optional; send only what you want to change.GET /v1/agent/events is an ordered, replayable log of everything that happens on an account. It is the source of truth your bot reconciles against.
| Query param | Type | Notes |
|---|---|---|
accountId | UUID, required | The granted account to read events for. |
sinceSequence | integer, default 0 | Returns events with sequence strictly greater than this value. |
limit | 1–500, default 100 | Maximum events per response. |
| Type | Fires when |
|---|---|
order_accepted | A limit order was accepted and is working. |
order_filled | A market order filled, or a working limit order executed. |
order_cancelled | A working order was cancelled. |
position_closed | An open position was fully closed. |
position_partially_closed | Part of a position was closed. |
position_brackets_updated | Take-profit / stop-loss on a position changed. |
account_breached | The account hit a risk limit; trading is now blocked. |
evaluation_passed | The evaluation profit target was reached. |
phase_advanced | The account moved to its next phase. |
sequence is monotonic per account. A gap means you have more to fetch, never that something was skipped.eventId.clientOrderId, so you can match fills to requests exactly.Every response is wrapped in the same envelope, so a single check on status tells you whether data or error is present.
{
"status": true,
"data": { }
}
{
"status": false,
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the order:write scope"
}
}
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | No Authorization header was sent. |
| 401 | invalid_api_key | Malformed token, unknown key id, or wrong secret. |
| 401 | api_key_revoked | The key was revoked from the dashboard. |
| 401 | api_key_expired | The key is past its expiry date. |
| 403 | insufficient_scope | The key lacks the scope this endpoint requires. |
| 404 | account_not_found | The account does not exist or is not granted to this key. |
| 400 | validation_failed | The body failed validation; error.details lists each bad field. |
| 400 | unsupported_symbol | The symbol is not in the instrument catalog. |
| 400 | invalid_order_size | Volume is outside the instrument’s min / max / step. |
| 409 | account_not_tradable | The account is breached, closed, or otherwise not in a tradable state. |
| 409 | trading_withdrawal_pending | A pending withdrawal blocks new orders until it settles. |
| 429 | rate_limit_exceeded | Too many requests; back off and retry. |
| 502 | trading_engine_error | The trading engine returned an unexpected error. Safe to retry reads. |
| 503 | trading_engine_unavailable | The trading engine is unreachable. Retry with backoff. |
Validation failures include an error.details array naming each offending field. Errors surfaced from inside the trading engine (for example “insufficient buying power”) return a human-readable error.message without a machine code.
Limits are generous for polling architectures: a bot reading state once per second uses a tenth of its budget.
| Limit | Keyed by | Applies to |
|---|---|---|
| 600 requests / minute | Per API key | All read endpoints |
| 600 requests / minute | Per API key | All order endpoints |
| 30 failed auths / 10 minutes | Per IP | Requests with bad credentials |
RateLimit-* headers so clients can pace themselves before hitting 429.429 rate_limit_exceeded, back off and retry. Reads are always safe to retry, and orders are safe to retry with the same clientOrderId.Automated trading should never mean handing over the keys to everything. Every control below is on from the first request.
Every key is limited to the scopes you pick (read-only, trading, or both) and only the accounts you explicitly grant it.
Kill a key instantly from the dashboard. In-flight requests stop the moment it is revoked; no session lingers.
Agent orders run through the identical sizing, account-status, and risk checks as the web terminal. No shortcuts.
If an account breaches its risk limits, trading scopes stop working automatically while reads keep flowing for reconciliation.
Every order carries a clientOrderId you generate, so a retried request after a dropped connection never double-fills.
Secrets are shown once at creation and stored only as a hash. We literally cannot recover a lost key; you rotate it.
Use of the Agent API is authorized programmatic access under Section 6.2 of the Terms and Conditions. Everything your keys do is attributed to your account, and the Acceptable Use Policy and all trading rules apply exactly as they do to manual trading.
Get funded, generate a key, and keep up to 90% of what your strategy makes.