Skip to main content
Version: Next

Solana Agent Integration with Fetch.ai uAgents

This Solana Devnet demo wires three local uAgents to Solana wallets: EscrowAgent, PlayerAgent, and ChallengerAgent. Players transfer SOL to escrow first, then send a typed message that includes the deposit transaction signature. Escrow verifies both deposits on Devnet, compares each spot BTC/USDT guess to Binance, and pays the full matched pot to the closer guess.

This is a local demo with a configured Escrow address. Agents use endpoint= on loopback. They do not discover each other through Almanac. Copy the Escrow uAgent address and Solana pubkey from Escrow startup logs into .env before starting the players.

See also AI Agents Reshaping the On-Chain Ecosystem and BNB Chain Agents. Solana secret lists are wallet keypairs (64-byte JSON integer arrays). The uAgent seed= string is a separate Fetch.ai identity; it does not derive your Solana pubkey.

Devnet only — not a real betting product

This tutorial is a Solana Devnet programming sample. Do not use mainnet, real funds, or production keys. It is not financial advice, not a licensed gambling product, and not a price-prediction market. Any keys that ever appeared in git (including the linked community repo) are burned — generate new keypairs.

Community sample

The original walkthrough lived in a personal repo, abhifetch/solana-fetch-uagents-integration (last updated 2024). Treat that tree as community / stale: it pins uagents ^0.1.0 and Python 3.8, and its example.env / README published full secret arrays. Do not copy those keys. Use the code on this page with uagents==0.25.5.

Prerequisites

Ordered setup:

  1. Clone or create a project folder and cd into it.
  2. Install Python 3.10+ (current uAgents adapters expect ≥3.10; do not use 3.8).
  3. Install the Solana CLI and point it at Devnet.
  4. Create a venv, then install the pins below (Poetry is optional).
  5. Generate three new Solana keypairs (solana-keygen new). Convert each JSON file to an env var without pasting real keys into chat, docs, or git.
  6. Airdrop Devnet SOL to player, challenger, and escrow (escrow must cover payout plus network fees).
  7. Run Escrow first, copy its logged addresses into .env, then start Player and Challenger. Leave both clients running until they receive EscrowResponse.

Default local ports: Escrow :8000, Player :8001, Challenger :8002.

High-level architecture

Solana escrow agents: player and challenger deposit SOL, escrow settles a spot BTC guess
  1. Player and Challenger each send equal SOL amounts to the Escrow Solana wallet and wait for confirmation.
  2. They then message Escrow with amount, spot BTC/USDT guess, their Solana pubkey, and the deposit tx signature.
  3. Escrow verifies both deposits on Devnet, requires equal stakes, fetches Binance spot BTCUSDT, and transfers the entire pot to the closer guess. Solana fees are paid by the escrow wallet, so fund extra SOL there.

This is not a futures bet. Escrow compares guesses to the spot ticker at match time.

Fetch wallet vs Solana wallet

WalletWhat it isHow you fund it
Solana Devnet keypairESCROW_SECRET_LIST / PLAYER_SECRET_LIST / CHALLENGER_SECRET_LISTsolana airdrop … --url devnet
uAgent identityseed= string → agent.address (agent1…)Separate from SOL. fund_agent_if_low (if you import it) tops up the Fetch/Almanac wallet, not Devnet SOL. This demo uses local endpoints, so skip it unless you add mailbox/testnet registration.

Do not set AGENTVERSE_API_KEY unless you switch these agents to Agentverse mailbox. The scripts below do not read that variable.

Dependencies

python -m venv venv
source venv/bin/activate

Windows:

python -m venv venv
venv\Scripts\activate
requirements.txt
uagents==0.25.5
python-dotenv==1.0.1
requests==2.32.3
solana==0.36.6
solders==0.26.0
base58==2.1.1
pip install -r requirements.txt

Optional Poetry pins (replace any uagents = "^0.1.0" / python = "^3.8" from the old community repo):

