nabuguides
BUILD · LANGGRAPH · HUMAN REVIEW · SPECULOS

Let's build your first real AI agent from scratch

This time, we're not just talking about agents—we're building one. It makes a proposal, the rules check it, and you approve or reject it. Nothing moves until you say so.

The short versionAn agent is a smart helper that can make suggestions and move a task through several steps. But we don't give it the keys, the budget, or the final permission. This project makes that separation real.
English edition · checked against official docs · September 1, 2026

New here? Catch up in two minutes

You don't need to read everything first. Each card gives you a one-line refresher. Open the pieces that are new to you, and skip the ones you already know.

What's new in this guide?

The earlier guides introduced the LEGO pieces. Here, we put them together and build a small system you can actually run.

What will you have at the end?

01A real agent

It reads a request and proposes a structured action.

02A rule it can't bypass

Amounts or destinations outside policy are blocked before human review.

03Pause and resume

LangGraph keeps state while you approve or reject.

04A separate signer

The agent never sees a seed phrase or private key.

05A learning emulator

Speculos lets you inspect a Ledger App interface without a device.

06A failure-path test

You prove that a dangerous request really gets stopped.

See the whole project before you code

User request flows to an agent proposal, policy check and human review before crossing the trust boundary to hardware enforcement and an execute or reject result
This is the trust boundary: the agent may build the action data, but policy and hardware enforcement stay outside the agent runtime.
One important correction

Human approval must not bypass a hard policy. If the limit is 0.01, a person can't wave a 1-unit request through this flow. The policy itself has to be changed through a separate, controlled process.

Install these before you start

Python 3.11 or newer

The project runs in Python. After installing it, run python3 --version in Terminal.

Ollama for a free model on your computer

The main path in this guide is free and local. If your machine is underpowered, you can swap the model adapter for a cloud API later.

A simple code editor

VS Code is convenient, but any editor that saves plain-text files will work.

Docker—only for the Speculos section

You don't need Docker to build the agent. We'll use it only when we reach the emulator.

Using an older or smaller computer?

Start with the fake proposal node or use a cloud API. The policy, human-review, and signer-boundary lessons do not need a huge model—and those are the most important parts of this build.

Step 1: create the project

TERMINAL
mkdir first-guarded-agent
cd first-guarded-agent
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install langgraph langchain langchain-ollama pydantic python-dotenv

Now get a small model ready in Ollama. You can change the model to match your hardware:

OLLAMA
ollama run qwen3:4b
If this is too heavy for your computer, use a smaller model or follow the offline-model guide above.

Step 2: the model only creates a proposal

Our first architecture rule: the model never sees a “send” or “sign” function. Its output is only a structured proposal.

agent.py · proposal
import os
from typing import TypedDict
from pydantic import BaseModel, Field
from langchain_ollama import ChatOllama
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command

class TransferProposal(BaseModel):
    asset: str = Field(description="demo asset symbol")
    amount: float = Field(gt=0)
    destination: str
    reason: str

class AgentState(TypedDict, total=False):
    request: str
    proposal: dict
    policy: dict
    approved: bool
    result: str

model = ChatOllama(
    model=os.getenv("OLLAMA_MODEL", "qwen3:4b"),
    temperature=0,
)
proposal_model = model.with_structured_output(TransferProposal)

def propose(state: AgentState):
    proposal = proposal_model.invoke(
        "Create a DEMO transfer proposal only. "
        "Never claim that anything was executed. Request: " + state["request"]
    )
    return {"proposal": proposal.model_dump()}
What does “structured output” mean?

Instead of accepting a messy paragraph, we make the model fill four specific fields: asset, amount, destination, and reason.

Step 3: policy gets the first decision

In this demo, only two fake destinations are allowed, and the maximum amount is 0.01. If a request breaks either rule, the flow stops right there.

agent.py · policy
MAX_AMOUNT = 0.01
ALLOWLIST = {"demo-alice", "demo-bob"}

def policy_check(state: AgentState):
    p = state["proposal"]
    reasons = []
    if p["amount"] > MAX_AMOUNT:
        reasons.append("amount is above the hard limit")
    if p["destination"] not in ALLOWLIST:
        reasons.append("destination is not allowlisted")

    return {
        "policy": {
            "allowed": not reasons,
            "reasons": reasons,
        }
    }

def after_policy(state: AgentState):
    return "review" if state["policy"]["allowed"] else "blocked"

def blocked(state: AgentState):
    return {"result": "REJECTED BY POLICY: " + ", ".join(state["policy"]["reasons"])}

Step 4: LangGraph pauses the run

interrupt() sends the proposal out for review and preserves state. Reusing the same thread_id brings the continuation back to that exact run.

