Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
35e4759
Bump bot deps: rand for maker, transaction-parser for taker
finnfujimura Apr 28, 2026
f5fe830
taker: add MarketSnapshot/ExecutionProfile and stateful
finnfujimura Apr 28, 2026
45b1f19
taker: rewire archetype defaults to ExecutionProfile presets
finnfujimura Apr 28, 2026
89cdd58
taker: plumb execution-profile knobs through agent config
finnfujimura Apr 28, 2026
4af62ea
taker: cache snapshot per tick, log per-agent execution
finnfujimura Apr 28, 2026
61b9573
maker: switch external price anchor to S5/12-candle window
finnfujimura Apr 28, 2026
c106d48
maker: add maker-style presets (tight/balanced/defensive) and
finnfujimura Apr 28, 2026
0d0dabd
maker: add local-book signals, hit detection, dynamic spread,
finnfujimura Apr 28, 2026
70e9ca2
maker: refresh resting liquidity on quote-TTL timeout
finnfujimura Apr 28, 2026
8e822c2
taker: only advance parent orders after successful submit
finnfujimura Apr 29, 2026
e591b0f
taker: restore fetch_market_snapshot and Arc-based
finnfujimura Apr 29, 2026
3636dfa
funding per agent takers during init
finnfujimura Apr 29, 2026
495becd
frontend: label transaction log fills with trader personality
finnfujimura Apr 29, 2026
985d435
new taker compose with mounted keypairs
finnfujimura Apr 29, 2026
be06414
gitignore: re-ignore keypairs/
finnfujimura Apr 29, 2026
7a8f1e8
Update services/.gitignore
finnfujimura Apr 29, 2026
7eab7f0
maker-bot: rename price_jitter_bps -> price_jitter_pct to match its s…
finnfujimura Apr 29, 2026
2c75370
maker-bot: fix jitter unit naming and tighten validation bounds
finnfujimura Apr 29, 2026
2468e5e
Update frontend/src/components/TransactionLog.tsx
finnfujimura Apr 29, 2026
c25d3e4
taker-bot: treat unknown spread as wide so the spread gate
finnfujimura May 8, 2026
220247c
maker-bot: name the rolling-window length and
finnfujimura May 8, 2026
60dff1e
initialization_helper: honor --force when (re)generating
finnfujimura May 8, 2026
456269e
services/.gitignore: document keypairs/ and
finnfujimura May 8, 2026
23f6212
docs: describe taker archetypes, maker styles, and the agent
finnfujimura May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions frontend/src/app/api/agents/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fs from "node:fs/promises";
import path from "node:path";
import { NextResponse } from "next/server";

export type AgentRegistryEntry = {
name: string;
kind: "maker" | "taker";
pubkey: string;
};

