Borro Bookrunners
Send depositors to Borro. Keep the performance fee.
One link or one contract call. Deposits stay in the depositor's wallet, shares and all. Paid monthly in USDG.
- MinimumsNone for the fee share. $50,000 in deposits for the Drip.
- SetupA link, or one call. Nothing to deploy.
- Your users' fundsVault shares land in their own wallet.
- The contractBorroBuilders, on Robinhood Chain
The economics
Fee share, plus the Drip.
Borro's fee in USDG, and 20M $BORRO split among bookrunners by the deposits they bring. Borrowers count too: Borro's fee on the interest they pay.
Deposits held a year; yours a fifth of all bookrunners bring. Assumes 30% rent (AAPL and GOOGL lenders today), today's Drip rate, and $BORRO at 1.75x TVL (Longbow: 1.1x). Not a promise.
- Counted per deposit
Each referred deposit is its own lot with its own clock. Withdrawals come off the oldest lots first.
- Paid from $1
Any month a code is owed $1 or more. Less carries over to the next month.
- Settled monthly
In USDG, to the code's wallet, by the keeper.
- Nothing to custody
Vault shares land in the depositor's wallet. The contract holds nothing between transactions.
Integrate
One call credits the deposit.
Approve the token to the Bookrunners contract, then deposit(vault, assets, code, minShares). The shares land in the caller's wallet.
import { ethers } from "ethers";
const BUILDERS = "0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291";
const NVDA_VAULT = "0x508E23274C39C60c345e35B1f0188bE180717CB9";
const NVDA = "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC";
const provider = new ethers.BrowserProvider(window.ethereum); // Robinhood Chain, chain id 4663
const signer = await provider.getSigner();
const vault = new ethers.Contract(NVDA_VAULT, ["function previewDeposit(uint256 assets) view returns (uint256)"], provider);
const token = new ethers.Contract(NVDA, ["function approve(address spender, uint256 amount) returns (bool)"], signer);
const builders = new ethers.Contract(BUILDERS, ["function deposit(address vault, uint256 assets, bytes32 code, uint256 minShares) returns (uint256 shares)"], signer);
const assets = ethers.parseUnits("1.5", 18); // 1500000000000000000n
const code = ethers.encodeBytes32String("route"); // 0x726f757465000…000
const minShares = (await vault.previewDeposit(assets)) * 995n / 1000n; // e.g. 1497000000000000000n gives 1489515000000000000n
await (await token.approve(BUILDERS, assets)).wait();
const receipt = await (await builders.deposit(NVDA_VAULT, assets, code, minShares)).wait();
// The shares are in the signer's wallet. The Referred event in the receipt carries the exact number.import { createPublicClient, createWalletClient, custom, parseAbi, parseUnits, stringToHex } from "viem";
const BUILDERS = "0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291";
const NVDA_VAULT = "0x508E23274C39C60c345e35B1f0188bE180717CB9";
const NVDA = "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC";
const chain = { id: 4663, name: "Robinhood Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["<your RPC>"] } } };
const pub = createPublicClient({ chain, transport: custom(window.ethereum) });
const wallet = createWalletClient({ chain, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();
const vaultAbi = parseAbi(["function previewDeposit(uint256 assets) view returns (uint256)"]);
const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);
const buildersAbi = parseAbi(["function deposit(address vault, uint256 assets, bytes32 code, uint256 minShares) returns (uint256 shares)"]);
const assets = parseUnits("1.5", 18); // 1500000000000000000n
const code = stringToHex("route", { size: 32 }); // 0x726f757465000…000
const quoted = await pub.readContract({ address: NVDA_VAULT, abi: vaultAbi, functionName: "previewDeposit", args: [assets] });
const minShares = quoted * 995n / 1000n; // e.g. 1497000000000000000n gives 1489515000000000000n
const approval = await wallet.writeContract({ account, address: NVDA, abi: erc20Abi, functionName: "approve", args: [BUILDERS, assets] });
await pub.waitForTransactionReceipt({ hash: approval });
const hash = await wallet.writeContract({ account, address: BUILDERS, abi: buildersAbi, functionName: "deposit", args: [NVDA_VAULT, assets, code, minShares] });
await pub.waitForTransactionReceipt({ hash });
// The shares are in the account's wallet.# With Foundry's cast, from the wallet that holds the NVDA ($RPC is your own endpoint)
cast call 0x508E23274C39C60c345e35B1f0188bE180717CB9 "previewDeposit(uint256)(uint256)" 1500000000000000000 --rpc-url $RPC
# 1497000000000000000 -> minShares = 1489515000000000000
cast send 0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC "approve(address,uint256)" \
0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291 1500000000000000000 \
--rpc-url $RPC --private-key $KEY
cast send 0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291 "deposit(address,uint256,bytes32,uint256)" \
0x508E23274C39C60c345e35B1f0188bE180717CB9 1500000000000000000 \
0x726f757465000000000000000000000000000000000000000000000000000000 1489515000000000000 \
--rpc-url $RPC --private-key $KEY
# The same two transactions as raw calldata (selectors: approve 0x095ea7b3, deposit 0xec2ac54e)
to: 0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC
data: 0x095ea7b3000000000000000000000000082ae609bdc7cceec0c58b2361a21c49dc2ae29100000000000000000000000000000000000000000000000014d1120d7b160000
to: 0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291
data: 0xec2ac54e000000000000000000000000508e23274c39c60c345e35b1f0188be180717cb900000000000000000000000000000000000000000000000014d1120d7b160000726f75746500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014abd2002e9db000Standings
The first codes registered.
Ranked by deposits referred, from the public statement. A seat promises nothing beyond the terms below.
The first referred deposit writes the board.
Seats show as open until a code has a referred deposit on chain. The statement is rebuilt every hour.
Reference
Everything the contract does, and how to read it.
01Quickstart: the link
Nothing to build. Send people to your link; for 90 days after that visit, every deposit they make on borro.markets carries your code.
https://borro.markets/?ref=<code>| Detail | Value |
|---|---|
| Code format | Lowercase letters, digits, - and _, up to 31 characters. |
| Window | 90 days from the visit. The code is kept in the visitor's browser, lowercase. |
| Shown to the visitor | On the Supply tab, "Deposits credit bookrunner <code>", with a way to clear it. |
| Borrows | The first borrow, leveraged long or short asks the wallet once for a free signature: "This is credited to bookrunner code <code>". |
02One call, worked
Two transactions from the depositor's wallet: approve the stock token (or USDG) to the Bookrunners contract, then:
deposit(address vault, uint256 assets, bytes32 code, uint256 minShares) returns (uint256 shares)| Argument | Meaning |
|---|---|
vault | One of Borro's vaults, listed under The contract. |
assets | The deposit, in units of the vault's underlying token. |
code | Your code as a bytes32 of its ASCII text, right-padded with zeros. |
minShares | The fewest shares the vault may give: previewDeposit(assets) less the slip you allow. The site allows 0.5%. |
returns shares | The vault shares, sent to the caller exactly as a deposit at the vault would. The contract keeps nothing between transactions. |
Worked example: 1.5 NVDA, code route
| Bookrunners contract | 0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291 |
|---|---|
| NVDA vault (bNVDA) | 0x508E23274C39C60c345e35B1f0188bE180717CB9 |
| NVDA token | 0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC |
| assets | 1500000000000000000 (1.5 NVDA, 18 decimals) |
| code | 0x726f757465000000000000000000000000000000000000000000000000000000 ("route") |
| previewDeposit | 1497000000000000000 (what the vault quoted when this example was written; read it fresh) |
| minShares | 1489515000000000000 (previewDeposit × 995 / 1000) |
The code for this call, in ethers, viem or raw calldata, is in the card above. The same call lends USDG: pass the USDG vault, USDG units (6 decimals) and approve USDG instead.
Reverts: NotRegistered if the code has no payee, BadVault if the vault is not one of Borro's, TooFewShares(got, wanted) if the vault would give fewer shares than minShares.
03The contract
| BorroBuilders | Not on chain yet |
|---|---|
| Chain | Robinhood Chain, chain id 4663 |
| Deployed | block 88624905, deploy transaction |
| Owner | 0x8A22d6e75e9427b9D57e42a7984200029e7E173C (the Borro multisig; can list vaults and sweep, cannot touch codes or deposits) |
| Source | contracts/src/BorroBuilders.sol, Solidity 0.8.28, GPL-2.0-or-later |
ABI
[
"function register(bytes32 code)",
"function setPayee(bytes32 code, address to)",
"function deposit(address vault, uint256 assets, bytes32 code, uint256 minShares) returns (uint256 shares)",
"function builders(bytes32 code) view returns (address payee)",
"function vaults(address vault) view returns (bool listed)",
"event Registered(bytes32 indexed code, address indexed payee)",
"event Referred(bytes32 indexed code, address indexed who, address indexed vault, uint256 assets, uint256 shares)",
"event VaultListed(address indexed vault, address indexed asset, bool listed)",
"error BadCode()",
"error Taken()",
"error NotPayee()",
"error BadPayee()",
"error NotRegistered()",
"error BadVault()",
"error Nothing()",
"error TooFewShares(uint256 got, uint256 wanted)"
]| Name | Meaning |
|---|---|
register(code) | Claims an unclaimed, non-zero code for msg.sender, who becomes its payee. First come, first served. |
setPayee(code, to) | Moves a code to another wallet. Only the code's current payee can; to cannot be the zero address. Announced as another Registered, so a code's whole history is in its Registered events. |
deposit(vault, assets, code, minShares) | Pulls assets of the vault's asset from the caller, deposits them to the vault in the caller's name, checks shares >= minShares, emits Referred. Reentrancy-guarded. |
builders(code) | The wallet a code is paid to; the zero address if the code is free. |
vaults(vault) | Whether a vault is one deposits may go to. |
Registered | A code was claimed or moved: code, payee. |
Referred | A referred deposit: code, who (the depositor), vault, assets in, shares out. The keeper counts these. |
VaultListed | The owner listed or unlisted a vault. |
BadCode | register was given the zero code. |
Taken | The code already has a payee. |
NotPayee | setPayee from a wallet that is not the code's payee. |
BadPayee | setPayee to the zero address. |
NotRegistered | deposit with a code nobody has claimed. |
BadVault | The vault is not listed, or reports no asset. |
Nothing | deposit with assets of zero. |
TooFewShares(got, wanted) | The vault gave fewer shares than minShares; the deposit is undone. |
Owner only: setVault(address vault, bool listed) and sweep(address token, address to), for anything sent to the contract by mistake. Between transactions it holds nothing of anyone's.
The vaults it deposits to
Nine stock vaults and the USDG vault. The underlying token is what you approve to the Bookrunners contract, and what assets is counted in.
| Vault | Vault address | Underlying token | Decimals |
|---|
Registering from code
The app registers for you at /app#builders. From code, with the ABI above: register(encodeBytes32String("route")) from the wallet to be paid, then setPayee(code, to) to pay another wallet.
Finding your deposits
Every referred deposit is a Referred log on the contract. On Blockscout, the Logs tab lists them; your code is the first indexed topic. From an RPC, filter from block 88624905:
// topic0 is Referred's signature hash, topic1 your code as bytes32
const logs = await provider.getLogs({
address: "0x082Ae609bdC7ccEec0C58B2361a21c49dc2aE291",
fromBlock: 88624905,
topics: [
"0x0ceba4fd933fd5fe81eae9e5fd1c015a5178d13c4d7db4a0b84e4a1062eb5511", // Referred(bytes32,address,address,uint256,uint256)
ethers.encodeBytes32String("route"),
],
});04Statement API
What each code brought, earned and was paid, rebuilt by the keeper from the chain's events and republished every hour. Public, no key.
GET https://borro.markets/builders.jsonAn example, with made-up numbers:
{
"contract": "0x082ae609bdc7cceec0c58b2361a21c49dc2ae291",
"terms": { "fullMonths": 12, "after": 0.5, "payoutDay": "monthly" },
"updatedAt": 1760515200,
"lastDay": "2026-10-14",
"codes": {
"route": {
"payee": "0x1111111111111111111111111111111111111111",
"lots": 42,
"referred": {
"0x508E23274C39C60c345e35B1f0188bE180717CB9": "63000000000000000000",
"0xF8e0ab443098c0738489B5BAFFbd32A1341F2289": "2500000000"
},
"valueUsdg": "14428.400000",
"owed": "38.215000",
"paid": "12.900000",
"payouts": [
{ "at": 1759363200, "tx": "0x6f1e…", "usdg": "12.900000", "month": "2026-09" }
]
}
},
"days": [
{ "day": "2026-10-13", "byCode": { "route": "1.187500" } },
{ "day": "2026-10-14", "byCode": { "route": "1.302500" } }
]
}| Field | Meaning |
|---|---|
contract | The Bookrunners contract the statement was built from, lowercase. |
terms | fullMonths: months a lot is paid at 100%. after: the share after that. payoutDay: how often payouts go out. |
updatedAt | Unix seconds of the chain block the statement was built at. |
lastDay | The last whole UTC day counted, or null before the first. |
codes.<code>.payee | The wallet the code is paid to, as the contract names it now. |
codes.<code>.lots | How many referred deposits the code has had. |
codes.<code>.referred | Per vault, the vault shares still counted for the code, in the vault's share units (a string of wei). |
codes.<code>.valueUsdg | What those shares were worth at the last day's close, in USDG. |
codes.<code>.owed | Everything the code has earned so far, in USDG; paid is how much of that has gone out. |
codes.<code>.payouts | Each payout: block time, transaction, USDG, the month it covered. |
days | The last 90 UTC days: what each code earned that day. |
codes.<code>.depositFee, borrowFee | Once borrows are counted: what deposits and borrows have earned the code so far, in USDG. owed is their sum. |
codes.<code>.borrowers | Once borrows are counted: how many wallets have signed for the code. |
totals | Once borrows are counted: the same sums over every code, and the last day the borrows were counted. |
Every USDG amount is a string with six decimals. Share amounts are strings of whole share units. Before the keeper's first run the route answers 404 with { "error": "no builders statement published yet" }.
05Borrows: one signature
A borrower credits your code with a free signature, not a transaction. The app asks for it the first time a wallet that came by your link borrows; an integrator or an agent sends it here. One code per wallet: the first binding stands, and its 12 months start at the signature.
// EIP-712, signed by the borrowing wallet (ethers v6): no gas, nothing moves
const timestamp = Math.floor(Date.now() / 1000);
const domain = { name: "Borro Builders", version: "1", chainId: 4663 };
const types = { BuilderBinding: [
{ name: "statement", type: "string" }, { name: "account", type: "address" }, { name: "code", type: "string" },
{ name: "chainId", type: "uint256" }, { name: "timestamp", type: "uint256" },
] };
const message = { statement: "Credit my Borro borrowing to builder code route", account, code: "route", chainId: 4663, timestamp };
const signature = await signer.signTypedData(domain, types, message);
await fetch("https://borro.markets/builders/bind", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ account, code: "route", timestamp, signature }),
}); // 201 { ok: true, binding: { account, code, timestamp, at } }| Field | Meaning |
|---|---|
account | The borrowing wallet. Its own key signs; a contract wallet cannot bind yet. |
code | Your code, as registered. |
timestamp | Unix seconds when it was signed. Taken for 10 minutes. |
signature | The wallet's signature of the typed data above. |
| Answers | 201 kept. 400 a bad shape, time or signature, or a wallet with no ETH and no transaction. 404 a code nobody registered. 409 the wallet is bound already. |
| Reading | GET /builders/bind/<wallet> for one wallet, GET /builders/bindings for all of them, signatures included. |
You earn Borro's fee on the interest the wallet pays in Borro's markets, shorts included: all of it for 12 months, half after. A fee is never paid twice: the part a referred deposit already earns stays that deposit's bookrunner's.
06Agents
An agent credits a code the same way: it calls deposit on the Bookrunners contract from the wallet it acts for, with the ABI and calldata above.
| Path | What it does |
|---|---|
deposit on the contract | A vault deposit with your code attached. The shares go to the wallet the agent acts for. |
MCP supply tool | Borro's MCP server (https://borro.markets/mcp, set up at /agents) prepares a plain vault deposit and takes no bookrunner code. An agent that wants the credit builds the deposit call itself. |
MCP bind_builder tool | Returns the typed data for the wallet to sign, then submits the signature: the wallet's borrowing is credited to your code. |
07Terms
- Get your code with the wallet that will be paid
Anyone can. A code is first come, first served, and only its payee can change where it is paid.
- Share your link
borro.markets/?ref=<code>. For 90 days after a visit, that visitor's deposits carry your code. Integrators can call the contract directly instead.
- Every referred deposit is an on-chain event
Anyone can check it. Each is a lot credited to you while it is in the vault; withdrawals come off a wallet's oldest lots first, so the lots still at 100% stay counted.
- You are paid your lots' part of the performance fee
The fee is the vault's own, 10% of the rent. All of it for a lot's first 365 days, half of it after that.
- Borrowers count too
A borrower who signs for your code earns you Borro's fee on the interest it pays: all of it for 12 months from the signature, half after.
- Size counts
The fee share pays from the first dollar you bring. The Bookrunner Drip is for size: a code shares a day's Drip when the deposits it referred add up to $50,000 or more, all its users together.
- Paid monthly in USDG
To the code's payee, by the keeper, in every month a code is owed at least $1. Less carries over to the next month.
- The statement is public
What each code brought, earned and was paid, updated hourly, in the app and at borro.markets/builders.json.
What to know
Rent floats with demand
If nobody borrows the stock there is no rent, no fee and nothing to share. No income is promised.
Lenders pay nothing extra
The split is between Borro and you, out of a fee the vaults already take.
The $BORRO Drip stays with the depositor
None of it goes to the bookrunner.
Your cut comes out of the $BORRO buyback
It is the part of the fee that would otherwise buy back and burn $BORRO. In a lot's 100% window, none of its fee funds a buyback.
08Get in touch
Placing a card, building on the contract, or a question about the statement: write to team@borro.markets.
Borro