agent.py · review + signer
def human_review(state: AgentState):
    decision = interrupt({
        "question": "Do you approve this demo proposal?",
        "proposal": state["proposal"],
        "allowed_decisions": ["approve", "reject"],
    })
    return {"approved": decision == "approve"}

def signer_boundary(state: AgentState):
    if not state["policy"]["allowed"]:
        return {"result": "REJECTED: policy cannot be bypassed"}
    if not state.get("approved"):
        return {"result": "REJECTED BY HUMAN"}

    # No seed, private key or transaction broadcast exists in this tutorial.
    # This adapter is where demo / Speculos / real hardware would be selected.
    mode = os.getenv("SIGNER_MODE", "demo")
    return {"result": f"APPROVED; handed to {mode} signer boundary (no broadcast)"}

Step 5: connect the graph

agent.py · graph
builder = StateGraph(AgentState)
builder.add_node("propose", propose)
builder.add_node("policy", policy_check)
builder.add_node("blocked", blocked)
builder.add_node("review", human_review)
builder.add_node("signer", signer_boundary)

builder.add_edge(START, "propose")
builder.add_edge("propose", "policy")
builder.add_conditional_edges(
    "policy",
    after_policy,
    {"review": "review", "blocked": "blocked"},
)
builder.add_edge("blocked", END)
builder.add_edge("review", "signer")
builder.add_edge("signer", END)

graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "demo-1"}}

first = graph.invoke({
    "request": "Send 0.005 DEMO to demo-alice for lunch"
}, config=config)

if "__interrupt__" in first:
    print(first["__interrupt__"])
    answer = input("Type approve or reject: ").strip().lower()
    final = graph.invoke(Command(resume=answer), config=config)
    print(final["result"])
else:
    print(first["result"])

Save the file, then run it:

RUN
SIGNER_MODE=demo python agent.py

Step 6: where does Speculos fit?

Speculos is Ledger's official project for running Ledger Apps on a computer. It lets you inspect and test the interface and behavior of a compatible app without owning the device.

But it is not real hardware

Speculos does not reproduce every device property, including the Secure Element and full operating-system isolation. It is useful for learning and testing—not for holding assets or claiming real hardware security.

Use Ledger's own repository and Quickstart for setup. The basic path usually looks like this:

SPECULOS · LEARNING SETUP
git clone https://github.com/LedgerHQ/speculos.git
cd speculos
python3 -m venv .venv
source .venv/bin/activate
pip install .
./speculos.py apps/boil.elf
# then open http://127.0.0.1:5000
Docker or WSL may be easier on some macOS and Windows setups. Check the official Quickstart for your operating system.
Why do we still need an app file?

Speculos isn't a ready-made wallet. Think of it like a game console that still needs a game. The .elf file is the Ledger App running inside the emulator.

In our project, signer_boundary is the adapter point. For now, it only reports that the proposal reached the boundary. A more realistic connection needs:

StageLearning versionMore realistic version
Agent proposalFake JSONDecoded, human-readable transaction
PolicyAmount cap + allowlistSession scope, expiry, nonce, and budget
Human reviewApprove/reject in TerminalClear destination, amount, and action meaning
SpeculosPractice a Ledger App UIAPDU adapter matching that app
HardwareNot presentKeys stay on-device; confirm on a secure screen
BroadcastIntentionally disabledOnly after valid signing and replay checks

Run these four tests

Allowed request + approve

It should reach the signer boundary, but this tutorial never broadcasts a transaction.

Amount above the limit

It should fail before human review. A person must not have a bypass path.

Destination outside the allowlist

Even a convincing request should be rejected independently by policy.

Allowed request + reject

It should end with REJECTED BY HUMAN without calling the signer.

Test a compromised agent

Temporarily change propose so it always returns an amount of 999. If the architecture is working, policy will stop it regardless of the model or prompt.

What you built is not a wallet. It's the right boundary for an agent.

The model proposes. LangGraph holds the path and pause. Policy limits what may pass. A person decides. The signer stays outside the agent runtime. Speculos lets you practice the experience of a Ledger App without pretending it provides real hardware security.

Official, up-to-date references

LangGraph Interrupts — pause, checkpoint, and resume LangChain Human-in-the-loop middleware LedgerHQ Speculos — official repository, setup, and limitations Speculos Quickstart — Linux, macOS, and Windows Ollama Quickstart — local model setup

Disclaimer: This project is for learning architecture with fake data. Never enter a seed phrase, private key, real assets, or a real transaction. Speculos is not a replacement for a Secure Element or a physical device. Parts of this guide's preparation and illustration were created with help from an AI agent.