pyproject.toml
[tool.poetry.dependencies]
python = "^3.10"
uagents = "0.25.5"
python-dotenv = "1.0.1"
requests = "2.32.3"
solana = "0.36.6"
solders = "0.26.0"
base58 = "2.1.1"

Keys and .env (placeholders only)

Format is a JSON integer array of 64 bytes, not Base64. solana-keygen new --outfile wallet.json already writes that array.

solana config set --url https://api.devnet.solana.com
solana-keygen new --outfile player-wallet.json --no-bip39-passphrase
solana-keygen new --outfile challenger-wallet.json --no-bip39-passphrase
solana-keygen new --outfile escrow-wallet.json --no-bip39-passphrase

Put each file’s single JSON line into .env yourself. Never commit .env or wallet JSON. Rotate any key that was ever committed (including prefixes shown in older versions of this page).

.env
# Placeholders — replace with YOUR 64-integer arrays. Do not reuse published keys.
PLAYER_SECRET_LIST=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
CHALLENGER_SECRET_LIST=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
ESCROW_SECRET_LIST=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

# Filled from EscrowAgent startup logs — do not hardcode a third-party address.
ESCROW_AGENT_ADDRESS=agent1q...your_local_escrow_address
ESCROW_SOLANA_PUBKEY=YourEscrowSolanaPubkeyBase58

PLAYER_BET_AMOUNT=0.1
PLAYER_BTC_GUESS=65000
CHALLENGER_BET_AMOUNT=0.1
CHALLENGER_BTC_GUESS=64000

[0,0,…] is invalid as a real keypair; it only shows the shape. After Escrow prints Escrow agent address and Escrow Solana pubkey, copy those values into ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY.

Shared models

models.py
from uagents import Model


class EscrowRequest(Model):
amount: float
price: float
public_key: str
deposit_tx_sig: str


class EscrowResponse(Model):
result: str

Utilities

check_balance takes a Solders Pubkey directly. get_latest_btc_price() returns None on failure (including Binance geo-blocks); escrow must not settle in that case. CoinGecko is a simple fallback.

functions.py
import json
import os

import requests
from dotenv import load_dotenv
from solana.rpc.api import Client
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.signature import Signature
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction

load_dotenv()

LAMPORTS_PER_SOL = 1_000_000_000
DEVNET_RPC = os.getenv("SOLANA_RPC_URL", "https://api.devnet.solana.com")
client = Client(DEVNET_RPC)


def load_keypair(env_name: str) -> Keypair:
raw = os.getenv(env_name)
if not raw:
raise SystemExit(f"Missing {env_name}. Add a 64-integer JSON array to .env.")
secret = json.loads(raw)
if not isinstance(secret, list) or len(secret) != 64:
raise SystemExit(
f"{env_name} must be a JSON array of 64 integers (Solana keypair bytes), not Base64."
)
return Keypair.from_bytes(bytes(secret))


def check_balance(pubkey: Pubkey) -> float:
balance_resp = client.get_balance(pubkey, commitment=Confirmed)
return balance_resp.value / LAMPORTS_PER_SOL


def transfer_sol(from_keypair: Keypair, to_pubkey_base58: str, amount_sol: float) -> str:
lamports = int(amount_sol * LAMPORTS_PER_SOL)
to_pubkey = Pubkey.from_string(to_pubkey_base58)
recent_blockhash = client.get_latest_blockhash(commitment=Confirmed).value.blockhash
instruction = transfer(
TransferParams(
from_pubkey=from_keypair.pubkey(),
to_pubkey=to_pubkey,
lamports=lamports,
)
)
transaction = Transaction.new_signed_with_payer(
[instruction],
from_keypair.pubkey(),
[from_keypair],
recent_blockhash,
)
result = client.send_raw_transaction(
bytes(transaction),
opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed),
)
sig = str(result.value)
client.confirm_transaction(Signature.from_string(sig), commitment=Confirmed)
return sig


