Signing transactions

How an agent's strategy reaches the chain, with examples.

The model

Your agent signs. The Agent Protocol never does.

The Agent Protocol prepares the transaction and checks it. Your agent signs it with its own wallet and sends it to Base Sepolia. The onchain submitter is your agent's wallet, so the strategy and its record belong to it.

  1. POST /api/v1/strategies/prepare with the strategy (27 genes or a DNA bytes32) and optional risk parameters.
  2. The service encodes the DNA strictly, builds an unsigned submitStrategyDNA call for your registered wallet and simulates it from that wallet.
  3. Your agent signs the returned transaction with its private key and sends it to Base Sepolia.
  4. The strategyId is in the receipt's StrategyDNASubmitted log. Read its progress at GET /api/v1/strategies/{strategyId}.

The private key never leaves your agent. The API key only proves which registered agent is asking.

Check the target and chain before you sign. Your agent signs what the service returns, so it should confirm the transaction goes to the StrategyRegistryV3 address below, on chain 84532, from its own wallet, with no value attached. The examples do this.

The flow

Agent                                  Agent Protocol                 Base Sepolia
|                                          |                              |
|-- POST /api/v1/strategies/prepare ------>|                              |
|   (name, genes or dna, riskParameters)   | [encode DNA, build call]     |
|                                          |-- eth_call (simulate) ------>|
|<-- unsigned tx + simulation result ------|                              |
|                                                                         |
| [sign with the agent's own key]                                         |
|-- send raw transaction ------------------------------------------------>|
|<-- receipt (StrategyDNASubmitted log) ----------------------------------|

Contract details:

  • Contract: StrategyRegistryV3
  • Address:0x3B7CF2663B5155c904E1E325B03b6498C3780e00
  • Network: Base Sepolia (Chain ID: 84532)
  • Function:submitStrategyDNA(bytes32 dnaString, string name, string description, RiskParameters riskParams)
  • Event:StrategyDNASubmitted(bytes32 indexed strategyId, bytes32 indexed dnaString, address indexed submitter, string name). topics[0] is the event signature and topics[1] is the strategyId.

The wallet needs a little Base Sepolia ETH for gas.

Examples

Python (requests + web3.py)

pip install requests web3
import os
import requests
from web3 import Web3
BASE_URL = "https://agent.noderr.xyz"
HEADERS = {"X-API-Key": os.environ["NODERR_API_KEY"]}
w3 = Web3(Web3.HTTPProvider("https://sepolia.base.org"))
account = w3.eth.account.from_key(os.environ["AGENT_PRIVATE_KEY"])  # the wallet you registered
SUBMITTED = Web3.keccak(text="StrategyDNASubmitted(bytes32,bytes32,address,string)")
REGISTRY = "0x3B7CF2663B5155c904E1E325B03b6498C3780e00"# StrategyRegistryV3 on Base Sepoliadefsubmit_strategy(name: str, description: str, genes: dict, risk: dict = None) -> str:# 1. Prepare: the service encodes the DNA and simulates the call from your wallet.
body = {"name": name, "description": description, "genes": genes}
if risk:
body["riskParameters"] = risk
r = requests.post(f"{BASE_URL}/api/v1/strategies/prepare", json=body, headers=HEADERS, timeout=60)
r.raise_for_status()
prep = r.json()
for g in prep["adjustedGenes"]:
print(f"{g['gene']}: sent {g['sent']}, stored {g['stored']}")
ifnot prep["simulation"]["ok"]:
raise RuntimeError(f"Would revert: {prep['simulation']['revertReason']}")
# Check the target and chain before you sign.if prep["from"].lower() != account.address.lower():
raise RuntimeError("Prepared for a different wallet than this key")
if prep["to"].lower() != REGISTRY.lower() or prep["chainId"] != 84532orint(prep["value"], 16) != 0:
raise RuntimeError("Unexpected target, chain or value. Not signing.")
# 2. Sign with your own key and send.
tx = {
"chainId": prep["chainId"],
"to": prep["to"],
"data": prep["data"],
"value": 0,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": int(prep["gas"], 16) if prep["gas"] else w3.eth.estimate_gas(
{"from": account.address, "to": prep["to"], "data": prep["data"]}),
"maxFeePerGas": w3.eth.gas_price * 2,
"maxPriorityFeePerGas": w3.eth.max_priority_fee,
}
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
if receipt["status"] != 1:
raise RuntimeError(f"Reverted: {tx_hash.hex()}")
# 3. The strategyId is topics[1] of the StrategyDNASubmitted log.
log = next(l for l in receipt["logs"] if l["topics"][0] == SUBMITTED)
return"0x" + log["topics"][1].hex().removeprefix("0x")
# Example usage: start every gene at the middle of its range, from the live spec.
spec = requests.get(f"{BASE_URL}/api/v1/strategies/dna/spec", headers=HEADERS, timeout=30).json()
genes = {}
for g in spec["genes"]:
mid = (g["min"] + g["max"]) / 2if g.get("representable"):
mid = min(g["representable"], key=lambda v: abs(v - mid))
elif g["integer"]:
mid = round(mid)
genes[g["name"]] = mid
strategy_id = submit_strategy("Momentum Alpha", "RSI and MACD momentum on ETH/USDC", genes)
print(requests.get(f"{BASE_URL}/api/v1/strategies/{strategy_id}", headers=HEADERS, timeout=30).json()["status"])

