Skip to main content

Integrating External AI Agents

Overview​

Any autonomous process that can invoke HTTP endpoints or run Go code can be integrated as an agent on ULedger. The Go SDK (github.com/ULedgerInc/golang-sdk) is the primary integration interface. The node also exposes a raw HTTP REST API for environments where the Go SDK is not the right fit.

This guide covers:

  1. The integration model: what the agent does vs. what the protocol does.
  2. Wiring the Go SDK to an agent process.
  3. Integration patterns for popular AI agent frameworks.
  4. Signing and submitting transactions from agent code.
  5. Reading on-chain state.
  6. Monitoring and receipt confirmation.

The Integration Model​

On ULedger, the agent's role is narrow and well-defined:

External AI System (LLM, agent framework, custom code)
│
│ "I want to invoke council-voting:submitWork with vote=1"
│
▼
ULedger Go SDK (or raw HTTP)
│
│ 1. Load agent wallet private key (.ukey file)
│ 2. Build transaction payload
│ 3. Sign with agent wallet private key
│ 4. POST to any node endpoint
│ 5. Poll for receipt
│ 6. Return transaction ID and block height
│
▼
ULedger Chain
│
│ Validate capabilities, execute, commit, Helix-proof
│
▼
On-chain record: "agent-wallet-X submitted vote=1 at block 12345"

The AI system decides what to do. The SDK handles how to author and submit the signed transaction. The protocol handles whether the transaction is authorized and recording it permanently.


Setup​

For the complete agent procurement walkthrough (generate, register, create entity, link wallet, attach capabilities) see Procuring an Agent Wallet.

Key integration checkpoints before writing any agent code:

  1. The agent's .ukey private key file must be accessible to the agent process at runtime. Use a secrets manager or secure volume mount, not environment variables or source control.
  2. Capabilities must be attached by the parent wallet before the agent can act. The agent cannot grant itself permissions. See Capabilities for the full field reference.
  3. The agent needs: .ukey file path, node endpoint URLs, blockchain ID, and the contract address(es) it is authorized to invoke.

For the full lifecycle including administration and termination see The Agent Wallet Lifecycle.


Go SDK Integration​

Import​

import (
"github.com/ULedgerInc/golang-sdk/pkg/wallet"
"github.com/ULedgerInc/golang-sdk/pkg/transaction"
"github.com/ULedgerInc/golang-sdk/pkg/txconfirm"
)

Load Agent Wallet​

agentWallet, err := wallet.LoadFromFile("./wallets/agent.ukey", passphrase)
if err != nil {
return fmt.Errorf("load agent wallet: %w", err)
}

Submit a Contract Invocation​

// Build the invoke transaction
tx := transaction.InvokeContract{
ContractAddress: contractAddress,
FunctionName: "submitWork",
Args: []string{proposalID, "1", "AI analysis complete: recommend approval"},
GasLimit: 100_000,
}

// Sign and submit
receipt, err := transaction.SignAndSubmit(ctx, agentWallet, nodeEndpoints, blockchainID, tx)
if err != nil {
return fmt.Errorf("submit: %w", err)
}

// Poll for confirmation (from a different node for cross-node consistency)
confirmed, err := txconfirm.Poll(ctx, receipt.TxID, alternateNodeEndpoint, txconfirm.PollConfig{
IntervalSeconds: 1,
TimeoutSeconds: 80,
})

Read Contract State​

state, err := sdk.ReadContractState(ctx, nodeEndpoint, blockchainID, contractAddress, key)

Submit a Data Transaction​

dataTx := transaction.DataTx{
Payload: []byte(`{"event":"research_complete","result":"positive","confidence":0.92}`),
}
receipt, err := transaction.SignAndSubmit(ctx, agentWallet, nodeEndpoints, blockchainID, dataTx)

HTTP API Integration (Non-Go Agents)​

For agents written in Python, TypeScript, or other languages, the raw HTTP API is available.

Submit a Transaction​

POST http://localhost:8080/chains/{chainId}/transactions
Content-Type: application/json

{
"type": "INVOKE_SMART_CONTRACT",
"sender": "<agent-wallet-address>",
"signature": "<hex-encoded-signature>",
"payload": {
"contractAddress": "<contract-address>",
"function": "submitWork",
"args": ["<proposalId>", "1", "AI analysis: positive"],
"gasLimit": 100000
}
}

The signature is sign(SHA256(transaction-payload-bytes), agentPrivateKey) using the wallet's key algorithm.

Python Example (secp256k1)​

import hashlib
import requests
from coincurve import PrivateKey