def verify_deposit(tx_sig: str, expected_to: str, expected_sol: float) -> bool:
"""Confirm the tx landed and increased the escrow account by the expected lamports."""
sig = Signature.from_string(tx_sig)
escrow_pk = str(Pubkey.from_string(expected_to))
expected_lamports = int(expected_sol * LAMPORTS_PER_SOL)
resp = client.get_transaction(
sig,
encoding="json",
max_supported_transaction_version=0,
commitment=Confirmed,
)
if resp.value is None or resp.value.transaction.meta is None:
return False
meta = resp.value.transaction.meta
if meta.err is not None:
return False
keys = [str(k) for k in resp.value.transaction.transaction.message.account_keys]
if escrow_pk not in keys:
return False
idx = keys.index(escrow_pk)
delta = meta.post_balances[idx] - meta.pre_balances[idx]
return delta == expected_lamports


def get_latest_btc_price() -> float | None:
urls = (
"https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
)
try:
response = requests.get(urls[0], timeout=10)
response.raise_for_status()
return float(response.json()["price"])
except (requests.RequestException, KeyError, TypeError, ValueError):
pass
try:
response = requests.get(urls[1], timeout=10)
response.raise_for_status()
return float(response.json()["bitcoin"]["usd"])
except (requests.RequestException, KeyError, TypeError, ValueError) as exc:
print(f"Error fetching BTC spot price: {exc}")
return None

Escrow agent

Complete handler: equal-stake check, on-chain deposit verify, escrow balance check, spot price, full-pot payout, then reset.

escrow_agent.py
import os

from dotenv import load_dotenv
from uagents import Agent, Context

from functions import check_balance, get_latest_btc_price, load_keypair, transfer_sol, verify_deposit
from models import EscrowRequest, EscrowResponse

load_dotenv()

FEE_BUFFER_SOL = 0.01 # extra Devnet SOL so payout txs can pay network fees

escrow_keypair = load_keypair("ESCROW_SECRET_LIST")
escrow_pubkey_base58 = str(escrow_keypair.pubkey())

agent = Agent(
name="EscrowAgent",
port=8000,
seed="Escrow Wallet",
endpoint=["http://127.0.0.1:8000/submit"],
)


@agent.on_event("startup")
async def on_startup(ctx: Context):
ctx.logger.info("Escrow agent initialized, ready for bids (local demo).")
ctx.logger.info(f"Escrow agent address: {agent.address}")
ctx.logger.info(f"Escrow Solana pubkey: {escrow_pubkey_base58}")
ctx.logger.info(
"Copy those two values into ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY, then start Player and Challenger."
)
sol_balance = check_balance(escrow_keypair.pubkey())
ctx.logger.info(f"Escrow SOL balance: {sol_balance}")
if sol_balance < FEE_BUFFER_SOL:
ctx.logger.warning(
"Escrow wallet is underfunded for fees. Airdrop Devnet SOL before matching."
)
ctx.storage.set("bids_count", 0)


def _store_bid(ctx: Context, prefix: str, sender: str, msg: EscrowRequest):
ctx.storage.set(f"{prefix}_sender", sender)
ctx.storage.set(f"{prefix}_amount", msg.amount)
ctx.storage.set(f"{prefix}_price", msg.price)
ctx.storage.set(f"{prefix}_pubkey", msg.public_key)
ctx.storage.set(f"{prefix}_tx", msg.deposit_tx_sig)


def _reset(ctx: Context):
for prefix in ("first", "second"):
for field in ("sender", "amount", "price", "pubkey", "tx"):
ctx.storage.set(f"{prefix}_{field}", None)
ctx.storage.set("bids_count", 0)


@agent.on_message(model=EscrowRequest, replies={EscrowResponse})
async def escrow_request_handler(ctx: Context, sender: str, msg: EscrowRequest):
current_count = ctx.storage.get("bids_count") or 0
ctx.logger.info(f"Received EscrowRequest from {sender}")