/**
* Returns the trader registry written by `services/shared/examples/initialization_helper.rs`.
*
* The frontend uses this to label each fill in the transaction log with the
* personality (maker, retail-1, whale-1, etc.) that submitted the trade.
*
* Returns an empty array when the file is missing — e.g. when the frontend is
* run against devnet/testnet/mainnet rather than the local helper script.
*/
export async function GET() {
const filePath = path.join(
process.cwd(),
"..",
"services",
"taker-bot",
"agents.json",
);
try {
const raw = await fs.readFile(filePath, "utf8");
const parsed = JSON.parse(raw) as AgentRegistryEntry[];
return NextResponse.json(parsed);
} catch {
return NextResponse.json([]);
}
}
68 changes: 63 additions & 5 deletions frontend/src/components/TransactionLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { Address } from "@solana/addresses";
import { Decimal } from "decimal.js";
import { Activity } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useAgentRegistry } from "@/lib/hooks/use-agent-registry";
import type { AgentRegistryEntry } from "@/lib/queries/fetch-agent-registry";
import { solscanTxUrl } from "@/lib/solana/explorer";
import { truncateAddress } from "@/lib/solana/format";
import { useMarketStore } from "@/lib/stores/market-store";
Expand Down Expand Up @@ -45,6 +47,8 @@ type FillData = {
relativeTime: string;
signature: string;
accounts: ResolvedAccount[];
trader: AgentRegistryEntry | null;
signerAddress: Address | null;
lastFillPrice: Decimal;
discriminator: number;
isBuy: boolean;
Expand All @@ -56,6 +60,35 @@ type FillData = {
encodedPrice: number;
};

const TAKER_PERSONALITY_COLORS: Record<string, string> = {
retail: "border-sky-500/30 bg-sky-500/15 text-sky-400",
whale: "border-violet-500/30 bg-violet-500/15 text-violet-400",
sniper: "border-amber-500/30 bg-amber-500/15 text-amber-400",
noise: "border-zinc-500/30 bg-zinc-500/15 text-zinc-400",
passive: "border-emerald-500/30 bg-emerald-500/15 text-emerald-400",
aggressive: "border-rose-500/30 bg-rose-500/15 text-rose-400",
};

const MAKER_BADGE = "border-indigo-500/30 bg-indigo-500/15 text-indigo-400";
const UNKNOWN_BADGE = "border-border bg-muted/40 text-muted-fg";

function traderBadgeClass(trader: AgentRegistryEntry | null): string {
if (!trader) return UNKNOWN_BADGE;
if (trader.kind === "maker") return MAKER_BADGE;
// Agent names look like `retail-1`, `whale-1`, etc. Color by the archetype prefix.
const archetype = trader.name.split("-")[0];
return TAKER_PERSONALITY_COLORS[archetype] ?? UNKNOWN_BADGE;
}

function traderLabel(
trader: AgentRegistryEntry | null,
signerAddress: Address | null,
): string {
if (trader) return trader.name;
if (signerAddress) return truncateAddress(signerAddress);
return "unknown";
}

function FillEntry({
fill,
barPercent,
Expand Down Expand Up @@ -89,6 +122,13 @@ function FillEntry({
{fill.relativeTime}
</span>

<span
className={`z-10 w-24 shrink-0 truncate rounded border px-1.5 py-0.5 text-center font-mono text-[10px] uppercase tracking-wider ${traderBadgeClass(fill.trader)}`}
title={fill.signerAddress ?? undefined}
Comment thread
finnfujimura marked this conversation as resolved.
>
{traderLabel(fill.trader, fill.signerAddress)}
</span>

<span className={`w-20 shrink-0 font-mono text-xs tabular-nums ${color}`}>
{fill.lastFillPrice.toDecimalPlaces(6).toString()}
</span>
Expand Down Expand Up @@ -146,19 +186,34 @@ export function TransactionLog({ marketAddress }: { marketAddress: Address }) {
const baseDecimals = market?.base.decimals ?? 0;
const quoteDecimals = market?.quote.decimals ?? 0;

const { byPubkey: agentByPubkey } = useAgentRegistry();

const fills: (FillData & { key: string })[] = useMemo(
() =>
transactions
.filter((tx) => !tx.err)
.flatMap((tx) =>
tx.parsed.dropsetEvents
.flatMap((tx) => {
// The fill-emitting transaction is a taker market order, so the
// first writable signer (i.e. the fee payer) is the trader we want
// to label. `parsed.accounts` from `resolveAccounts` is ordered with
// writable signers first.
const signer =
tx.parsed.accounts.find((a) => a.signer && a.writable) ?? null;
const signerAddress = signer?.address ?? null;
const trader = signerAddress
? (agentByPubkey.get(signerAddress) ?? null)
: null;

return tx.parsed.dropsetEvents
.filter((e) => e.kind === "fill")
.map((fill, i) => ({
...fill.data,
key: `${tx.signature}-${i}`,
relativeTime: tx.blockTime ? relativeTime(tx.blockTime) : "—",
signature: tx.signature,
accounts: tx.parsed.accounts,
trader,
signerAddress,
lastFillPrice: encodedU32ToDecimal(fill.data.encodedPrice),
baseFilledUi: new Decimal(fill.data.baseFilled.toString())
.div(new Decimal(10).pow(baseDecimals))
Expand All @@ -168,9 +223,9 @@ export function TransactionLog({ marketAddress }: { marketAddress: Address }) {
.div(new Decimal(10).pow(quoteDecimals))
.toDecimalPlaces(quoteDecimals)
.toString(),
})),
),
[transactions, baseDecimals, quoteDecimals],
}));
}),
[transactions, baseDecimals, quoteDecimals, agentByPubkey],
);

return (
Expand Down Expand Up @@ -207,6 +262,9 @@ export function TransactionLog({ marketAddress }: { marketAddress: Address }) {
{fills.length > 0 && (
<div className="flex items-center gap-3 border-border/50 border-b px-3 py-1.5">
<span className="w-16 shrink-0 text-muted-fg/60 text-xs">Time</span>
<span className="w-24 shrink-0 text-center text-muted-fg/60 text-xs">
Trader
</span>
<span className="w-20 shrink-0 text-muted-fg/60 text-xs">
Price
</span>
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/lib/hooks/use-agent-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import type { Address } from "@solana/addresses";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import {
type AgentRegistryEntry,
fetchAgentRegistry,
} from "@/lib/queries/fetch-agent-registry";

export function useAgentRegistry() {
const query = useQuery({
queryKey: ["agent-registry"],
queryFn: fetchAgentRegistry,
// The registry is rewritten on every `run-services-on-localnet.sh --force`,
// so a long stale time is fine — refetch on remount is enough.
staleTime: Number.POSITIVE_INFINITY,
});

const byPubkey = useMemo(() => {
const map = new Map<Address, AgentRegistryEntry>();
for (const entry of query.data ?? []) {
map.set(entry.pubkey, entry);
}
return map;
}, [query.data]);

return { entries: query.data ?? [], byPubkey };
}
13 changes: 13 additions & 0 deletions frontend/src/lib/queries/fetch-agent-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Address } from "@solana/addresses";

export type AgentRegistryEntry = {
name: string;
kind: "maker" | "taker";
pubkey: Address;
};

export async function fetchAgentRegistry(): Promise<AgentRegistryEntry[]> {
const res = await fetch("/api/agents");
if (!res.ok) return [];
return (await res.json()) as AgentRegistryEntry[];
}
7 changes: 7 additions & 0 deletions services/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
config.toml
keypair.json
# Per-agent keypair directory written by `services/shared/examples/initialization_helper.rs`
# when bootstrapping localnet (one file per taker agent). Regenerate by re-running the
# helper; pass `--force` to overwrite existing files.
keypairs/
# Generated agent registry (`{name, kind, pubkey}` array) consumed by the frontend
# for fill labeling. Written by the same initialization_helper run that funds the agents.
taker-bot/agents.json
Comment thread
finnfujimura marked this conversation as resolved.
1 change: 1 addition & 0 deletions services/maker-bot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dropset-interface = { path = "../../interface" }
dropset-services-shared = { path = "../shared" }
itertools.workspace = true
price = { path = "../../price", features = ["client" ] }
rand.workspace = true
reqwest.workspace = true
rust_decimal = { workspace = true, features = ["macros", "serde-with-arbitrary-precision"] }
solana-address.workspace = true
Expand Down
31 changes: 31 additions & 0 deletions services/maker-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ A prototype market-making bot implementing a naive version of the
[Avellaneda-Stoikov model] for a `dropset` market. Intended for
experimentation and local testing, not production use.

## Quoting model

In addition to the Avellaneda-Stoikov reservation price, the bot adapts its
quotes to local-book conditions:

- **Effective mid** blends an external fair-value anchor (OANDA `S5`) with a
local microprice from the on-chain book, weighted by `local_book_weight_bps`.
- **Dynamic half-spread** widens with recent local-fair-price volatility and
with one-sided "hit pressure" (counted by comparing maker-owned depth across
market updates).
- **Hit detection** flags which side was most recently lifted/hit, applies a
per-side refill delay, and skips re-posting at the touch on that side until
the delay elapses.
- **Quote TTL** refreshes resting liquidity even without a websocket-triggered
state change, so quotes age out instead of going stale.
- **Per-level jitter** (size and price) is applied across the ladder so the
ladder looks less robotic.

### Style presets

Each maker run picks a `style` preset (overridable per field in `config.toml`):

| Style | Quote TTL | Refill delay (min–max) | `replenish_ratio_bps` | `hit_widening_bps` | Max quote levels | Spread multiplier |
|-------------|-----------|------------------------|-----------------------|--------------------|------------------|-------------------|
| `tight` | 1 500 ms | 250–900 ms | 9 000 | 10 | 10 | 0.90× |
| `balanced` | 2 500 ms | 600–2 000 ms | 7 000 | 18 | 8 | 1.20× |
| `defensive` | 3 500 ms | 1 200–4 000 ms | 5 500 | 28 | 6 | 1.75× |

See `src/config.rs` (`MakerStyleDefaults`) for the full preset values and
`config.toml.example` for descriptions of the override knobs.

## Running

1. If you're using Docker Desktop, make sure `Enable host networking` is checked
Expand Down
Loading