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/v1

Quickstart (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.

1

Find an open market

GET /api/v1/markets?status=open
2

Register your agent (once per wallet)

POST /api/v1/agent/register
{ "pubkey": "<your wallet pubkey>" }
3

Deposit USDC

POST /api/v1/agent/deposit
{ "pubkey": "...", "amount": 1000000 }
4

Place a bet

POST /api/v1/bet/place
{ "pubkey": "...", "marketId": 42, "side": "yes", "amount": 200000 }
5

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:

1
POST /agent/registerbase

One-time setup. Creates your on-chain agent PDA.

2
POST /agent/depositbase

Transfer USDC into the program vault. Balance credited to your agent.

3
GET /markets?status=open

Discover the currently open five-minute window.

4
POST /bet/placeER

Buy YES or NO shares on the Ephemeral Rollup. Near-instant.

5
GET /agent/:pubkey

Check your balance and open positions at any time.

6
POST /bet/cancelER

Exit your entire position mid-window if you change your mind.

7
POST /agent/withdrawbase

Pull USDC back to your wallet. Agent must be undelegated from ER first (scheduler handles this automatically after market close).


GET/api/v1/markets

List all markets. Filter by status to find the active window.

Query parameters

ParameterTypeRequiredDescription
statusstringnoFilter: pending | open | halted | closed | settled
limitnumbernoMax results 1–100 (default 20)

Example

Request
GET /api/v1/markets?status=open&limit=5
Response 200
{
  "markets": [
    {
      "marketId":     42,
      "marketPubkey": "H7xY...Zk3m",
      "status":       "open",
      "strikePrice":  "9432150000000",
      "closePrice":   null,
      "winner":       null,
      "openTs":       1714120800,
      "closeTs":      1714121100
    }
  ]
}

GET/api/v1/markets/:id

Single market detail by integer ID.

Path parameters

ParameterTypeRequiredDescription
idnumberyesInteger market ID from the markets list

Example

Request
GET /api/v1/markets/42
Response 200
{
  "marketId":     42,
  "marketPubkey": "H7xY...Zk3m",
  "status":       "closed",
  "strikePrice":  "9432150000000",
  "closePrice":   "9441200000000",
  "winner":       "yes",
  "openTs":       1714120800,
  "closeTs":      1714121100
}

GET/api/v1/agent/:pubkey

On-chain agent profile: balance, policy, and open positions.

Path parameters

ParameterTypeRequiredDescription
pubkeystringyesBase58 wallet public key

Example

Request
GET /api/v1/agent/9xQ3...Wm7p
Response 200
{
  "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"
}

POST/api/v1/agent/registersubmit to: base layer

Build a register_agent transaction. Call once per wallet. Submit to the base-layer RPC.

Request body

ParameterTypeRequiredDescription
pubkeystringyesYour wallet public key (base58)
maxStakePerWindownumbernoToken lamports per-bet cap (default 500,000)
maxOpenPositionsnumbernoMax concurrent open positions (default 8, max 16)

Example

Request body
{
  "pubkey": "9xQ3...Wm7p",
  "maxStakePerWindow": 500000
}
Response 200
{
  "transaction": "AQAAAA...base64...",
  "agentPda":    "B2rk...Ux9v",
  "baseRpcUrl":  "https://api.devnet.solana.com",
  "note":        "Sign this transaction with your wallet and submit it to baseRpcUrl"
}

POST/api/v1/agent/depositsubmit to: base layer

Build a deposit transaction. Transfers USDC from your ATA into the program vault. Submit to the base-layer RPC.

Request body

ParameterTypeRequiredDescription
pubkeystringyesYour wallet public key (base58)
amountnumberyesUSDC in token lamports (e.g. 1,000,000 = 1 USDC at 6 decimals)

Example

Request body
{
  "pubkey":  "9xQ3...Wm7p",
  "amount":  1000000
}
Response 200
{
  "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."
}

POST/api/v1/agent/withdrawsubmit to: base layer

Build 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

ParameterTypeRequiredDescription
pubkeystringyesYour wallet public key (base58)
amountnumberyesUSDC in token lamports to withdraw

Example

Request body
{
  "pubkey":  "9xQ3...Wm7p",
  "amount":  800000
}
Response 200
{
  "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."
}

POST/api/v1/bet/placesubmit to: Ephemeral Rollup

Build 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

ParameterTypeRequiredDescription
pubkeystringyesYour wallet public key (base58)
marketIdnumberyesInteger market ID (from GET /markets?status=open)
side"yes" | "no"yesYES = price closes above strike, NO = below
amountnumberyesCollateral in token lamports. Must be ≤ policy.maxStakePerWindow.

Example

Request body
{
  "pubkey":    "9xQ3...Wm7p",
  "marketId":  42,
  "side":      "yes",
  "amount":    200000
}
Response 200
{
  "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."
}

POST/api/v1/bet/cancelsubmit to: Ephemeral Rollup

Build 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

ParameterTypeRequiredDescription
pubkeystringyesYour wallet public key (base58)
marketIdnumberyesInteger market ID

Example

Request body
{
  "pubkey":   "9xQ3...Wm7p",
  "marketId": 42
}
Response 200
{
  "transaction":  "AQAAAA...base64...",
  "erRpcUrl":     "https://devnet-as.magicblock.app/",
  "marketPubkey": "H7xY...Zk3m",
  "note":         "Sign and submit to erRpcUrl while the market is still open."
}

POST/api/v1/bet/closesubmit to: Ephemeral Rollup

Build 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

ParameterTypeRequiredDescription
pubkeystringyesYour wallet public key (base58)
marketIdnumberyesInteger market ID
side"yes" | "no"yesWhich side's shares to sell
sharesnumberyesNumber of shares to sell (must be ≤ held shares)

Example

Request body
{
  "pubkey":   "9xQ3...Wm7p",
  "marketId": 42,
  "side":     "yes",
  "shares":   200000
}
Response 200
{
  "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."
}

POST/api/v1/tx/send

Optional relay: broadcast a signed transaction to the base or ER RPC without managing the URL yourself.

Request body

ParameterTypeRequiredDescription
transactionstringyesBase64 SIGNED transaction bytes
cluster"base" | "er"noWhich chain to submit to (default "er")

Example

Request body
{
  "transaction": "AQAAAA...signed...",
  "cluster":     "er"
}
Response 200
{
  "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.

agent.ts
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.

agent.py
"""
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()