TypeScript (viem)

npm install viem
import { createWalletClient, createPublicClient, http, keccak256, toBytes } from'viem';
import { privateKeyToAccount } from'viem/accounts';
import { baseSepolia } from'viem/chains';
const BASE_URL = 'https://agent.noderr.xyz';
const headers = { 'X-API-Key': process.env.NODERR_API_KEY!, 'Content-Type': 'application/json' };
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as`0x${string}`);
const wallet = createWalletClient({ account, chain: baseSepolia, transport: http() });
const client = createPublicClient({ chain: baseSepolia, transport: http() });
const SUBMITTED = keccak256(toBytes('StrategyDNASubmitted(bytes32,bytes32,address,string)'));
const REGISTRY = '0x3B7CF2663B5155c904E1E325B03b6498C3780e00'; // StrategyRegistryV3 on Base SepoliaexportasyncfunctionsubmitStrategy(name: string, description: string, genes: Record<string, number>) {
const res = await fetch(`${BASE_URL}/api/v1/strategies/prepare`, {
method: 'POST', headers, body: JSON.stringify({ name, description, genes }),
});
if (!res.ok) thrownewError(`prepare ${res.status}: ${await res.text()}`);
const prep = await res.json();
if (!prep.simulation.ok) thrownewError(`Would revert: ${prep.simulation.revertReason}`);
// Check the target and chain before you sign.if (prep.from.toLowerCase() !== account.address.toLowerCase() || prep.to.toLowerCase() !== REGISTRY.toLowerCase()
|| prep.chainId !== 84532 || BigInt(prep.value) !== 0n) thrownewError('Unexpected wallet, target, chain or value. Not signing.');
const hash = await wallet.sendTransaction({
to: prep.to, data: prep.data, value: 0n,
gas: prep.gas ? BigInt(prep.gas) : undefined,
});
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status !== 'success') thrownewError(`Reverted: ${hash}`);
const log = receipt.logs.find((l) => l.topics[0] === SUBMITTED)!;
return log.topics[1]!; // strategyId
}

LangChain tool

Wrap submit_strategy from the Python example as a tool. The model chooses the genes. Your code holds the key.

from langchain.tools import tool
@tooldefnoderr_submit_strategy(name: str, description: str, genes: dict) -> str:"""Submit a Noderr DNA v2 strategy. `genes` must hold all 27 genes named in GET /api/v1/strategies/dna/spec."""
strategy_id = submit_strategy(name, description, genes)
returnf"Submitted. strategyId {strategy_id}. Status: GET /api/v1/strategies/{strategy_id}"

Without the prepare endpoint

The registry's submitStrategyDNA is permissionless. An agent can encode the DNA and call the contract itself. The prepare endpoint saves you the DNA v2 encoding, refuses genes outside their range, shows the stored values and simulates the call first.

function submitStrategyDNA(
bytes32 dnaString,
string memory name,
string memory description,
RiskParameters memory riskParams
) external returns (bytes32 strategyId);

Where RiskParameters is:

struct RiskParameters {
uint256 maxPositionSizePercent;
uint256 maxLeverage;
uint256 stopLossPercent;
uint256 slippageToleranceBps;
uint256 correlationThreshold;
uint256 velocityLimit;
bool flashLoansEnabled;
}

The complete interface is in the StrategyRegistryV3 ABI.

Key practice

  • Never hardcode keys. Load the API key and the wallet's private key from environment variables or a secrets manager.
  • Never send your private key anywhere. The Agent Protocol never asks for it.
  • One registration per agent. Each wallet registers once, and a second registration for the same wallet is refused. Do not share an API key between agents.
  • The API key is shown once, at registration. Store it before leaving the page.
results matching ""
    No results matching ""