1"use strict";(self.webpackChunkinnovation_labs=self.webpackChunkinnovation_labs||[]).push([[68],{12507:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>p,frontMatter:()=>a,metadata:()=>t,toc:()=>c});const t=JSON.parse('{"id":"examples/on-chain-examples/solana-agents","title":"Solana Agent Integration with Fetch.ai uAgents","description":"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.","source":"@site/versioned_docs/version-1.0.5/examples/on-chain-examples/solana.mdx","sourceDirName":"examples/on-chain-examples","slug":"/examples/on-chain-examples/solana-agents","permalink":"/resources/docs/examples/on-chain-examples/solana-agents","draft":false,"unlisted":false,"tags":[],"version":"1.0.5","frontMatter":{"id":"solana-agents","title":"Solana Agent Integration with Fetch.ai uAgents"},"sidebar":"tutorialSidebar","previous":{"title":"Mettalex case study","permalink":"/resources/docs/examples/on-chain-examples/mettalex-agents"},"next":{"title":"BNB Chain Testnet (Chapel) Agents","permalink":"/resources/docs/examples/on-chain-examples/bnb-chain-agents"}}');var r=s(74848),o=s(28453);const a={id:"solana-agents",title:"Solana Agent Integration with Fetch.ai uAgents"},i=void 0,l={},c=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"High-level architecture",id:"high-level-architecture",level:2},{value:"Fetch wallet vs Solana wallet",id:"fetch-wallet-vs-solana-wallet",level:2},{value:"Dependencies",id:"dependencies",level:2},{value:"Keys and <code>.env</code> (placeholders only)",id:"keys-and-env-placeholders-only",level:2},{value:"Shared models",id:"shared-models",level:2},{value:"Utilities",id:"utilities",level:2},{value:"Escrow agent",id:"escrow-agent",level:2},{value:"Player agent",id:"player-agent",level:2},{value:"Challenger agent",id:"challenger-agent",level:2},{value:"Fund wallets on Devnet",id:"fund-wallets-on-devnet",level:2},{value:"Run order",id:"run-order",level:2},{value:"Sample output",id:"sample-output",level:3},{value:"Troubleshooting",id:"troubleshooting",level:2},{value:"What you built",id:"what-you-built",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(n.p,{children:["This ",(0,r.jsx)(n.strong,{children:"Solana Devnet"})," demo wires three local ",(0,r.jsx)(n.a,{href:"https://github.com/fetchai/uAgents",children:"uAgents"})," to Solana wallets: ",(0,r.jsx)(n.strong,{children:"EscrowAgent"}),", ",(0,r.jsx)(n.strong,{children:"PlayerAgent"}),", and ",(0,r.jsx)(n.strong,{children:"ChallengerAgent"}),". Players transfer SOL to escrow ",(0,r.jsx)(n.strong,{children:"first"}),", then send a typed message that includes the deposit transaction signature. Escrow verifies both deposits on Devnet, compares each ",(0,r.jsx)(n.strong,{children:"spot"})," BTC/USDT guess to Binance, and pays the ",(0,r.jsx)(n.strong,{children:"full matched pot"})," to the closer guess."]}),"\n",(0,r.jsxs)(n.p,{children:["This is a ",(0,r.jsx)(n.strong,{children:"local demo with a configured Escrow address"}),". Agents use ",(0,r.jsx)(n.code,{children:"endpoint="})," on loopback. They do ",(0,r.jsx)(n.strong,{children:"not"})," discover each other through Almanac. Copy the Escrow uAgent address and Solana pubkey from Escrow startup logs into ",(0,r.jsx)(n.code,{children:".env"})," before starting the players."]}),"\n",(0,r.jsxs)(n.p,{children:["See also ",(0,r.jsx)(n.a,{href:"/docs/examples/on-chain-examples/agent-on-chain",children:"AI Agents Reshaping the On-Chain Ecosystem"})," and ",(0,r.jsx)(n.a,{href:"/docs/examples/on-chain-examples/bnb-chain-agents",children:"BNB Chain Agents"}),". Solana secret lists are ",(0,r.jsx)(n.strong,{children:"wallet keypairs"})," (64-byte JSON integer arrays). The uAgent ",(0,r.jsx)(n.code,{children:"seed="})," string is a ",(0,r.jsx)(n.strong,{children:"separate Fetch.ai identity"}),"; it does not derive your Solana pubkey."]}),"\n",(0,r.jsx)(n.admonition,{title:"Devnet only \u2014 not a real betting product",type:"danger",children:(0,r.jsxs)(n.p,{children:["This tutorial is a ",(0,r.jsx)(n.strong,{children:"Solana Devnet"})," programming sample. Do ",(0,r.jsx)(n.strong,{children:"not"})," use mainnet, real funds, or production keys. It is ",(0,r.jsx)(n.strong,{children:"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 ",(0,r.jsx)(n.strong,{children:"burned"})," \u2014 generate new keypairs."]})}),"\n",(0,r.jsx)(n.admonition,{title:"Community sample",type:"warning",children:(0,r.jsxs)(n.p,{children:["The original walkthrough lived in a personal repo, ",(0,r.jsx)(n.a,{href:"https://github.com/abhifetch/solana-fetch-uagents-integration",children:"abhifetch/solana-fetch-uagents-integration"})," (last updated 2024). Treat that tree as ",(0,r.jsx)(n.strong,{children:"community / stale"}),": it pins ",(0,r.jsx)(n.code,{children:"uagents ^0.1.0"})," and Python 3.8, and its ",(0,r.jsx)(n.code,{children:"example.env"})," / README published full secret arrays. ",(0,r.jsx)(n.strong,{children:"Do not copy those keys."})," Use the code on ",(0,r.jsx)(n.strong,{children:"this page"})," with ",(0,r.jsx)(n.code,{children:"uagents==0.25.5"}),"."]})}),"\n",(0,r.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,r.jsx)(n.p,{children:"Ordered setup:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Clone or create a project folder and ",(0,r.jsx)(n.code,{children:"cd"})," into it."]}),"\n",(0,r.jsxs)(n.li,{children:["Install ",(0,r.jsx)(n.strong,{children:"Python 3.10+"})," (current uAgents adapters expect \u22653.10; do not use 3.8)."]}),"\n",(0,r.jsxs)(n.li,{children:["Install the ",(0,r.jsx)(n.a,{href:"https://docs.solana.com/cli/install-solana-cli-tools",children:"Solana CLI"})," and point it at Devnet."]}),"\n",(0,r.jsx)(n.li,{children:"Create a venv, then install the pins below (Poetry is optional)."}),"\n",(0,r.jsxs)(n.li,{children:["Generate ",(0,r.jsx)(n.strong,{children:"three new"})," Solana keypairs (",(0,r.jsx)(n.code,{children:"solana-keygen new"}),"). Convert each JSON file to an env var ",(0,r.jsx)(n.strong,{children:"without pasting real keys into chat, docs, or git"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:["Airdrop Devnet SOL to player, challenger, ",(0,r.jsx)(n.strong,{children:"and escrow"})," (escrow must cover payout ",(0,r.jsx)(n.strong,{children:"plus"})," network fees)."]}),"\n",(0,r.jsxs)(n.li,{children:["Run ",(0,r.jsx)(n.strong,{children:"Escrow first"}),", copy its logged addresses into ",(0,r.jsx)(n.code,{children:".env"}),", then start Player and Challenger. ",(0,r.jsx)(n.strong,{children:"Leave both clients running"})," until they receive ",(0,r.jsx)(n.code,{children:"EscrowResponse"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Default local ports: Escrow ",(0,r.jsx)(n.code,{children:":8000"}),", Player ",(0,r.jsx)(n.code,{children:":8001"}),", Challenger ",(0,r.jsx)(n.code,{children:":8002"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"high-level-architecture",children:"High-level architecture"}),"\n",(0,r.jsx)("div",{style:{textAlign:"center"},children:(0,r.jsx)("img",{src:"/resources/img/solana-integration.png",alt:"Solana escrow agents: player and challenger deposit SOL, escrow settles a spot BTC guess",style:{width:"90%",maxWidth:"1000px"}})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Player and Challenger each send ",(0,r.jsx)(n.strong,{children:"equal"})," SOL amounts to the Escrow ",(0,r.jsx)(n.strong,{children:"Solana"})," wallet and wait for confirmation."]}),"\n",(0,r.jsxs)(n.li,{children:["They then message Escrow with amount, ",(0,r.jsx)(n.strong,{children:"spot BTC/USDT guess"}),", their Solana pubkey, and the ",(0,r.jsx)(n.strong,{children:"deposit tx signature"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:["Escrow verifies both deposits on Devnet, requires equal stakes, fetches Binance ",(0,r.jsx)(n.strong,{children:"spot"})," ",(0,r.jsx)(n.code,{children:"BTCUSDT"}),", and transfers the ",(0,r.jsx)(n.strong,{children:"entire pot"})," to the closer guess. Solana fees are paid by the escrow wallet, so fund extra SOL there."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["This is ",(0,r.jsx)(n.strong,{children:"not"})," a futures bet. Escrow compares guesses to the ",(0,r.jsx)(n.strong,{children:"spot"})," ticker at match time."]}),"\n",(0,r.jsx)(n.h2,{id:"fetch-wallet-vs-solana-wallet",children:"Fetch wallet vs Solana wallet"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Wallet"}),(0,r.jsx)(n.th,{children:"What it is"}),(0,r.jsx)(n.th,{children:"How you fund it"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Solana Devnet keypair"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"ESCROW_SECRET_LIST"})," / ",(0,r.jsx)(n.code,{children:"PLAYER_SECRET_LIST"})," / ",(0,r.jsx)(n.code,{children:"CHALLENGER_SECRET_LIST"})]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"solana airdrop \u2026 --url devnet"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"uAgent identity"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"seed="})," string \u2192 ",(0,r.jsx)(n.code,{children:"agent.address"})," (",(0,r.jsx)(n.code,{children:"agent1\u2026"}),")"]}),(0,r.jsxs)(n.td,{children:["Separate from SOL. ",(0,r.jsx)(n.code,{children:"fund_agent_if_low"}
1)," (if you import it) tops up the ",(0,r.jsx)(n.strong,{children:"Fetch/Almanac"})," wallet, ",(0,r.jsx)(n.strong,{children:"not"})," Devnet SOL. This demo uses local endpoints, so skip it unless you add mailbox/testnet registration."]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["Do ",(0,r.jsx)(n.strong,{children:"not"})," set ",(0,r.jsx)(n.code,{children:"AGENTVERSE_API_KEY"})," unless you switch these agents to Agentverse mailbox. The scripts below do not read that variable."]}),"\n",(0,r.jsx)(n.h2,{id:"dependencies",children:"Dependencies"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python -m venv venv\nsource venv/bin/activate\n"})}),"\n",(0,r.jsx)(n.p,{children:"Windows:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python -m venv venv\nvenv\\Scripts\\activate\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",metastring:'title="requirements.txt"',children:"uagents==0.25.5\npython-dotenv==1.0.1\nrequests==2.32.3\nsolana==0.36.6\nsolders==0.26.0\nbase58==2.1.1\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"pip install -r requirements.txt\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Optional Poetry pins (replace any ",(0,r.jsx)(n.code,{children:'uagents = "^0.1.0"'})," / ",(0,r.jsx)(n.code,{children:'python = "^3.8"'})," from the old community repo):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-toml",metastring:'title="pyproject.toml"',children:'[tool.poetry.dependencies]\npython = "^3.10"\nuagents = "0.25.5"\npython-dotenv = "1.0.1"\nrequests = "2.32.3"\nsolana = "0.36.6"\nsolders = "0.26.0"\nbase58 = "2.1.1"\n'})}),"\n",(0,r.jsxs)(n.h2,{id:"keys-and-env-placeholders-only",children:["Keys and ",(0,r.jsx)(n.code,{children:".env"})," (placeholders only)"]}),"\n",(0,r.jsxs)(n.p,{children:["Format is a ",(0,r.jsx)(n.strong,{children:"JSON integer array of 64 bytes"}),", not Base64. ",(0,r.jsx)(n.code,{children:"solana-keygen new --outfile wallet.json"})," already writes that array."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"solana config set --url https://api.devnet.solana.com\nsolana-keygen new --outfile player-wallet.json --no-bip39-passphrase\nsolana-keygen new --outfile challenger-wallet.json --no-bip39-passphrase\nsolana-keygen new --outfile escrow-wallet.json --no-bip39-passphrase\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Put each file\u2019s single JSON line into ",(0,r.jsx)(n.code,{children:".env"})," yourself. ",(0,r.jsxs)(n.strong,{children:["Never commit ",(0,r.jsx)(n.code,{children:".env"})," or wallet JSON."]})," Rotate any key that was ever committed (including prefixes shown in older versions of this page)."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title=".env"',children:"# Placeholders \u2014 replace with YOUR 64-integer arrays. Do not reuse published keys.\nPLAYER_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]\nCHALLENGER_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]\nESCROW_SECRET_LIST
1=[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]\n\n# Filled from EscrowAgent startup logs \u2014 do not hardcode a third-party address.\nESCROW_AGENT_ADDRESS=agent1q...your_local_escrow_address\nESCROW_SOLANA_PUBKEY=YourEscrowSolanaPubkeyBase58\n\nPLAYER_BET_AMOUNT=0.1\nPLAYER_BTC_GUESS=65000\nCHALLENGER_BET_AMOUNT=0.1\nCHALLENGER_BTC_GUESS=64000\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"[0,0,\u2026]"})," is invalid as a real keypair; it only shows the shape. After Escrow prints ",(0,r.jsx)(n.code,{children:"Escrow agent address"})," and ",(0,r.jsx)(n.code,{children:"Escrow Solana pubkey"}),", copy those values into ",(0,r.jsx)(n.code,{children:"ESCROW_AGENT_ADDRESS"})," and ",(0,r.jsx)(n.code,{children:"ESCROW_SOLANA_PUBKEY"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"shared-models",children:"Shared models"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",metastring:'title="models.py"',children:"from uagents import Model\n\n\nclass EscrowRequest(Model):\n amount: float\n price: float\n public_key: str\n deposit_tx_sig: str\n\n\nclass EscrowResponse(Model):\n result: str\n"})}),"\n",(0,r.jsx)(n.h2,{id:"utilities",children:"Utilities"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"check_balance"})," takes a Solders ",(0,r.jsx)(n.code,{children:"Pubkey"})," directly. ",(0,r.jsx)(n.code,{children:"get_latest_btc_price()"})," returns ",(0,r.jsx)(n.code,{children:"None"})," on failure (including Binance geo-blocks); escrow ",(0,r.jsx)(n.strong,{children:"must not"})," settle in that case. CoinGecko is a simple fallback."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",metastring:'title="functions.py"',children:'import json\nimport os\n\nimport requests\nfrom dotenv import load_dotenv\nfrom solana.rpc.api import Client\nfrom solana.rpc.commitment import Confirmed\nfrom solana.rpc.types import TxOpts\nfrom solders.keypair import Keypair\nfrom solders.pubkey import Pubkey\nfrom solders.signature import Signature\nfrom solders.system_program import TransferParams, transfer\nfrom solders.transaction import Transaction\n\nload_dotenv()\n\nLAMPORTS_PER_SOL = 1_000_000_000\nDEVNET_RPC = os.getenv("SOLANA_RPC_URL", "https://api.devnet.solana.com")\nclient = Client(DEVNET_RPC)\n\n\ndef load_keypair(env_name: str) -> Keypair:\n raw = os.getenv(env_name)\n if not raw:\n raise SystemExit(f"Missing {env_name}. Add a 64-integer JSON array to .env.")\n secret = json.loads(raw)\n if not isinstance(secret, list) or len(secret) != 64:\n raise SystemExit(\n f"{env_name} must be a JSON array of 64 integers (Solana keypair bytes), not Base64."\n )\n return Keypair.from_bytes(bytes(secret))\n\n\ndef check_balance(pubkey: Pubkey) -> float:\n balance_resp = client.get_balance(pubkey, commitment=Confirmed)\n return balance_resp.value / LAMPORTS_PER_SOL\n\n\ndef transfer_sol(from_keypair: Keypair, to_pubkey_base58: str, amount_sol: float) -> str:\n lamports = int(amount_sol * LAMPORTS_PER_SOL)\n to_pubkey = Pubkey.from_string(to_pubkey_base58)\n recent_blockhash = client.get_latest_blockhash(commitment=Confirmed).value.blockhash\n instruction = transfer(\n TransferParams(\n from_pubkey=from_keypair.pubkey(),\n to_pubkey=to_pubkey,\n lamports=lamports,\n )\n )\n transaction = Transaction.new_signed_with_payer(\n [instruction],\n from_keypair.pubkey(),\n [from_keypair],\n recent_blockhash,\n )\n result = client.send_raw_transaction(\n bytes(transaction),\n opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed),\n )\n sig = str(result.value)\n client.confirm_transaction(Signature.from_string(sig), commitment=Confirmed)\n return sig\n\n\ndef verify_deposit(tx_sig: str, expected_to: str, expected_sol: float) -> bool:\n """Confirm the tx landed and increased the escrow account by the expected lamports."""\n sig = Signature.from_string(tx_sig)\n escrow_pk = str(Pubkey.from_string(expected_to))\n expected_lamports = int(expected_sol * LAMPORTS_PER_SOL)\n resp = client.get_transaction(\n sig,\n encoding="json",\n max_supported_transaction_version=0,\n commitment=Confirmed,\n )\n if resp.value is None or resp.value.transaction.meta is None:\n return False\n meta = resp.value.transaction.meta\n if meta.err is not None:\n return False\n keys = [str(k) for k in resp.value.transaction.transaction.message.account_keys]\n if escrow_pk not in keys:\n return False\n idx = keys.index(escrow_pk)\n delta = meta.post_balances[idx] - meta.pre_balances[idx]\n return delta == expected_lamports\n\n\ndef get_latest_btc_price() -> float | None:\n urls = (\n "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",\n "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",\n )\n try:\n response = requests.get(urls[0], timeout=10)\n response.raise_for_status()\n return float(response.json()["price"])\n except (requests.RequestException, KeyError, TypeError, ValueError):\n pass\n try:\n response = requests.get(urls[1], timeout=10)\n response.raise_for_status()\n return float(response.json()["bitcoin"]["usd"])\n except (requests.RequestException, KeyError, TypeError, ValueError) as exc:\n print(f"Error fetching BTC spot price: {exc}")\n return None\n'})}),"\n",(0,r.jsx)(n.h2,{id:"escrow-agent",children:"Escrow agent"}),"\n",(0,r.jsx)(n.p,{children:"Complete handler: equal-stake check, on-chain deposit verify, escrow balance check, spot price, full-pot payout, then reset."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",metastring:'title="escrow_agent.py"',children:'import os\n\nfrom dotenv import load_dotenv\nfrom uagents import Agent, Context\n\nfrom functions import check_balance, get_latest_btc_price, load_keypair, transfer_sol, verify_deposit\nfrom models import EscrowRequest, EscrowResponse\n\nload_dotenv()\n\nFEE_BUFFER_SOL = 0.01 # extra Devnet SOL so payout txs can pay network fees\n\nescrow_keypair = load_keypair("ESCROW_SECRET_LIST")\nescrow_pubkey_base58 = str(escrow_keypair.pubkey())\n\nagent = Agent(\n name="EscrowAgent",\n port=8000,\n seed="Escrow Wallet",\n endpoint=["http://127.0.0.1:8000/submit"],\n)\n\n\[email protected]_event("startup")\nasync def on_startup(ctx: Context):\n ctx.logger.info("Escrow agent initialized, ready for bids (local demo).")\n ctx.logger.info(f"Escrow agent address: {agent.address}")\n ctx.logger.info(f"Escrow Solana pubkey: {escrow_pubkey_base58}")\n ctx.logger.info(\n "Copy those two values into ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY, then start Player and Challenger."\n )\n sol_balance = check_balance(escrow_keypair.pubkey())\n ctx.logger.info(f"Escrow SOL balance: {sol_balance}")\n if sol_balance < FEE_BUFFER_SOL:\n ctx.logger.warning(\n "Escrow wallet is underfunded for fees. Airdrop Devnet SOL before matching."\n )\n ctx.storage.set("bids_count", 0)\n\n\ndef _store_bid(ctx: Context, prefix: str, sender: str, msg: EscrowRequest):\n ctx.storage.set(f"{prefix}_sender", sender)\n ctx.storage.set(f"{prefix}_amount", msg.amount)\n ctx.storage.set(f"{prefix}_price", msg.price)\n ctx.storage.set(f"{prefix}_pubkey", msg.public_key)\n ctx.storage.set(f"{prefix}_tx", msg.deposit_tx_sig)\n\n\ndef _reset(ctx: Context):\n for prefix in ("first", "second"):\n for field in ("sender", "amount", "price", "pubkey", "tx"):\n ctx.storage.set(f"{prefix}_{field}", None)\n ctx.storage.set("bids_count", 0)\n\n\[email protected]_message(model=EscrowRequest, replies={EscrowResponse})\nasync def escrow_request_handler(ctx: Context, sender: str, msg: EscrowRequest):\n current_count = ctx.storage.get("bids_count") or 0\n ctx.logger.info(f"Received EscrowRequest from {sender}")\n\n if not verify_deposit(msg.deposit_tx_sig, escrow_pubkey_base58, msg.amount):\n ctx.logger.error("Deposit verification failed; ignoring bid.")\n await ctx.send(sender, EscrowResponse(result="Rejected: deposit not confirmed on Devnet"))\n return\n\n if current_count == 0:\n _store_bid(ctx, "first", sender, msg)\n ctx.storage.set("bids_count", 1)\n ctx.logger.info("Stored first verified bid; waiting for a matching stake.")\n return\n\n if current_count != 1:\n await ctx.send(sender, EscrowResponse(result="Rejected: escrow is busy"))\n return\n\n _store_bid(ctx, "second", sender, msg)\n first_amount = float(ctx.storage.get("first_amount"))\n second_amount = float(msg.amount)\n if first_amount != second_amount:\n ctx.logger.error("Unequal stakes; refusing to match.")\n await ctx.send(\n ctx.storage.get("first_sender"),\n EscrowResponse(result="Rejected: stakes must be equal"),\n )\n await ctx.send(sender, EscrowResponse(result="Rejected: stakes must be equal"))\n _reset(ctx)\n return\n\n pot = first_amount + second_amount\n escrow_balance = check_balance(escrow_keypair.pubkey())\n if escrow_balance < pot + FEE_BUFFER_SOL:\n ctx.logger.error(\n f"Escrow SOL {escrow_balance} cannot pay pot {pot} plus fee buffer {FEE_BUFFER_SOL}."\n )\n await ctx.send(\n ctx.storage.get("first_sender"),\n EscrowResponse(result="Rejected: escrow underfunded for payout"),\n )\n await ctx.send(sender, EscrowResponse(result="Rejected: escrow underfunded for payout"))\n _reset(ctx)\n return\n\n spot = get_latest_btc_price()\n if spot is None:\n ctx.logger.error("BTC spot price unavailable (Binance/CoinGecko). Not settling.")\n await ctx.send(\n ctx.storage.get("first_sender"),\n EscrowResponse(result="Rejected: spot price feed unavailable"),\n )\n await ctx.send(sender, EscrowResponse(result="Rejected: spot price feed unavailable"))\n _reset(ctx)\n return\n\n first_price = float(ctx.storage.get("first_price"))\n second_price = float(msg.price)\n first_diff = abs(first_price - spot)\n second_diff = abs(second_price - spot)\n ctx.logger.info(f"Spot BTC/USDT: {spot}. Diffs: {first_diff}, {second_diff}")\n\n if first_diff <= second_diff:\n winner, loser = ctx.storage.get("first_sender"), sender\n winner_pk = ctx.storage.get("first_pubkey")\n else:\n winner, loser = sender, ctx.storage.get("first_sender")\n winner_pk = msg.public_key\n\n ctx.logger.info(f"Paying full pot {pot} SOL to {winner_pk} (no house cut).")\n transfer_sol(escrow_keypair, winner_pk, pot)\n await ctx.send(winner, EscrowResponse(result="You Won"))\n await ctx.send(loser, EscrowResponse(result="You Lost"))\n _reset(ctx)\n\n\nif __name__ == "__main__":\n agent.run()\n'})}),"\n",(0,r.jsx)(n.p,{children:"Run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python escrow_agent.py\n"})}),"\n",(0,r.jsx)(n.h2,{id:"player-agent",children:"Player agent"}),"\n",(0,r.jsxs)(n.p,{children:["Bets come from ",(0,r.jsx)(n.strong,{children:"CLI flags or env"}),", parsed ",(0,r.jsx)(n.strong,{children:"before"})," ",(0,r.jsx)(n.code,{children:"agent.run()"}),". There is no ",(0,r.jsx)(n.code,{children:"input()"})," inside ",(0,r.jsx)(n.code,{children:'@agent.on_event("startup")'})," (that would block the event loop). Transfer SOL and confirm ",(0,r.jsx)(n.strong,{children:"before"})," sending ",(0,r.jsx)(n.code,{children:"EscrowRequest"}),". Keep this process alive to receive ",(0,r.jsx)(n.code,{children:"EscrowResponse"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",metastring:'title="player_agent.py"',children:'import argparse\nimport os\n\nfrom dotenv import load_dotenv\nfrom uagents import Agent, Context\n\nfrom functions import check_balance, load_keypair, transfer_sol\nfrom models import EscrowRequest, EscrowResponse\n\nload_dotenv()\n\n\ndef parse_args():\n parser = argparse.ArgumentParser(description="Player: Devnet SOL spot-BTC guess")\n parser.add_argument(\n "--amount",\n type=float,\n default=float(os.getenv("PLAYER_BET_AMOUNT", "0")),\n )\n parser.add_argument(\n "--price",\n type=float,\n default=float(os.getenv("PLAYER_BTC_GUESS", "0")),\n )\n return parser.parse_args()\n\n\nargs = parse_args()\nif args.amount <= 0 or args.price <= 0:\n raise SystemExit("Set PLAYER_BET_AMOUNT and PLAYER_BTC_GUESS or pass --amount and --price.")\n\nescrow_agent_address = os.getenv("ESCROW_AGENT_ADDRESS")\nescrow_solana_pubkey = os.getenv("ESCROW_SOLANA_PUBKEY")\nif not escrow_agent_address or not escrow_solana_pubkey:\n raise SystemExit("Set ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY from Escrow startup logs.")\n\nagent_keypair = load_keypair("PLAYER_SECRET_LIST")\nagent_pubkey_base58 = str(agent_keypair.pubkey())\n\nagent = Agent(\n name="PlayerAgent",\n port=8001,\n seed="Player Escrow Wallet 1",\n endpoint=["http://127.0.0.1:8001/submit"],\n)\n\n\[email protected]_event("startup")\nasync def starter_function(ctx: Context):\n ctx.logger.info(f"Initial SOL: {check_balance(agent_keypair.pubkey())}")\n ctx.logger.info(f"Spot BTC/USDT guess: {args.price}; stake: {args.amount} SOL")\n sig = transfer_sol(agent_keypair, escrow_solana_pubkey, args.amount)\n ctx.logger.info(f"Deposit confirmed: {sig}")\n await ctx.send(\n escrow_agent_address,\n EscrowRequest(\n amount=args.amount,\n price=args.price,\n public_key=agent_pubkey_base58,\n deposit_tx_sig=sig,\n ),\n )\n ctx.logger.info("Waiting for EscrowResponse \u2014 keep this process running.")\n ctx.logger.info(f"SOL after deposit: {check_balance(agent_keypair.pubkey())}")\n\n\[email protected]_message(model=EscrowResponse)\nasync def on_escrow_response(ctx: Context, sender: str, msg: EscrowResponse):\n ctx.logger.info(\n f"{msg.result}. Updated SOL: {check_balance(agent_keypair.pubkey())}"\n )\n\n\nif __name__ == "__main__":\n agent.run()\n'})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python player_agent.py --amount 0.1 --price 65000\n"})}),"\n",(0,r.jsx)(n.h2,{id:"challenger-agent",children:"Challenger agent"}),"\n",(0,r.jsxs)(n.p,{children:["Same protocol on port ",(0,r.jsx)(n.code,{children:"8002"}),". Do ",(0,r.jsx)(n.strong,{children:"not"})," call ",(0,r.jsx)(n.code,{children:"fund_agent_if_low"})," here: it would fund the Fetch wallet, not SOL."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",metastring:'title="challenger_agent.py"',children:'import argparse\nimport os\n\nfrom dotenv import load_dotenv\nfrom uagents import Agent, Context\n\nfrom functions import check_balance, load_keypair, transfer_sol\nfrom models import EscrowRequest, EscrowResponse\n\nload_dotenv()\n\n\ndef parse_args():\n parser = argparse.ArgumentParser(description="Challenger: Devnet SOL spot-BTC guess")\n parser.add_argument(\n "--amount",\n type=float,\n default=float(os.getenv("CHALLENGER_BET_AMOUNT", "0")),\n )\n parser.add_argument(\n "--price",\n type=float,\n default=float(os.getenv("CHALLENGER_BTC_GUESS", "0")),\n )\n return parser.parse_args()\n\n\nargs = parse_args()\nif args.amount <= 0 or args.price <= 0:\n raise SystemExit(\n "Set CHALLENGER_BET_AMOUNT and CHALLENGER_BTC_GUESS or pass --amount and --price."\n )\n\nescrow_agent_address = os.getenv("ESCROW_AGENT_ADDRESS")\nescrow_solana_pubkey = os.getenv("ESCROW_SOLANA_PUBKEY")\nif not escrow_agent_address or not escrow_solana_pubkey:\n raise SystemExit("Set ESCROW_AGENT_ADDRESS and ESCROW_SOLANA_PUBKEY from Escrow startup logs.")\n\nagent_keypair = load_keypair("CHALLENGER_SECRET_LIST")\nagent_pubkey_base58 = str(agent_keypair.pubkey())\n\nagent = A
1gent(\n name="Challenger",\n port=8002,\n seed="Challenger Escrow Wallet 2",\n endpoint=["http://127.0.0.1:8002/submit"],\n)\n\n\[email protected]_event("startup")\nasync def starter_function(ctx: Context):\n ctx.logger.info(f"Initial SOL: {check_balance(agent_keypair.pubkey())}")\n sig = transfer_sol(agent_keypair, escrow_solana_pubkey, args.amount)\n ctx.logger.info(f"Deposit confirmed: {sig}")\n await ctx.send(\n escrow_agent_address,\n EscrowRequest(\n amount=args.amount,\n price=args.price,\n public_key=agent_pubkey_base58,\n deposit_tx_sig=sig,\n ),\n )\n ctx.logger.info("Waiting for EscrowResponse \u2014 keep this process running.")\n\n\[email protected]_message(model=EscrowResponse)\nasync def on_escrow_response(ctx: Context, sender: str, msg: EscrowResponse):\n ctx.logger.info(\n f"{msg.result}. Updated SOL: {check_balance(agent_keypair.pubkey())}"\n )\n\n\nif __name__ == "__main__":\n agent.run()\n'})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python challenger_agent.py --amount 0.1 --price 64000\n"})}),"\n",(0,r.jsx)(n.h2,{id:"fund-wallets-on-devnet",children:"Fund wallets on Devnet"}),"\n",(0,r.jsxs)(n.p,{children:["Replace the pubkeys with ",(0,r.jsx)(n.code,{children:"solana-keygen pubkey <wallet.json>"})," output. Airdrop ",(0,r.jsx)(n.strong,{children:"escrow"})," as well: after both deposits, it must still pay the pot plus fees."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"solana airdrop 2 <PLAYER_PUBKEY> --url devnet\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"solana airdrop 2 <CHALLENGER_PUBKEY> --url devnet\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"solana airdrop 2 <ESCROW_PUBKEY> --url devnet\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"solana balance <ESCROW_PUBKEY> --url devnet\n"})}),"\n",(0,r.jsx)(n.h2,{id:"run-order",children:"Run order"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Start Escrow and copy ",(0,r.jsx)(n.code,{children:"Escrow agent address"})," / ",(0,r.jsx)(n.code,{children:"Escrow Solana pubkey"})," into ",(0,r.jsx)(n.code,{children:".env"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:["Restart is not required for Escrow; update ",(0,r.jsx)(n.code,{children:".env"})," then start the clients."]}),"\n",(0,r.jsx)(n.li,{children:"Start Player; leave it running."}),"\n",(0,r.jsxs)(n.li,{children:["Start Challenger; leave it running until both log ",(0,r.jsx)(n.code,{children:"You Won"})," or ",(0,r.jsx)(n.code,{children:"You Lost"}),"."]}),"\n"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python escrow_agent.py\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python player_agent.py --amount 0.1 --price 65000\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"python challenger_agent.py --amount 0.1 --price 64000\n"})}),"\n",(0,r.jsx)(n.h3,{id:"sample-output",children:"Sample output"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"EscrowAgent:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"INFO: [EscrowAgent]: Escrow agent initialized, ready for bids.\nINFO: [EscrowAgent]: Received escrowRequest message\nINFO: [EscrowAgent]: Storing first request ...\nINFO: [EscrowAgent]: Received escrowRequest message\nINFO: [EscrowAgent]: Storing second request ...\nINFO: [EscrowAgent]: Processing bids to determine the winner.\nINFO: [EscrowAgent]: First difference: 1820.0, Second difference: 586820.0\nINFO: [EscrowAgent]: Transferring 0.9 SOL to winner ...\nINFO: [EscrowAgent]: Notifying winner and loser.\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"What is the amount of SOL you want to deposit? 0.5\nWhat is the price of Bitcoin you want to bid at? 65000\nINFO: [PlayerAgent]: Transfer result: ...\nINFO: [PlayerAgent]: Final agent balance: 4.3
11998 SOL\nINFO: [PlayerAgent]: You Won. Updated account balance: 5.21998 SOL\n"})}),"\n",(0,r.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Symptom"}),(0,r.jsx)(n.th,{children:"What to check"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"Missing PLAYER_SECRET_LIST"})," / cannot decode key"]}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:".env"})," value is a ",(0,r.jsx)(n.strong,{children:"JSON array of 64 integers"}),", no trailing comma. Call ",(0,r.jsx)(n.code,{children:"load_dotenv()"})," before ",(0,r.jsx)(n.code,{children:"os.getenv"}),". Not Base64."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Deposit verification failed"})}),(0,r.jsxs)(n.td,{children:["Transfer ",(0,r.jsx)(n.strong,{children:"before"})," the uAgents message; wait for confirmation; ",(0,r.jsx)(n.code,{children:"ESCROW_SOLANA_PUBKEY"})," must match the Escrow wallet."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Unequal stakes rejected"}),(0,r.jsxs)(n.td,{children:["Player and Challenger ",(0,r.jsx)(n.code,{children:"--amount"})," must match."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Escrow underfunded for payout"}),(0,r.jsxs)(n.td,{children:["Airdrop the ",(0,r.jsx)(n.strong,{children:"escrow"})," pubkey; it pays the pot ",(0,r.jsx)(n.strong,{children:"plus"})," ",(0,r.jsx)(n.code,{children:"FEE_BUFFER_SOL"}),"."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"spot price feed unavailable"})}),(0,r.jsx)(n.td,{children:"Binance may geo-block; CoinGecko is the fallback. Retry from another network if both fail."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["No ",(0,r.jsx)(n.code,{children:"You Won"})," / ",(0,r.jsx)(n.code,{children:"You Lost"})]}),(0,r.jsxs)(n.td,{children:["Escrow must be up first; ",(0,r.jsx)(n.strong,{children:"keep Player and Challenger running"})," to receive ",(0,r.jsx)(n.code,{children:"EscrowResponse"}),"."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Almanac / Agentverse errors"}),(0,r.jsxs)(n.td,{children:["This demo does not use mailbox. You do ",(0,r.jsx)(n.strong,{children:"not"})," need ",(0,r.jsx)(n.code,{children:"AGENTVERSE_API_KEY"})," for local ",(0,r.jsx)(n.code,{children:"endpoint="})," agents."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"solana balance"})," less than 1 SOL"]}),(0,r.jsx)(n.td,{children:"Faucet rate limits; retry airdrop. Insufficient lamports fail transfers."})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"what-you-built",children:"What you built"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Local uAgents (",(0,r.jsx)(n.code,{children:"uagents==0.25.5"}),") with Solana ",(0,r.jsx)(n.strong,{children:"Devnet"})," transfers."]}),"\n",(0,r.jsxs)(n.li,{children:["Env-configured Escrow address (no hardcoded third-party ",(0,r.jsx)(n.code,{children:"agent1\u2026"})," or Solana pubkey)."]}),"\n",(0,r.jsx)(n.li,{children:"Deposit-verified, equal-stake spot BTC guess; full pot to the winner."}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Extend the same pattern for other Devnet flows. Stay on Devnet until you have a real threat model, audits, and secret handling."})]})}function p(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>a,x:()=>i});var t=s(96540);const r={},o=t.createContext(r);function a(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.