if not verify_deposit(msg.deposit_tx_sig, escrow_pubkey_base58, msg.amount):
ctx.logger.error("Deposit verification failed; ignoring bid.")
await ctx.send(sender, EscrowResponse(result="Rejected: deposit not confirmed on Devnet"))
return

if current_count == 0:
_store_bid(ctx, "first", sender, msg)
ctx.storage.set("bids_count", 1)
ctx.logger.info("Stored first verified bid; waiting for a matching stake.")
return

if current_count != 1:
await ctx.send(sender, EscrowResponse(result="Rejected: escrow is busy"))
return

_store_bid(ctx, "second", sender, msg)
first_amount = float(ctx.storage.get("first_amount"))
second_amount = float(msg.amount)
if first_amount != second_amount:
ctx.logger.error("Unequal stakes; refusing to match.")
await ctx.send(
ctx.storage.get("first_sender"),
EscrowResponse(result="Rejected: stakes must be equal"),
)
await ctx.send(sender, EscrowResponse(result="Rejected: stakes must be equal"))
_reset(ctx)
return

pot = first_amount + second_amount
escrow_balance = check_balance(escrow_keypair.pubkey())
if escrow_balance < pot + FEE_BUFFER_SOL:
ctx.logger.error(
f"Escrow SOL {escrow_balance} cannot pay pot {pot} plus fee buffer {FEE_BUFFER_SOL}."
)
await ctx.send(
ctx.storage.get("first_sender"),
EscrowResponse(result="Rejected: escrow underfunded for payout"),
)
await ctx.send(sender, EscrowResponse(result="Rejected: escrow underfunded for payout"))
_reset(ctx)
return

spot = get_latest_btc_price()
if spot is None:
ctx.logger.error("BTC spot price unavailable (Binance/CoinGecko). Not settling.")
await ctx.send(
ctx.storage.get("first_sender"),
EscrowResponse(result="Rejected: spot price feed unavailable"),
)
await ctx.send(sender, EscrowResponse(result="Rejected: spot price feed unavailable"))
_reset(ctx)
return

first_price = float(ctx.storage.get("first_price"))
second_price = float(msg.price)
first_diff = abs(first_price - spot)
second_diff = abs(second_price - spot)
ctx.logger.info(f"Spot BTC/USDT: {spot}. Diffs: {first_diff}, {second_diff}")

if first_diff <= second_diff:
winner, loser = ctx.storage.get("first_sender"), sender
winner_pk = ctx.storage.get("first_pubkey")
else:
winner, loser = sender, ctx.storage.get("first_sender")
winner_pk = msg.public_key

ctx.logger.info(f"Paying full pot {pot} SOL to {winner_pk} (no house cut).")
transfer_sol(escrow_keypair, winner_pk, pot)
await ctx.send(winner, EscrowResponse(result="You Won"))
await ctx.send(loser, EscrowResponse(result="You Lost"))
_reset(ctx)


if __name__ == "__main__":
agent.run()

Run:

python escrow_agent.py

Player agent

Bets come from CLI flags or env, parsed before agent.run(). There is no input() inside @agent.on_event("startup") (that would block the event loop). Transfer SOL and confirm before sending EscrowRequest. Keep this process alive to receive EscrowResponse.

player_agent.py
import argparse
import os

from dotenv import load_dotenv
from uagents import Agent, Context

from functions import check_balance, load_keypair, transfer_sol
from models import EscrowRequest, EscrowResponse

load_dotenv()


def parse_args():
parser = argparse.ArgumentParser(description="Player: Devnet SOL spot-BTC guess")
parser.add_argument(
"--amount",
type=float,
default=float(os.getenv("PLAYER_BET_AMOUNT", "0")),
)
parser.add_argument(
"--price",
type=float,
default=float(os.getenv("PLAYER_BTC_GUESS", "0")),
)
return parser.parse_args()


