In Part 1, you spun up a live GoldRush OHLCV stream for a Uniswap v3 pool on Base and turned those candles into a simple, deterministic z-score signal. Part 2 will turn that signal into profitable on-chain action and then prove it with PnL backfill. Think of it as turning a good idea into a disciplined, auditable loop: GoldRush for data, Base for execution, Uniswap for liquidity.
Budget the fees so “fast” doesn’t eat your edge: The transaction fees on Base, which are in two components ( L2 and an L1 fee), must be translated into basis points (bp) of your trade size so your signal only acts when it clears the total cost, not just gas.
Price the actual costs, then execute: From your z-score, you estimate an expected edge, then request a firm quote on Base via the 0x Swap API v2 (which taps Uniswap liquidity). Set a slippageBps
cap, estimate L2+L1 fees, and only send the tx if the conditions are right. We’ll also handle the one-time token approval that the API calls out.
Prove the trade with backfill: After you trade, pull your wallet’s txs via GoldRush Transactions v3, decode token movements, and roll up realized PnL. That ledger lets you tighten thresholds and buffers with evidence, not vibes.
When you start wiring an agent for real-world execution, the theory ends and the math begins. The line between a profitable and a losing trade often comes down to whether your edge can survive the cost of acting on it. That’s where cost-aware execution and PnL backfill come in.
Cost-aware execution is exactly what it sounds like. It's trading only when it’s actually worth it after fees, slippage, and uncertainty. Instead of reacting to every signal your model throws, the agent first checks whether the expected gain clears the total cost of doing business. On Base, those costs are more nuanced than just gas.
A Base transaction includes:
An L2 execution fee: Which is the gas you pay to run the transaction on Base, and
An L1 data fee: This is the cost of posting that transaction back to Ethereum for security. The L1 component fluctuates with Ethereum L1 conditions; the L2 part depends on load and your gas settings.
Base Network is fast, but a few basis points of fees often decide whether your edge pays or gets eaten. Since mid-2025, Base has led the L2 landscape, accounting for more than 80% of all L2 transaction fees according to Dune’s July report, meaning most onchain “alpha races” are happening here first.
Yes, Flashblocks changed the latency profile on Base, allowing streams to occur at over 200ms preconfirmations. This means strategies that react quickly (and price their costs correctly) are the ones that’ll actually win fills without waiting for full blocks.
Profitability on Base is simple to write, relentless to meet; therefore, to stay profitable,
“trade only when expected_edge_bps > fee_bps + slippage_bps + safety_buffer_bps.
”
This might feel cumbersome to understand. Let’s break it down:
(bps = “basis points” = 0.01%)
expected_edge_bps
: your model’s expected advantage for this trade, expressed in basis points of notional.
In Part 1, we mapped signal strength to edge, e.g., edge_bps = |z| × 30
.
If z = 3.0
, then edge_bps = 90
(i.e., +0.90% expected).
fee_bps
: the network fee for sending the transaction on Base, converted into bps of the trade size.
Compute the result from estimateGas * gasPrice
, convert wei to ETH, then ETH to your quote currency (e.g., USDC), divide by notional, and multiply by 10,000.
slippage_bps
: your worst-case price movement during execution, in bps of notional.
From a firm quote (0x v2 on Base), either use your cap (e.g., slippageBps = 50
) or derive it:slippage_bps = (1 − guaranteedPrice / price) × 10,000
.
safety_buffer_bps
: a cushion for things you didn’t price perfectly (small drifts, rounding, micro-latency).
Start with 10–25 bps; tighten once you’ve logged real fills.
This guarantees your predicted gain (in bps) beats your all-in costs (also in bps).
Example (Uniswap on Base)
Notional: 100 USDC
Model edge: z = 3.0
, mapping 30 bps per σ → expected_edge_bps = 90
Fee estimate: your provider says the tx will cost 0.000005 ETH; if ETH≈$4,000, that’s $0.04 = 0.04 USDC
fee_bps = (0.04 / 100) × 10,000 = 4.0
Slippage cap: slippage_bps = 50
(0.50%)
Safety buffer: safety_bps = 15
(0.15%)
Decision
90 > 4.0 + 50 + 15 → 90 > 69.0
→ trade ✅
If z = 2.0
(edge 60 bps), then 60 < 69.0
is false → skip ❌
That’s the whole idea: express everything in the same units (bps), then let a single inequality decide—no vibes, no guesswork.
PnL (profit and loss) backfill is how you prove whether your model worked after each transaction. Every fill, fee, and transfer gets recorded on-chain. GoldRush decodes that data for you so you can reconstruct the full trade history and measure whether your agent’s predicted edge actually showed up in real dollars.
This feedback is vital, and without it, you’re flying blind. With it, you can fine-tune thresholds, adjust buffers, and evolve from “reactive” trading to statistically disciplined execution.
While the cost-aware analysis above produces a clean rule for deciding whether to make a trade, PnL backfill actually measures what actually happened during and after the trade.
This is exactly where GoldRush shines. With its unified blockchain data API, you can pull every fill, gas receipt, token transfer, and fee adjustment directly from Base, then map them into your profitability models.
For example, if your Base strategy consistently overestimates edge during volatile hours, PnL backfill from GoldRush will reveal that pattern in seconds. You can then adjust your thresholds, raising your safety buffer or skipping specific pairs when gas volatility spikes instead of finding out after your USDC balance dips.
In Part 1, you brought GoldRush Streaming online and produced a deterministic z-score from live OHLCV. We’ll keep that setup, but be explicit about the venue: for Base, we’ll use Uniswap v3 pools as our market source and subscribe via GoldRush’s OHLCV Pairs stream. For Uniswap on Base, you can confirm deployments and canonical addresses in Uniswap’s docs before you pick the pool.
Signals: the z-score you built from GoldRush OHLCV Pairs on Base (Uniswap pool).
Firm quotes & slippage caps: the 0x Swap API v2 on chainId 8453 (Base); we’ll request firm quotes that include {to, data, value
} so we can send the tx verbatim and enforce slippageBps
. Check their chain support and Swap docs.
Execution: ethers.js
against a Base RPC (avoid the rate-limited public URL; use a provider, assert chainId 8453).
PnL/backfill: GoldRush Foundational API (Transactions v3) to decode fills and compute realized PnL after the fact.
You’ll wrap your z-score in a profit gate that prices fees, enforces a slippage cap, and only sends the Uniswap trade when the inequality clears. Then you’ll backfill fills and fees to produce a daily PnL—closing the loop between predicted edge and realized results. When you’re done, the agent won’t just trade on Base; it will prove why each trade made (or lost) money, with timestamps you can audit.
You’re continuing the same project from Part 1. Keep your existing .env and dependencies. If you’re starting fresh, complete Part 1’s setup first (GoldRush SDK + Node 18+, .env
with GOLDRUSH_API_KEY, BASE_RPC_URL
, and your Uniswap pool address).
a1
1
src/rpc.ts
to connect to the Base Mainnet. The file creates a single, shared ethers
provider wired to your Base RPC URL (from .env
), then provides a tiny helper assertBase()
that checks the provider is connected to chainId 8453 and throws early if not. You’ll import provider
and assertBase
from other modules (costs, trade, etc.), so every piece of code uses the same node and network checks. This keeps network checks consistent and prevents you from accidentally signing on the wrong chain.// src/rpc.ts
import { ethers } from "ethers";
/**
* .env must contain:
* BASE_RPC_URL=https://your-dedicated-base-rpc.example
*
* NOTE: avoid the public rate-limited endpoints for production.
*/
export const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC_URL!);
/**
* Call this before sending transactions or making fee decisions.
* Throws if the connected chain is not Base mainnet (8453).
*/
export async function assertBase() {
const net = await provider.getNetwork();
if (net.chainId !== 8453n) {
throw new Error(`Wrong chain connected: ${net.chainId.toString()} (need 8453 for Base mainnet)`);
}
}
assertBase()
, it will either:need 8453 ...
)," which indicates that the node is pointed at the wrong network.2
8453
). A firm quote returns the exact transaction payload you can submit (to, data, value
) along with price
and guaranteedPrice
fields you’ll use to compute slippage. We prefer firm quotes because they let you compare a model-predicted edge to a slippage-protected execution path before signing anything. The helper below also returns the API’s issue
s block (e.g., the spender
that needs an approval), so your trade flow can handle one-time approvals automatically.src/quote.ts
in your environment.// src/quote.ts
/**
* getFirmQuoteUSDCtoWETH
*
* What to set in .env:
* ZEROX_API_KEY=your_0x_key
*
* Notes:
* - sellAmountUSDC is provided in USDC's raw units (6 decimals), e.g. "100000000" = 100 USDC.
* - taker should be the address that will send the transaction (your bot's wallet).
* - slippageBps is a safety cap (e.g., 50 = 0.50%).
*
* Returns the JSON response from 0x which includes:
* - to, data, value (tx payload)
* - price, guaranteedPrice
* - gas (estimated gas)
* - issues.allowance.spender (if approval is required)
*/
export const USDC_BASE = "0x833589fCD6EDb6E08f4c7C32D4f71B54Bda02913"; // example; confirm on Base
export const WETH_BASE = "0x4200000000000000000000000000000000000006"; // canonical WETH on Base
export async function getFirmQuoteUSDCtoWETH(
sellAmountUSDC: string, // e.g. "100000000" = 100 USDC
taker: string, // your bot address (0x...)
slippageBps = 50 // default slippage cap (basis points)
) {
if (!process.env.ZEROX_API_KEY) {
throw new Error("ZEROX_API_KEY missing in .env");
}
const url = new URL("https://api.0x.org/swap/allowance-holder/quote");
url.searchParams.set("chainId", "8453"); // Base mainnet
url.searchParams.set("sellToken", USDC_BASE);
url.searchParams.set("buyToken", WETH_BASE);
url.searchParams.set("sellAmount", sellAmountUSDC);
url.searchParams.set("taker", taker);
url.searchParams.set("slippageBps", String(slippageBps));
const res = await fetch(url.toString(), {
method: "GET",
headers: {
"0x-api-key": process.env.ZEROX_API_KEY!,
“0x-version”: 'v2'
"Accept": "application/json"
}
});
if (!res.ok) {
const body = await res.text();
throw new Error(`0x quote failed: ${res.status} ${res.statusText} — ${body}`);
}
const payload = await res.json();
// payload contains: to, data, value, price, guaranteedPrice, gas, estimatedGas, issues, ...
return payload;
}
to, data, value
— the exact transaction payload you can pass to wallet.sendTransaction(...)
.price
and guaranteedPrice
— use these to calculate slippage:slippage_bps ≈ (1 - guaranteedPrice/price) * 10_000
.gas
/ estimatedGas
— useful as a sanity check before calling provider.estimateGas.
issues.allowance.spender
— if present, that contract needs an ERC20 approval before you can execute the returned payload.ZEROX_API_KEY
in .env
and confirm it's valid and not rate-limited.sellAmount
to test.issues.allowance.spender
present: call approve(spender, amount)
from your bot wallet (one-time) before attempting to send the quoted payload.guaranteedPrice
is missing, prefer the explicit slippageBps
cap and treat the quote as non-guaranteed.to
and data
before sending, and keep a router/allowlist if you want to restrict execution targets.3
trade.ts
use it as part of the decision gate.src/costs.ts
// src/costs.ts
import { provider } from "./rpc";
import { ethers } from "ethers";
/**
* Minimal shape of the 0x quote used here
*/
type Quote = {
to: string;
data: string;
value?: string | number;
price?: string | number; // optional, from 0x
guaranteedPrice?: string | number;
};
/**
* Convert native (ETH) cost into USDC using a price source.
* Replace this with a proper oracle or API call in production.
*/
async function ethToUSDC(ethAmount: number, ethUsdPrice: number) {
return ethAmount * ethUsdPrice; // approximate conversion
}
/**
* Compute transaction fee in basis points (bps)
*/
export async function feeBpsForQuote(
quote: Quote,
sellNotionalUSDC: string, // e.g., "100000000" = 100 USDC
ethUsdPrice: number // current ETH→USD price, e.g., 2500
) {
if (!quote?.to || !quote?.data) throw new Error("Invalid quote payload");
// Estimate gas cost for this quote
const txReq = {
to: quote.to,
data: quote.data,
value: (quote.value ? ethers.BigInt(String(quote.value)) : undefined) as any
};
const gasUsed = await provider.estimateGas(txReq);
const feeData = await provider.getFeeData();
const gasPrice = feeData.maxFeePerGas ?? feeData.gasPrice;
if (!gasPrice) throw new Error("Missing gas price data");
const txCostWei = gasUsed * gasPrice;
const txCostEth = Number(ethers.formatEther(txCostWei));
const txCostUSDC = await ethToUSDC(txCostEth, ethUsdPrice);
const notionalUSD = Number(sellNotionalUSDC) / 1e6; // USDC has 6 decimals
const feeBps = (txCostUSDC / notionalUSD) * 10_000;
return { feeBps, txCostUSDC };
}
to
" and "data
" fields.8453
), and token addresses.maxFeePerGas
or gasPrice
.estimateGas()
sometimes overshoots, especially for dynamic router calls.ethToUSDC()
function assumes a static price.4
guaranteedPrice
, adds a small safety buffer, and executes the transaction via ethers
only if the inequality clears:expected_edge_bps > fee_bps + slippage_bps + safety_bps
agent.ts
decision loop.src/trade.ts
// src/trade.ts
import { ethers } from "ethers";
import { provider } from "./rpc";
import { getFirmQuoteUSDCtoWETH } from "./quote";
import { feeBpsForQuote } from "./costs";
/**
* Inputs needed to evaluate + execute a trade
*/
export type TradeRequest = {
sellAmountUSDC: string; // raw units, e.g. "100000000" = 100 USDC
expectedEdgeBps: number; // from your model (e.g., z-score mapping)
slippageCapBps?: number; // guard cap, default 50 (0.50%)
safetyBps?: number; // buffer for micro-drifts, default 15 bps
ethUsdPrice: number; // USD/ETH used to convert gas -> USD
botPrivateKey: string; // signer for execution
};
export type TradeDecision =
| { status: "skip"; reason: string; context?: any }
| { status: "needs_approval"; spender: string; context?: any }
| { status: "submitted"; txHash: string; context?: any }
| { status: "failed"; error: string; context?: any };
/**
* Slippage in bps with 0x v2-first logic, v1 fallback, then cap.
* v2: buyAmount/minBuyAmount (numbers in raw units)
* v1: price/guaranteedPrice (strings)
*/
function slippageBpsFromQuote(quote: any, capBps: number) {
// --- v2 shape ---
const buyAmount = Number(quote?.buyAmount ?? 0);
const minBuyAmount = Number(quote?.minBuyAmount ?? 0);
if (buyAmount > 0 && minBuyAmount > 0 && minBuyAmount <= buyAmount) {
return ((buyAmount - minBuyAmount) / buyAmount) * 10_000;
}
// --- v1 shape ---
const price = Number(quote?.price ?? 0);
const gp = Number(quote?.guaranteedPrice ?? 0);
if (price > 0 && gp > 0) {
return (1 - gp / price) * 10_000;
}
// --- fallback ---
return capBps;
}
/**
* Gate and (optionally) execute a USDC->WETH trade on Base via 0x, only if profitable.
*/
export async function tryTradeIfProfitable(req: TradeRequest): Promise<TradeDecision> {
const slippageCap = req.slippageCapBps ?? 50;
const safety = req.safetyBps ?? 15;
// 1) Ensure we’re on Base mainnet (8453)
const net = await provider.getNetwork();
if (net.chainId !== 8453n) {
return { status: "skip", reason: `wrong chain ${net.chainId.toString()} (need 8453)` };
}
// 2) Signer
let wallet: ethers.Wallet;
try {
wallet = new ethers.Wallet(req.botPrivateKey, provider);
} catch (e) {
return { status: "failed", error: "invalid private key", context: { e: String(e) } };
}
const takerAddr = await wallet.getAddress();
// 3) Firm quote on Base
let quote: any;
try {
quote = await getFirmQuoteUSDCtoWETH(req.sellAmountUSDC, takerAddr, slippageCap);
} catch (e) {
return { status: "failed", error: "0x quote failed", context: { e: String(e) } };
}
// 4) Allowance requirement?
const spender = quote?.issues?.allowance?.spender;
if (spender) {
return { status: "needs_approval", spender, context: { quoteHint: { to: (quote.transaction || quote)?.to } } };
}
// 5) Fee (bps) for the quoted payload (v2 nests under .transaction)
let feeBps: number, txCostUSDC: number;
try {
const txForFees = quote?.transaction || quote;
const feeRes = await feeBpsForQuote(txForFees, req.sellAmountUSDC, req.ethUsdPrice);
feeBps = feeRes.feeBps;
txCostUSDC = feeRes.txCostUSDC;
} catch (e) {
return { status: "failed", error: "fee estimation failed", context: { e: String(e) } };
}
// 6) Slippage (bps) with v2-first logic
const slippageBps = slippageBpsFromQuote(quote, slippageCap);
// 7) Profitability gate
const hurdle = feeBps + slippageBps + safety;
if (req.expectedEdgeBps <= hurdle) {
return {
status: "skip",
reason: "edge <= costs",
context: { edge: req.expectedEdgeBps, feeBps, slippageBps, safetyBps: safety, hurdle }
};
}
// 8) Execute the provided transaction (handle v2 nesting)
try {
const txData = quote?.transaction || quote;
const tx = await wallet.sendTransaction({
to: txData.to,
data: txData.data,
value: txData.value ?? 0
});
const rec = await tx.wait();
return {
status: "submitted",
txHash: tx.hash,
context: {
blockNumber: rec?.blockNumber,
edge_bps: req.expectedEdgeBps,
fee_bps: feeBps,
slippage_bps: slippageBps,
safety_bps: safety,
txCostUSDC
}
};
} catch (e) {
return { status: "failed", error: "sendTransaction failed", context: { e: String(e) } };
}
}
BASE_RPC_URL
to a Base mainnet provider.ZEROX_API_KEY
, token addresses, or reduce sellAmountUSDC
.approve(spender, amount)
once, then retry.quote.to/data/value
exactly.slippageCapBps/safetyBps
.ethUsdPrice
with a live source (0x /price
, Chainlink, or GoldRush pricing).predict.ts
produced expectedEdgeBps
from your z-score.trade.ts
requests 0x for the route and payload, converts fees and slippage to bps, gates, and executes.Up to this point, your agent can see the market (GoldRush stream), decide (z-score + profitability check), and act (guarded 0x execution).
Now it’s time to teach it how to remember.
Before moving to the next stage, we need to verify if the codes for the cost-aware executions are working properly. The output should tell us whether the trade is viable in terms of gas, slippage, etc.
To do this, we create a tiny probe that stops before sendTransaction
and just prints the math. This validates your quote, fee bps, and slippage bps.
Create a src/decision_probe.ts
file and paste this code into it.
// src/trade.ts
import { ethers } from "ethers";
import { provider } from "./rpc";
const ERC20_ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)"
];
// USDC on Base (confirm once in your project)
export const USDC_BASE = "0x833589fCD6EDb6E08f4c7C32D4f71B54Bda02913";
export type EnsureAllowanceInput = {
token?: string; // defaults to USDC_BASE
ownerPk: string; // BOT_WALLET_PK
spender: string; // from quote.issues.allowance.spender
minAmountRaw: string; // e.g., "100000000" = 100 USDC
maxAmountRaw?: string; // optional, e.g., MaxUint256 for “approve max”
};
export async function ensureAllowance({
token = USDC_BASE,
ownerPk,
spender,
minAmountRaw,
maxAmountRaw
}: EnsureAllowanceInput) {
const wallet = new ethers.Wallet(ownerPk, provider);
const owner = await wallet.getAddress();
const erc20 = new ethers.Contract(token, ERC20_ABI, wallet);
// 1) Check current allowance
const current = await erc20.allowance(owner, spender);
const need = ethers.BigInt(minAmountRaw);
if (current >= need) {
return { status: "ok", action: "skip_approve", allowance: current.toString() };
}
// 2) Approve (either exact min or a higher, managed ceiling)
const amount = ethers.BigInt(maxAmountRaw ?? minAmountRaw);
const tx = await erc20.approve(spender, amount);
const rec = await tx.wait();
return {
status: "approved",
txHash: tx.hash,
blockNumber: rec?.blockNumber,
allowanceSet: amount.toString()
};
}
What you should see (examples):
Healthy proceed{ expectedEdgeBps: 90, feeBps: 4.2, slippageBps: 28.1, safetyBps: 15, decision: 'PROCEED' }
Correct skip{ expectedEdgeBps: 60, feeBps: 4.8, slippageBps: 50.0, safetyBps: 15, decision: 'SKIP' }
Allowance required (from the quote)quote.issues.allowance.spender present → you’ll handle with ensureAllowance(...)
in Step 5.
If this probe prints reasonable numbers and a sensible decision, your cost math is correct.
5
issues.allowance.spender
, your bot must approve that spender to move your USDC before any swap can execute. This helper checks the current allowance and, if needed, submits an approve(spender, amount)
from your bot wallet. It’s called only when trade.ts
returns status: "needs_approval
".src/approve.ts
// src/approve.ts
import { ethers } from "ethers";
import { provider } from "./rpc";
const ERC20_ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)"
];
// USDC on Base (confirm once in your project)
export const USDC_BASE = "0x833589fCD6EDb6E08f4c7C32D4f71B54Bda02913";
export type EnsureAllowanceInput = {
token?: string; // defaults to USDC_BASE
ownerPk: string; // BOT_WALLET_PK
spender: string; // from quote.issues.allowance.spender
minAmountRaw: string; // e.g., "100000000" = 100 USDC
maxAmountRaw?: string; // optional, e.g., MaxUint256 for “approve max”
};
export async function ensureAllowance({
token = USDC_BASE,
ownerPk,
spender,
minAmountRaw,
maxAmountRaw
}: EnsureAllowanceInput) {
const wallet = new ethers.Wallet(ownerPk, provider);
const owner = await wallet.getAddress();
const erc20 = new ethers.Contract(token, ERC20_ABI, wallet);
// 1) Check current allowance
const current = await erc20.allowance(owner, spender);
const need = ethers.BigInt(minAmountRaw);
if (current >= need) {
return { status: "ok", action: "skip_approve", allowance: current.toString() };
}
// 2) Approve (either exact min or a higher, managed ceiling)
const amount = ethers.BigInt(maxAmountRaw ?? minAmountRaw);
const tx = await erc20.approve(spender, amount);
const rec = await tx.wait();
return {
status: "approved",
txHash: tx.hash,
blockNumber: rec?.blockNumber,
allowanceSet: amount.toString()
};
}
trade.ts
returns { status: "needs_approval", spender }
.ensureAllowance({ ownerPk: BOT_WALLET_PK, spender, minAmountRaw: sellAmountUSDC })
.tryTradeIfProfitable(...)
again with the same inputs.quote.issues.allowance.spender
and approve that address.6
// src/pnl.ts
import 'dotenv/config';
import { GoldRushClient } from '@covalenthq/client-sdk';
const client = new GoldRushClient(process.env.GOLDRUSH_API_KEY!);
// Basic types
type Tx = any;
type TokenDelta = { token: string; amount: string };
type PricedFlow = { ts: number; value_usd: number };
// 1) Fetch paginated transactions for a wallet on Base
export async function fetchTxPageBase(address: string, page = 0) {
const res = await client.TransactionService.getPaginatedTransactionsForAddress({
chainName: 'base-mainnet',
walletAddress: address,
page
});
return res.data;
}
// 2) Iterate through pages to get a date range
export async function fetchTxnsRangeBase(address: string, startTs: number, endTs: number) {
const out: Tx[] = [];
for (let page = 0; page < 10; page++) {
const data = await fetchTxPageBase(address, page);
const items: Tx[] = data?.items ?? [];
for (const tx of items) {
const ts = new Date(tx.timestamp ?? tx.block_signed_at).getTime();
if (ts < startTs) return out;
if (ts <= endTs) out.push(tx);
}
if (!items.length) break;
}
return out;
}
// 3) Compute token deltas (in/out) for your wallet
export function tokenDeltasForWallet(tx: Tx, wallet: string): TokenDelta[] {
const deltas: Record<string, bigint> = {};
const logs = tx?.log_events ?? [];
for (const ev of logs) {
if (ev?.decoded?.name === 'Transfer') {
const params = ev.decoded.params ?? [];
const from = params.find((p:any)=>p.name==='from')?.value?.toLowerCase?.();
const to = params.find((p:any)=>p.name==='to')?.value?.toLowerCase?.();
const val = params.find((p:any)=>p.name==='value')?.value as string;
const token = ev?.sender_address;
if (!token || !val) continue;
const amt = BigInt(val);
if (from === wallet.toLowerCase()) deltas[token] = (deltas[token] ?? 0n) - amt;
if (to === wallet.toLowerCase()) deltas[token] = (deltas[token] ?? 0n) + amt;
}
}
return Object.entries(deltas).map(([token, amount]) => ({ token, amount: amount.toString() }));
}
// 4) Value flows in USD (stub: replace with real pricing source)
export async function priceAtTsUSDC(token: string, ts: number, amountRaw: string): Promise<number> {
// Plug in GoldRush pricing endpoint here
return 0; // placeholder
}
// 5) Compute realized PnL for a transaction
export async function realizedPnlForTx(tx: Tx, wallet: string) {
const ts = new Date(tx.timestamp ?? tx.block_signed_at).getTime();
const flows = tokenDeltasForWallet(tx, wallet);
const values: PricedFlow[] = [];
for (const f of flows) {
const v = await priceAtTsUSDC(f.token, ts, f.amount);
values.push({ ts, value_usd: v });
}
const grossUSD = values.reduce((a, b) => a + b.value_usd, 0);
const gasUSD = 0; // Add later using tx.fees_paid and pricing helper
const realized = grossUSD - gasUSD;
return { ts, grossUSD, gasUSD, realizedUSD: realized };
}
// 6) Roll up daily PnL
export function rollupDailyPnL(rows: { ts: number; realizedUSD: number }[]) {
const day = (ms:number) => new Date(new Date(ms).toISOString().slice(0,10)).getTime();
const map: Record<string, number> = {};
for (const r of rows) {
const d = day(r.ts).toString();
map[d] = (map[d] ?? 0) + r.realizedUSD;
}
return Object.entries(map)
.map(([d, v]) => ({ day: new Date(Number(d)).toISOString().slice(0,10), pnl_usd: v }))
.sort((a,b)=>a.day.localeCompare(b.day));
}
// src/pnl_demo.ts
import { fetchTxnsRangeBase, realizedPnlForTx, rollupDailyPnL } from './pnl';
const WALLET = '0xYourWalletHere';
const endTs = Date.now();
const startTs = endTs - 7 * 24 * 60 * 60 * 1000;
(async () => {
const txs = await fetchTxnsRangeBase(WALLET, startTs, endTs);
const rows = await Promise.all(txs.map(tx => realizedPnlForTx(tx, WALLET)));
console.table(rollupDailyPnL(rows));
})();
npm run dev -- src/pnl_demo.ts
Each row corresponds to:
day
→ UTC date of your trade activity
pnl_usd
→ realised net profit or loss (USD or USDC equivalent) after subtracting fees
If there’s no trading history, you’ll instead see an empty table []
.
Or if your pricing function (priceAtTsUSDC
) still returns 0, all PnL will appear as pnl_usd = 0
Behind the Output
Each PnL value represents the sum of token inflows/outflows for your agent’s wallet over a 24-hour window, converted into USD.
So if your bot made several small swaps on a given day, the script adds all realized profits/losses to a single number.
You can later pipe this into a visualization (e.g., Grafana, Dune, or a simple chart) to monitor daily returns, variance, and hit rates.
No transactions found: Check your wallet address or widen the date range.
Value is 0 for all flows: Replace priceAtTsUSDC()
stub with GoldRush’s pricing endpoint.
Out-of-memory errors: Reduce history depth or use pagination to limit fetch size.
Duplicate transactions: GoldRush API paginates cleanly, but store results by tx_hash
to dedupe.
By this point, your agent has evolved from a reactive listener into a self-contained trading loop that can see, decide, act, and remember — all on Base.
In Part 1, you built the foundation: a live OHLCV stream, a deterministic z-score signal, and a simple logic harness to prove it worked in real time.
Here, in Part 2, you gave that signal economic context. You taught the agent how to respect costs, quote trades safely through 0x, and verify its own performance with GoldRush’s decoded transaction data.
Together, those layers form a practical feedback loop:
GoldRush Streaming → real-time perception
0x Swap API → safe, executable quotes
Base Network → low-latency settlement
GoldRush Transactions v3 → transparent post-trade truth
That loop is what makes an agent production-ready. It doesn’t just react — it learns from its own ledger.
In Part 3, you’ll extend this foundation:
introducing risk controls and safety limits,
integrating LLM-based planning for multi-step actions, and
connecting your metrics to a live monitoring dashboard.
When you’re done, you’ll have a fully measurable, verifiable trading agent that runs autonomously on Base that is fast, data-driven, and accountable.