Guides
Build an agent
Drive a fund from code with the SDK and the API.
An agent is any program holding the fund's agent key. It drives the fund through the same API and instructions as the app, and every rule is still enforced on chain.
The SDK
The TypeScript SDK lives in web/lib/protocol and uses @solana/web3.js:
| Export | What it does |
|---|---|
NodClient | Signs in, prepares and submits exact transactions, follows receipts, requests routes |
buildTransaction | Builds a v0 transaction with a fresh finalized blockhash and the one-time intent memo |
proposeInstruction, directSwapInstruction | Post a proposal or swap inside the budget, from a committed route |
investorInstructions | Deposit, redeem and update NAV |
conditionalInstructions, marketInstructions | Split, merge, redeem copies; trade; add and remove liquidity |
decisionInstruction, executeInstruction, expireInstruction | Start, finalize, execute and expire |
JupiterClient | Quotes a route from Jupiter directly and composes it onto the fund's vaults |
derivePushFeed | The Pyth push feed account for a feed ID |
A proposal from code
import { Connection, PublicKey } from "@solana/web3.js";
import { NodClient, buildTransaction, derivePushFeed, proposeInstruction } from "./lib/protocol";
const connection = new Connection(rpcUrl, "confirmed");
const api = new NodClient("https://<your-nod-app>/api/v1");
await api.authenticate(wallet); // signs the one-time sign-in message
const fund = await api.fund(fundAddress);
const route = await api.route({ fund: fundAddress, direction: "buy", amount: 3_000_000_000n, slippageBps: 100 });
const action = {
program: new PublicKey(route.action.program),
accounts: route.action.accounts.map(meta => ({ ...meta, pubkey: new PublicKey(meta.pubkey) })),
data: Buffer.from(route.action.data, "base64"),
inputAmount: BigInt(route.action.inputAmount),
minOutput: BigInt(route.action.minOutput),
quoteToAsset: route.action.quoteToAsset,
};
const instruction = proposeInstruction({
agent: wallet.publicKey, fund: new PublicKey(fundAddress), data: fund.data,
sequence: BigInt(fund.data.nextSequence), oracle: derivePushFeed(assetFeedId), quoteOracle: derivePushFeed(usdcFeedId), action,
});
const table = (await connection.getAddressLookupTable(new PublicKey(fund.lookupTable))).value!;
const transaction = await buildTransaction(connection, wallet.publicKey, [instruction], [table]);
const receipt = await api.signAndSubmit(wallet, transaction, "proposal:2026-10-06:buy-sol", [table]);
await api.waitForTransaction(receipt.id);
The amount is in base units: 3,000 USDC is 3_000_000_000n. assetFeedId is the registered asset's Pyth feed ID and usdcFeedId the configuration's USDC/USD feed ID, both 32 bytes. The fund's lookup table keeps a proposal, which carries its whole route as data, inside one transaction.
Idempotency
Give every intended action a stable idempotency key of 8 to 128 characters. Retrying with the same key returns the same prepared transaction and never signs a second one. A new key is a new action.
If a submission's answer is lost, the SDK throws SubmissionUncertainError with the transaction's ID; retrySubmission sends exactly the same signed bytes again. If another signed transaction for the same fund is still settling, the API answers PENDING_INTENT_CONFLICT with that receipt's ID before your wallet is asked to sign.
The reference agent
npm run protocol:agent in web runs one configured buy or sell, as a proposal or inside the budget, then exits. It reads its settings from the environment:
| Variable | Value |
|---|---|
NOD_API_URL | The app's API, ending in /api/v1 |
SOLANA_RPC_URL, NOD_EXPECTED_GENESIS_HASH | The network to act on, and its genesis hash |
NOD_AGENT_KEYPAIR | Path to the agent's keypair file |
NOD_FUND | The fund's address |
NOD_PRICE_UPDATE, NOD_QUOTE_PRICE_UPDATE | The asset/USD and USDC/USD price accounts |
NOD_SWAP_DIRECTION | buy or sell |
NOD_AGENT_MODE | proposal or direct |
NOD_SWAP_INPUT, NOD_SWAP_MINIMUM_OUTPUT | Base units in, and the least out |
NOD_SLIPPAGE_BPS | 0 to 500 |
NOD_IDEMPOTENCY_KEY | A stable key for this one action |
JUPITER_API_URL, JUPITER_API_KEY | Jupiter's Swap API and its key |
It signs in, checks it is the fund's agent, quotes a route, commits it and waits for the receipt. Run it again with the same key and it reports the existing receipt instead of acting twice.