args = parse_args()
if args.amount <= 0 or args.price <= 0:
raise SystemExit("Set PLAYER_BET_AMOUNT and PLAYER_BTC_GUESS or pass --amount and --price.")

escrow_agent_address = os.getenv("ESCROW_AGENT_ADDRESS")
escrow_solana_pubkey = os.getenv("ESCROW_SOLANA_PUBKEY")
if not escrow_agent_address or not escrow_solana_pubkey:
raise SystemExit("Set ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY from Escrow startup logs.")

agent_keypair = load_keypair("PLAYER_SECRET_LIST")
agent_pubkey_base58 = str(agent_keypair.pubkey())

agent = Agent(
name="PlayerAgent",
port=8001,
seed="Player Escrow Wallet 1",
endpoint=["http://127.0.0.1:8001/submit"],
)


@agent.on_event("startup")
async def starter_function(ctx: Context):
ctx.logger.info(f"Initial SOL: {check_balance(agent_keypair.pubkey())}")
ctx.logger.info(f"Spot BTC/USDT guess: {args.price}; stake: {args.amount} SOL")
sig = transfer_sol(agent_keypair, escrow_solana_pubkey, args.amount)
ctx.logger.info(f"Deposit confirmed: {sig}")
await ctx.send(
escrow_agent_address,
EscrowRequest(
amount=args.amount,
price=args.price,
public_key=agent_pubkey_base58,
deposit_tx_sig=sig,
),
)
ctx.logger.info("Waiting for EscrowResponse — keep this process running.")
ctx.logger.info(f"SOL after deposit: {check_balance(agent_keypair.pubkey())}")


@agent.on_message(model=EscrowResponse)
async def on_escrow_response(ctx: Context, sender: str, msg: EscrowResponse):
ctx.logger.info(
f"{msg.result}. Updated SOL: {check_balance(agent_keypair.pubkey())}"
)


if __name__ == "__main__":
agent.run()
python player_agent.py --amount 0.1 --price 65000

Challenger agent

Same protocol on port 8002. Do not call fund_agent_if_low here: it would fund the Fetch wallet, not SOL.

challenger_agent.py
import argparse
import os

from dotenv import load_dotenv
from uagents import Agent, Context

from functions import check_balance, load_keypair, transfer_sol
from models import EscrowRequest, EscrowResponse

load_dotenv()


def parse_args():
parser = argparse.ArgumentParser(description="Challenger: Devnet SOL spot-BTC guess")
parser.add_argument(
"--amount",
type=float,
default=float(os.getenv("CHALLENGER_BET_AMOUNT", "0")),
)
parser.add_argument(
"--price",
type=float,
default=float(os.getenv("CHALLENGER_BTC_GUESS", "0")),
)
return parser.parse_args()


args = parse_args()
if args.amount <= 0 or args.price <= 0:
raise SystemExit(
"Set CHALLENGER_BET_AMOUNT and CHALLENGER_BTC_GUESS or pass --amount and --price."
)

escrow_agent_address = os.getenv("ESCROW_AGENT_ADDRESS")
escrow_solana_pubkey = os.getenv("ESCROW_SOLANA_PUBKEY")
if not escrow_agent_address or not escrow_solana_pubkey:
raise SystemExit("Set ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY from Escrow startup logs.")

agent_keypair = load_keypair("CHALLENGER_SECRET_LIST")
agent_pubkey_base58 = str(agent_keypair.pubkey())

agent = Agent(
name="Challenger",
port=8002,
seed="Challenger Escrow Wallet 2",
endpoint=["http://127.0.0.1:8002/submit"],
)