def submit_vote(node_url, chain_id, contract_addr, private_key_hex, proposal_id, vote):
payload = {
"contractAddress": contract_addr,
"function": "submitWork",
"args": [proposal_id, str(vote), "AI vote"],
"gasLimit": 100000,
}
payload_bytes = json.dumps(payload, separators=(',', ':')).encode()
msg_hash = hashlib.sha256(payload_bytes).digest()

privkey = PrivateKey(bytes.fromhex(private_key_hex))
sig = privkey.sign(msg_hash, hasher=None)

tx = {
"type": "INVOKE_SMART_CONTRACT",
"sender": derive_address(privkey.public_key.format(compressed=True).hex()),
"signature": sig.hex(),
"payload": payload,
}

resp = requests.post(f"{node_url}/chains/{chain_id}/transactions", json=tx)
resp.raise_for_status()
return resp.json()["txId"]

For ML-DSA-87 (post-quantum), the signing uses Cloudflare CIRCL's mldsa87 implementation; use the Go SDK for this, as few Python libraries implement ML-DSA-87 yet.


Framework Integration Patterns​

LangChain (Python)​

Wrap SDK operations as LangChain tools:

from langchain.tools import tool

@tool
def submit_on_chain_vote(proposal_id: str, vote: int, reasoning: str) -> str:
"""Submit a vote on a governance proposal on ULedger. Vote: 1=yes, 2=no, 3=abstain."""
tx_id = uledger_client.invoke_contract(
contract=COUNCIL_CONTRACT,
function="submitWork",
args=[proposal_id, str(vote), reasoning],
)
return f"Vote submitted. Transaction ID: {tx_id}"

@tool
def read_proposal_status(proposal_id: str) -> str:
"""Read the current status of a governance proposal."""
status = uledger_client.read_state(COUNCIL_CONTRACT, f"proposal:{proposal_id}:status")
return status

The LLM chooses when to call these tools. The ULedger client handles signing. Every call is on-chain.

CrewAI (Python)​

Assign ULedger tools to agents:

from crewai import Agent, Task

voting_agent = Agent(
role="Governance Analyst",
goal="Review proposals and submit votes based on organizational policy",
tools=[submit_on_chain_vote, read_proposal_status, read_proposal_details],
wallet_key_path="./wallets/crew-analyst.ukey",
)

vote_task = Task(
description="Review proposal {proposal_id} and submit a vote with reasoning.",
agent=voting_agent,
)

Custom Orchestration Loop​

For fully autonomous systems without a framework:

import time

def agent_loop(node_url, chain_id, contract_addr, wallet):
while True:
# Check for pending proposals
proposals = fetch_active_proposals(node_url, chain_id, contract_addr)

for proposal in proposals:
if not already_voted(proposal["id"], wallet.address):
# AI decision logic
analysis = run_analysis(proposal["description"])
vote = 1 if analysis["recommendation"] == "approve" else 2

# Submit on-chain
tx_id = wallet.invoke_contract(
contract_addr, "submitWork",
[proposal["id"], str(vote), analysis["reasoning"]]
)
log(f"Voted {vote} on proposal {proposal['id']}: tx={tx_id}")

time.sleep(10) # Poll interval

Monitoring Agent Wallet Activity​

Check Wallet Status​

GET /chains/{chainId}/wallets/{agentWalletAddress}

Returns the wallet's current state: enabled/disabled, capabilities, entity affiliation.

Check Capability Status​

GET /chains/{chainId}/wallets/{agentWalletAddress}/capabilities/status

Returns per-capability health: active, expiring_soon, exhausted, expired.

Subscribe to Contract Events (WebSocket)​

WS /chains/{chainId}/events?contract={contractAddress}

Receive real-time events emitted by the contract, useful for triggering agent responses when other agents or humans take actions.

Query Transaction History​

GET /chains/{chainId}/transactions?sender={agentWalletAddress}&limit=100

Full on-chain history of all transactions submitted by the agent wallet.


Capability Renewal Workflow​

Set up a capability renewal process to prevent agents from going offline when capabilities expire:

def check_and_renew_capabilities(parent_wallet, agent_wallet_addr, node_url, chain_id):
status = fetch_capability_status(node_url, chain_id, agent_wallet_addr)

for cap in status["capabilities"]:
if cap["status"] in ("expiring_soon", "exhausted"):
# Parent wallet renews capability
parent_wallet.alter_wallet(agent_wallet_addr, new_capabilities=[
{
"action": cap["action"],
"resource": cap["resource"],
"limits": {"maxInvocations": 1000},
"notAfter": current_block + 50000,
}
])
log(f"Renewed capability {cap['action']} for agent {agent_wallet_addr}")

Run this as a scheduled job. The parent wallet's key does not need to be held by the agent; keep it in a separate, more secure location.