Kestrel Agent API
Kestrel is a five-minute Bitcoin prediction market settled on Solana via MagicBlock Ephemeral Rollups. This API lets any agent with a Solana wallet trade on Kestrel markets without writing Anchor code or knowing the program internals.
Every mutating endpoint returns an unsigned base64 transaction. Your agent decodes it, signs with its keypair, and submits to the indicated RPC. Read endpoints return plain JSON with no signing required.
Base URL
https://kestrel.priyanshpatel.com/api/v1Quickstart (5 API calls)
Everything an agent needs to go from zero to a live bet — no SDK, no Anchor dependency, just a wallet keypair and fetch.
Find an open market
GET /api/v1/markets?status=open
Register your agent (once per wallet)
POST /api/v1/agent/register
{ "pubkey": "<your wallet pubkey>" }Deposit USDC
POST /api/v1/agent/deposit
{ "pubkey": "...", "amount": 1000000 }Place a bet
POST /api/v1/bet/place
{ "pubkey": "...", "marketId": 42, "side": "yes", "amount": 200000 }Each response gives you a base64 tx to sign + send
// pseudo-code
const { transaction, erRpcUrl } = await res.json();
const tx = Transaction.from(Buffer.from(transaction, "base64"));
tx.sign(myKeypair);
await connection.sendRawTransaction(tx.serialize());Agent lifecycle
A full agent lifecycle from first registration through withdrawal:
POST /agent/registerbaseOne-time setup. Creates your on-chain agent PDA.
POST /agent/depositbaseTransfer USDC into the program vault. Balance credited to your agent.
GET /markets?status=openDiscover the currently open five-minute window.
POST /bet/placeERBuy YES or NO shares on the Ephemeral Rollup. Near-instant.
GET /agent/:pubkeyCheck your balance and open positions at any time.
POST /bet/cancelERExit your entire position mid-window if you change your mind.
POST /agent/withdrawbasePull USDC back to your wallet. Agent must be undelegated from ER first (scheduler handles this automatically after market close).
/api/v1/marketsList all markets. Filter by status to find the active window.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | no | Filter: pending | open | halted | closed | settled |
| limit | number | no | Max results 1–100 (default 20) |
Example
GET /api/v1/markets?status=open&limit=5
{
"markets": [
{
"marketId": 42,
"marketPubkey": "H7xY...Zk3m",
"status": "open",
"strikePrice": "9432150000000",
"closePrice": null,
"winner": null,
"openTs": 1714120800,
"closeTs": 1714121100
}
]
}/api/v1/markets/:idSingle market detail by integer ID.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | number | yes | Integer market ID from the markets list |
Example
GET /api/v1/markets/42
{
"marketId": 42,
"marketPubkey": "H7xY...Zk3m",
"status": "closed",
"strikePrice": "9432150000000",
"closePrice": "9441200000000",
"winner": "yes",
"openTs": 1714120800,
"closeTs": 1714121100
}/api/v1/agent/:pubkeyOn-chain agent profile: balance, policy, and open positions.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Base58 wallet public key |
Example
GET /api/v1/agent/9xQ3...Wm7p
{
"ownerPubkey": "9xQ3...Wm7p",
"agentPda": "B2rk...Ux9v",
"balance": "800000",
"deposited": "1000000",
"status": "Active",
"policy": {
"maxStakePerWindow": "500000",
"maxOpenPositions": 8,
"paused": false
},
"positions": [
{
"marketId": 42,
"yesShares": "398212",
"noShares": "0",
"stake": "200000",
"settled": false
}
],
"role": "trader",
"label": null,
"lastEventAt": "2024-04-26T09:00:01Z"
}/api/v1/agent/registersubmit to: base layerBuild a register_agent transaction. Call once per wallet. Submit to the base-layer RPC.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Your wallet public key (base58) |
| maxStakePerWindow | number | no | Token lamports per-bet cap (default 500,000) |
| maxOpenPositions | number | no | Max concurrent open positions (default 8, max 16) |
Example
{
"pubkey": "9xQ3...Wm7p",
"maxStakePerWindow": 500000
}{
"transaction": "AQAAAA...base64...",
"agentPda": "B2rk...Ux9v",
"baseRpcUrl": "https://api.devnet.solana.com",
"note": "Sign this transaction with your wallet and submit it to baseRpcUrl"
}/api/v1/agent/depositsubmit to: base layerBuild a deposit transaction. Transfers USDC from your ATA into the program vault. Submit to the base-layer RPC.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Your wallet public key (base58) |
| amount | number | yes | USDC in token lamports (e.g. 1,000,000 = 1 USDC at 6 decimals) |
Example
{
"pubkey": "9xQ3...Wm7p",
"amount": 1000000
}{
"transaction": "AQAAAA...base64...",
"baseRpcUrl": "https://api.devnet.solana.com",
"usdcMint": "4zMMC...MkGb",
"userAta": "FjqQ...7y2t",
"note": "Sign this transaction with your wallet and submit it to baseRpcUrl. Your USDC ATA must already exist."
}/api/v1/agent/withdrawsubmit to: base layerBuild a withdraw transaction. Pulls USDC back to your wallet. Submit to the base-layer RPC. Agent must be undelegated from the ER first.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Your wallet public key (base58) |
| amount | number | yes | USDC in token lamports to withdraw |
Example
{
"pubkey": "9xQ3...Wm7p",
"amount": 800000
}{
"transaction": "AQAAAA...base64...",
"baseRpcUrl": "https://api.devnet.solana.com",
"usdcMint": "4zMMC...MkGb",
"userAta": "FjqQ...7y2t",
"treasuryAta": "Hm3p...9wQs",
"note": "Sign and submit to baseRpcUrl. Agent must be undelegated from the ER first."
}/api/v1/bet/placesubmit to: Ephemeral RollupBuild a place_bet transaction. Buys YES or NO shares on the open market. Submit to the Ephemeral Rollup RPC for near-instant settlement.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Your wallet public key (base58) |
| marketId | number | yes | Integer market ID (from GET /markets?status=open) |
| side | "yes" | "no" | yes | YES = price closes above strike, NO = below |
| amount | number | yes | Collateral in token lamports. Must be ≤ policy.maxStakePerWindow. |
Example
{
"pubkey": "9xQ3...Wm7p",
"marketId": 42,
"side": "yes",
"amount": 200000
}{
"transaction": "AQAAAA...base64...",
"erRpcUrl": "https://devnet-as.magicblock.app/",
"marketPubkey": "H7xY...Zk3m",
"oracleFeed": "71wtT...51sr",
"note": "Sign this transaction and submit it to erRpcUrl. This runs on the Ephemeral Rollup for instant settlement."
}/api/v1/bet/cancelsubmit to: Ephemeral RollupBuild a cancel_bet transaction. Closes your entire position in a market and returns collateral to your agent balance. Submit to the ER RPC.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Your wallet public key (base58) |
| marketId | number | yes | Integer market ID |
Example
{
"pubkey": "9xQ3...Wm7p",
"marketId": 42
}{
"transaction": "AQAAAA...base64...",
"erRpcUrl": "https://devnet-as.magicblock.app/",
"marketPubkey": "H7xY...Zk3m",
"note": "Sign and submit to erRpcUrl while the market is still open."
}/api/v1/bet/closesubmit to: Ephemeral RollupBuild a close_position transaction. Partially sells shares of one side back to the AMM at the current market price. Submit to the ER RPC.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| pubkey | string | yes | Your wallet public key (base58) |
| marketId | number | yes | Integer market ID |
| side | "yes" | "no" | yes | Which side's shares to sell |
| shares | number | yes | Number of shares to sell (must be ≤ held shares) |
Example
{
"pubkey": "9xQ3...Wm7p",
"marketId": 42,
"side": "yes",
"shares": 200000
}{
"transaction": "AQAAAA...base64...",
"erRpcUrl": "https://devnet-as.magicblock.app/",
"marketPubkey": "H7xY...Zk3m",
"note": "Sign and submit to erRpcUrl while the market is still open. Partial closes are allowed."
}/api/v1/tx/sendOptional relay: broadcast a signed transaction to the base or ER RPC without managing the URL yourself.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| transaction | string | yes | Base64 SIGNED transaction bytes |
| cluster | "base" | "er" | no | Which chain to submit to (default "er") |
Example
{
"transaction": "AQAAAA...signed...",
"cluster": "er"
}{
"signature": "4fGZ...base58sig...",
"cluster": "er",
"rpcUrl": "https://devnet-as.magicblock.app/",
"explorerUrl": "https://explorer.solana.com/tx/4fGZ...?cluster=devnet"
}TypeScript — full agent loop
A minimal but complete agent that finds an open market, places a YES bet, and logs the result. No Anchor dependency required.
import {
Connection,
Keypair,
Transaction,
} from "@solana/web3.js";
import bs58 from "bs58";
const BASE_URL = "https://<your-kestrel-host>/api/v1";
// Load your keypair however you manage secrets.
const secret = JSON.parse(process.env.AGENT_SECRET_KEY!);
const keypair = Keypair.fromSecretKey(Uint8Array.from(secret));
const pubkey = keypair.publicKey.toBase58();
async function call(path: string, body?: object) {
const res = await fetch(`${BASE_URL}${path}`, {
method: body ? "POST" : "GET",
headers: { "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`${path} ${res.status}: ${await res.text()}`);
return res.json();
}
async function signAndSend(
transaction: string,
rpcUrl: string,
): Promise<string> {
const connection = new Connection(rpcUrl, "confirmed");
const tx = Transaction.from(Buffer.from(transaction, "base64"));
tx.sign(keypair);
return connection.sendRawTransaction(tx.serialize());
}
async function main() {
// 1. Register (safe to call multiple times — will fail if already registered).
try {
const reg = await call("/agent/register", { pubkey });
const sig = await signAndSend(reg.transaction, reg.baseRpcUrl);
console.log("registered", sig);
} catch (e) {
console.log("register skipped:", (e as Error).message.slice(0, 60));
}
// 2. Deposit 1 USDC.
const dep = await call("/agent/deposit", { pubkey, amount: 1_000_000 });
const depSig = await signAndSend(dep.transaction, dep.baseRpcUrl);
console.log("deposited", depSig);
// 3. Find an open market.
const { markets } = await call("/markets?status=open");
if (!markets.length) { console.log("no open market"); return; }
const market = markets[0];
console.log("open market:", market.marketId, "closes at", market.closeTs);
// 4. Place a YES bet of 200,000 lamports.
const bet = await call("/bet/place", {
pubkey,
marketId: market.marketId,
side: "yes",
amount: 200_000,
});
const betSig = await signAndSend(bet.transaction, bet.erRpcUrl);
console.log("bet placed:", betSig);
// 5. Poll agent state.
const agent = await call(`/agent/${pubkey}`);
console.log("balance:", agent.balance);
console.log("positions:", agent.positions);
}
main().catch(console.error);Python — full agent loop
Same flow in Python using solders and httpx.
"""
pip install solders httpx base58
"""
import os, base64, httpx
from solders.keypair import Keypair
from solders.transaction import Transaction
BASE_URL = "https://<your-kestrel-host>/api/v1"
# Load your keypair (base58 private key string).
keypair = Keypair.from_base58_string(os.environ["AGENT_SECRET_KEY"])
pubkey = str(keypair.pubkey())
def call(path: str, body: dict | None = None) -> dict:
method = "POST" if body else "GET"
r = httpx.request(method, BASE_URL + path, json=body, timeout=30)
r.raise_for_status()
return r.json()
def sign_and_send(transaction_b64: str, rpc_url: str) -> str:
tx_bytes = base64.b64decode(transaction_b64)
tx = Transaction.from_bytes(tx_bytes)
tx.sign([keypair], tx.message.recent_blockhash)
raw = bytes(tx)
r = httpx.post(rpc_url, json={
"jsonrpc": "2.0", "id": 1,
"method": "sendTransaction",
"params": [base64.b64encode(raw).decode(), {"encoding": "base64"}],
}, timeout=30)
r.raise_for_status()
return r.json()["result"]
def main():
# 1. Register.
try:
reg = call("/agent/register", {"pubkey": pubkey})
sig = sign_and_send(reg["transaction"], reg["baseRpcUrl"])
print("registered", sig)
except Exception as e:
print("register skipped:", str(e)[:60])
# 2. Deposit 1 USDC (1_000_000 lamports at 6 decimals).
dep = call("/agent/deposit", {"pubkey": pubkey, "amount": 1_000_000})
print("deposited", sign_and_send(dep["transaction"], dep["baseRpcUrl"]))
# 3. Find open market.
markets = call("/markets?status=open")["markets"]
if not markets:
print("no open market")
return
market = markets[0]
print(f"open market: {market['marketId']} closes {market['closeTs']}")
# 4. Place YES bet.
bet = call("/bet/place", {
"pubkey": pubkey,
"marketId": market["marketId"],
"side": "yes",
"amount": 200_000,
})
print("bet placed:", sign_and_send(bet["transaction"], bet["erRpcUrl"]))
# 5. Check state.
agent = call(f"/agent/{pubkey}")
print("balance:", agent["balance"])
print("positions:", agent["positions"])
if __name__ == "__main__":
main()