Bring your own bot
Read Solana mainnet-beta market quotes and answer with buy, sell or hold for a NOCREN Practice bot. Custom strategies use the same risk limits and Practice leaderboard as Rules bots.
1. Get a key
On Build a bot, choose My own code, set your limits and sign with your wallet. The API is a $NOCREN holder feature. Copy the key when it appears. The server stores its hash, and rotating it from the bot’s page revokes the old key immediately.
Holder verification is not open yet. You can start with the built-in Practice strategies.
2. Read the market
curl -sS "https://sherwood-replica.vercel.app/api/v1/state" \
-H "Authorization: Bearer ${NOCREN_API_KEY}"State includes simulated USD cash, estimated value, open positions, risk limits, next action time and Solana mainnet-beta quotes. Each quote identifies its mint and SPL decimals. An empty quote list means there are no fresh prices available for new entries.
{
"handle": "example-bot",
"mode": "practice",
"market": "solana",
"cluster": "mainnet-beta",
"status": "running",
"cashUsd": 1000,
"valueUsd": 1000,
"positions": [],
"quotes": [],
"limits": {
"maxTradeUsd": 40, "dailyLimit": 30, "intervalMinutes": 6,
"takeProfitPct": 30, "stopLossPct": 12, "maxHoldMinutes": 360,
"maxLossPct": 50, "tradesToday": 0, "tradesRemaining": 30,
"entryQuotaResetsAt": null
},
"nextActionAt": "2026-11-11T00:00:00.000Z",
"asOf": "2026-11-11T00:00:00.000Z"
}3. Act
curl -sS -X POST "https://sherwood-replica.vercel.app/api/v1/act" \
-H "Authorization: Bearer ${NOCREN_API_KEY}" -H "Content-Type: application/json" \
-d "{\"action\":\"hold\",\"reason\":\"Waiting for a signal.\",\"requestId\":\"$(python3 -c 'import uuid; print(uuid.uuid4())')\"}"actionacceptsbuy,sellorhold. Thetokenfield is the exact, case-sensitive Solana mint from a quote or position. Sell closes the entire position in that mint.- For buy, set
amountUsdbefore fees. It is capped by your per-trade limit and by available cash after the fee. - Use
reasonfor the public explanation.detailadds a longer explanation that readers can expand. - Optional
takeProfitPct,stopLossPctandmaxHoldMinutesoverride the bot’s default exit rules for this position. - Send a fresh UUID as
requestIdfor each new decision. Retry an identical request with the same ID if its response was lost.
Successful responses include action, trade (or null for hold), cashUsd and nextActionAt. Errors return error and code.
| HTTP | Code |
|---|---|
| 400 | invalid_parameter |
| 401 | invalid_key |
| 403 | tier_required |
| 404 | position_not_found |
| 409 | bot_stopped, stale_quote, idempotency_conflict, risk_stop |
| 422 | insufficient_funds, trade_limit, daily_limit |
| 429 | decision_interval |
| 503 | live_unavailable, storage_unavailable |
Rules
- One decision per interval, including hold. Read
nextActionAtbefore sending another action. - Practice starts with $1,000. Each buy and sell incurs a 1% simulated cost.
tradesTodayandtradesRemainingcount new entries. The quota resets at midnight UTC. Once the quota is reached,entryQuotaResetsAtgives the reset time. Exits remain available.- New orders require fresh market data. Existing exits are evaluated against fresh quotes.
- A drawdown stop prevents new entries. Existing positions continue following their exit rules.
A full loop in Python
import json, os, time, uuid
from datetime import datetime, timezone
from urllib.error import HTTPError
from urllib.request import Request, urlopen
BASE = "https://sherwood-replica.vercel.app/api/v1"
HEADERS = {
"Authorization": "Bearer " + os.environ["NOCREN_API_KEY"],
"Content-Type": "application/json",
}
def call(path, data=None):
body = json.dumps(data).encode() if data is not None else None
with urlopen(Request(BASE + path, data=body, headers=HEADERS), timeout=20) as response:
return json.load(response)
def decide(state):
limits = state["limits"]
amount = round(min(25, limits["maxTradeUsd"], state["cashUsd"] / 1.01), 2)
held = {position["token"] for position in state["positions"]}
for quote in state["quotes"]:
if (quote["token"] not in held and amount >= 1
and limits["tradesRemaining"] > 0
and quote["change1h"] > 8 and quote["volume1h"] > 50000):
return {
"action": "buy", "token": quote["token"], "amountUsd": amount,
"takeProfitPct": limits["takeProfitPct"],
"stopLossPct": limits["stopLossPct"],
"maxHoldMinutes": limits["maxHoldMinutes"],
"reason": f"{quote['symbol']} is up {quote['change1h']:.1f}% this hour.",
}
return {"action": "hold", "reason": "No eligible quote meets my entry criteria."}
while True:
try:
state = call("/state")
if state["mode"] != "practice":
raise SystemExit("This example only runs on Practice bots.")
if state["status"] != "running":
break
due = state.get("nextActionAt")
remaining = (datetime.fromisoformat(due.replace("Z", "+00:00"))
- datetime.now(timezone.utc)).total_seconds() if due else 0
if remaining <= 0:
print(call("/act", {**decide(state), "requestId": str(uuid.uuid4())}))
time.sleep(max(15, remaining))
except HTTPError as error:
print(error.code, error.read().decode())
if error.code in (400, 401, 403):
break
time.sleep(max(30, int(error.headers.get("Retry-After") or 0)))Set NOCREN_API_KEY in the environment and run with Python 3. The example acts only on a Practice bot.