@agent.on_event("startup")
async def starter_function(ctx: Context):
ctx.logger.info(f"Initial SOL: {check_balance(agent_keypair.pubkey())}")
sig = transfer_sol(agent_keypair, escrow_solana_pubkey, args.amount)
ctx.logger.info(f"Deposit confirmed: {sig}")
await ctx.send(
escrow_agent_address,
EscrowRequest(
amount=args.amount,
price=args.price,
public_key=agent_pubkey_base58,
deposit_tx_sig=sig,
),
)
ctx.logger.info("Waiting for EscrowResponse — keep this process running.")


@agent.on_message(model=EscrowResponse)
async def on_escrow_response(ctx: Context, sender: str, msg: EscrowResponse):
ctx.logger.info(
f"{msg.result}. Updated SOL: {check_balance(agent_keypair.pubkey())}"
)


if __name__ == "__main__":
agent.run()
python challenger_agent.py --amount 0.1 --price 64000

Fund wallets on Devnet

Replace the pubkeys with solana-keygen pubkey <wallet.json> output. Airdrop escrow as well: after both deposits, it must still pay the pot plus fees.

solana airdrop 2 <PLAYER_PUBKEY> --url devnet
solana airdrop 2 <CHALLENGER_PUBKEY> --url devnet
solana airdrop 2 <ESCROW_PUBKEY> --url devnet
solana balance <ESCROW_PUBKEY> --url devnet

Run order

  1. Start Escrow and copy Escrow agent address / Escrow Solana pubkey into .env.
  2. Restart is not required for Escrow; update .env then start the clients.
  3. Start Player; leave it running.
  4. Start Challenger; leave it running until both log You Won or You Lost.
python escrow_agent.py
python player_agent.py --amount 0.1 --price 65000
python challenger_agent.py --amount 0.1 --price 64000

Sample output

EscrowAgent:

INFO: [EscrowAgent]: Escrow agent initialized, ready for bids.
INFO: [EscrowAgent]: Received escrowRequest message
INFO: [EscrowAgent]: Storing first request ...
INFO: [EscrowAgent]: Received escrowRequest message
INFO: [EscrowAgent]: Storing second request ...
INFO: [EscrowAgent]: Processing bids to determine the winner.
INFO: [EscrowAgent]: First difference: 1820.0, Second difference: 586820.0
INFO: [EscrowAgent]: Transferring 0.9 SOL to winner ...
INFO: [EscrowAgent]: Notifying winner and loser.
What is the amount of SOL you want to deposit? 0.5
What is the price of Bitcoin you want to bid at? 65000
INFO: [PlayerAgent]: Transfer result: ...
INFO: [PlayerAgent]: Final agent balance: 4.31998 SOL
INFO: [PlayerAgent]: You Won. Updated account balance: 5.21998 SOL

Troubleshooting

SymptomWhat to check
Missing PLAYER_SECRET_LIST / cannot decode key.env value is a JSON array of 64 integers, no trailing comma. Call load_dotenv() before os.getenv. Not Base64.
Deposit verification failedTransfer before the uAgents message; wait for confirmation; ESCROW_SOLANA_PUBKEY must match the Escrow wallet.
Unequal stakes rejectedPlayer and Challenger --amount must match.
Escrow underfunded for payoutAirdrop the escrow pubkey; it pays the pot plus FEE_BUFFER_SOL.
spot price feed unavailableBinance may geo-block; CoinGecko is the fallback. Retry from another network if both fail.
No You Won / You LostEscrow must be up first; keep Player and Challenger running to receive EscrowResponse.
Almanac / Agentverse errorsThis demo does not use mailbox. You do not need AGENTVERSE_API_KEY for local endpoint= agents.
solana balance less than 1 SOLFaucet rate limits; retry airdrop. Insufficient lamports fail transfers.

What you built

  • Local uAgents (uagents==0.25.5) with Solana Devnet transfers.
  • Env-configured Escrow address (no hardcoded third-party agent1… or Solana pubkey).
  • Deposit-verified, equal-stake spot BTC guess; full pot to the winner.

Extend the same pattern for other Devnet flows. Stay on Devnet until you have a real threat model, audits, and secret handling.