Skip to content

Agentic Web Interop

skobeltsyn edited this page Jun 20, 2026 · 1 revision

Agentic-Web Interop — Serve, Charge, and Run an Agent Anywhere

The hands-on tour of how an Agent<IN, OUT> reaches the outside world. Agents.KT speaks the four agentic-web protocols from one from(agent) shape, can charge for a served endpoint over x402, can pay for resources it needs, and — as a spike — can even run in a browser over WebAssembly.

Layer Protocol Surface
agent ↔ tools MCP McpServer.from(agent) — see MCP Integration
agent ↔ agent A2A A2AServer.from(agent) / a2aAgent<IN,OUT>(url)
agent ↔ web content NLWeb NlWebServer.from(agent)
agent ↔ user/frontend AG-UI AgUiServer.from(agent)
settlement (beneath all) x402 X402PaymentGate (sell) / X402Client (buy)

All serve surfaces bind 127.0.0.1 only and take an optional bearerToken — front them with a TLS gateway for network reach.


1. Serve an agent to a frontend (AG-UI)

Turn any agent into a streaming chat backend a CopilotKit/React UI can consume.

val support = agent<String, String>("support") {
    model { ollama("gemma3:4b") }
    tools { val lookup = tool("lookup_account", "Look up an account") { a -> accounts[a["id"]] } }
    skills { skill<String, String>("help", "Answer support questions") { tools(lookup) } }
}

val server = AgUiServer.from(support, port = 8765).start()

POST a RunAgentInput; the last user message is the input, and you get an SSE stream:

curl -N http://localhost:8765/agent -H 'Content-Type: application/json' \
  -d '{"threadId":"t1","runId":"r1","messages":[{"role":"user","content":"why was my card declined?"}]}'
data: {"type":"RUN_STARTED","threadId":"t1","runId":"r1"}
data: {"type":"TEXT_MESSAGE_START","role":"assistant", ...}
data: {"type":"TEXT_MESSAGE_CONTENT","delta":"Let me check…"}
data: {"type":"TOOL_CALL_START","toolCallId":"c1","toolCallName":"lookup_account"}
data: {"type":"TOOL_CALL_END","toolCallId":"c1"}
data: {"type":"TOOL_CALL_RESULT","toolCallId":"c1","content":"{\"status\":\"past_due\"}","role":"tool","isError":false}
data: {"type":"TEXT_MESSAGE_CONTENT","delta":"Your account is past due."}
data: {"type":"RUN_FINISHED","threadId":"t1","runId":"r1"}

TOOL_CALL_RESULT carries what the tool returned — the frontend renders outputs, not just invocations. If the model emits a thinking stream (model { reasoning(...) }), it surfaces as the REASONING_* family before the answer. Full reference: docs/agui.md.


2. Charge for it (x402, seller side)

Front any served endpoint with a payment gate so it's served only after a settled USDC payment. You hold no key and take no custody — a hosted facilitator verifies + settles.

val gate = X402PaymentGate(
    PaymentRequirements(
        network = "base-sepolia", maxAmountRequired = "1000",   // testnet first!
        payTo = "0xYourPublicAddress",
        asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e",   // USDC on Base Sepolia
        resource = "/agent", extra = mapOf("name" to "USD Coin", "version" to "2"),
    ),
    facilitator = HttpFacilitatorClient("https://x402.org/facilitator"),  // free testnet facilitator
)
val paidServer = AgUiServer.from(support, payment = gate).start()

Unpaid requests get 402 with the terms; anything that fails, fails closed (never served unpaid).


3. Pay for a resource (x402, buyer side — guardrails-first)

Let an agent autonomously pay for something it needs. This is where irreversible money moves, so the key lives below the model layer and every payment clears a spend policy first.

val account = X402Account.fromPrivateKey(
    System.getenv("X402_KEY"),                              // never in a prompt
    X402SpendPolicy(
        maxValuePerPayment = BigInteger.valueOf(2_000),     // hard cap
        allowedPayTo = setOf("0xKnownSeller"),              // pin the counterparty
        confirm = { plan -> humanApproves(plan) },          // optional human-in-the-loop
    ),
)
val resp = X402Client(account).get("https://seller.example/agent")   // 402 -> sign -> retry -> 200

A rejected payment throws X402PaymentDeniedExceptionno signature, no money moved. Test the whole signing path for free on Base Sepolia (testnet USDC + the free x402.org/facilitator); the signing itself is real secp256k1 + EIP-712/EIP-3009, pinned to ethers.js vectors. Full reference + sandbox recipes: docs/x402.md.


4. Run an agent in the browser (WebAssembly — spike)

The typed core (Agent, Skill, then) is not JVM-bound: a no-reflection slice compiles to wasmJs and runs in a browser/node, reaching a local LLM over fetch.

cd wasm_tmp && ./run.sh        # builds the wasm bundle, serves on :8080, opens the browser

Verified end-to-end against local Ollama in both node and headless Chrome. Must be served over http (ES modules can't load via file://). Status: feasibility spike (#4548), conditional GO — see docs/wasm.md.


Serving several at once

The same agent instance can serve multiple protocols simultaneously — each from(...) is an independent wrapper that just invokes the agent (invocation is concurrent-safe):

val mcp  = McpServer.from(support).start()
val a2a  = A2AServer.from(support, bearerToken = t).start()
val agui = AgUiServer.from(support).start()
// three loopback listeners; a gateway in front makes that one address.

See also

Clone this wiki locally