Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Build Agentic Commerce Workflows

PaymentsAdvanced40 min

Agentic commerce on Tenzro sits on four independent subsystems: TDIP identity for provisioning humans and machines, MPP for session-based streaming payments, x402 for stateless HTTP 402 pay-resource flows, and the settlement engine for escrowed service delivery. In this tutorial you'll build every one of those flows end-to-end using the tenzro CLI and AgentKit templates — no Rust source code, no cargo build, no compiled examples. Every step is a shell command or a curl call against a running Tenzro node.

What You'll Build

  • Identity + wallet binding workflow — provision a TDIP human DID and auto-provision its MPC wallet with a single CLI command
  • MPP payment workflow — spawn the ref-mpp-payment-agent-v1 AgentKit template to create a challenge, sign an Ed25519 credential, verify it, and settle on-chain
  • MPP tamper-rejection workflow — confirm the server rejects credentials whose amount was altered post-signing
  • x402 pay-resource workflow — build a requirement, submit valid/underpaid/wrong-chain payloads to the facilitator via JSON-RPC, and settle the accepted one
  • Settlement engine workflow — release escrow to a provider via JSON-RPC and verify the 0.5% network fee math

Prerequisites

You need a running Tenzro node (local or testnet) and the tenzro CLI installed. If you're running against the public testnet, the RPC endpoint is https://rpc.tenzro.network. For a local node, the default is http://127.0.0.1:8545.

# Set the RPC endpoint for all commands in this tutorial.
# Use your local node or the public testnet.
export TENZRO_RPC="https://rpc.tenzro.network"

# Verify the CLI is installed and the node is reachable.
tenzro info --rpc $TENZRO_RPC

You should see the node's block height, peer count, and role in the output. If the node is not reachable, check your endpoint and try again before continuing.

Step 1: Identity Provisioning + Wallet Binding

Every agentic commerce flow starts with identity. TDIP gives you a unified registry that provisions both human and machine DIDs, and the wallet binder auto-provisions an MPC wallet for each new identity at registration time — no seed phrase, no separate wallet creation step.

The fastest way to get started is the tenzro join command. It provisions your identity, wallet, and hardware profile in a single call:

# One-click: provisions identity + wallet + hardware profile.
tenzro join --rpc $TENZRO_RPC

Expected output:

Participating in Tenzro Network...
Identity provisioned: did:tenzro:human:a1b2c3d4-...
Wallet created: 0x7f3a...
Hardware profile detected: x86_64, 16 cores, 32 GB RAM
Network participation complete.

Save the DID for later steps. If you need more control over the identity, you can register one explicitly using the identity subcommand instead:

# Register a human identity with Enhanced KYC tier.
tenzro identity register \
  --type human \
  --display-name "Alice" \
  --kyc-tier enhanced \
  --rpc $TENZRO_RPC

This returns the full identity record including the DID, wallet address, and wallet id. Verify the round-trip by resolving the DID back through the registry:

# Store the DID from the previous command.
export PAYER_DID="did:tenzro:human:a1b2c3d4-..."

# Resolve the DID to confirm it's registered.
tenzro identity resolve --did $PAYER_DID --rpc $TENZRO_RPC

# Export the W3C DID Document for interoperability.
tenzro identity document --did $PAYER_DID --rpc $TENZRO_RPC

The resolve command returns the identity's status, KYC tier, wallet address, and public keys. The document command exports a standard W3C DID Document that any DID-compatible system can consume. The important thing is that the DID, wallet address, and wallet id all flow out of a single registration call — you do not manage wallet provisioning separately.

Fund the wallet from the testnet faucet so you have TNZO for later payment steps:

# Request 100 testnet TNZO (24h cooldown per address).
curl -s -X POST $TENZRO_RPC/../api/faucet \
  -H "Content-Type: application/json" \
  -d '{"address": "0x7f3a..."}' | jq .

# Verify the balance.
tenzro wallet balance --rpc $TENZRO_RPC

Step 2: MPP Payment Workflow with AgentKit

MPP (Machine Payments Protocol, co-authored by Stripe and Tempo) is the session-based streaming payment protocol. The flow is: server creates an MppChallenge pinned to a resource, client signs a PaymentCredential over the canonical message, server verifies against the stored challenge, server settles (producing a receipt), and the challenge is consumed.

Instead of writing Rust code to drive this flow, use the ref-mpp-payment-agent-v1 AgentKit template. This template encapsulates the entire MPP happy path — challenge creation, Ed25519 credential signing, verification, and settlement — in a pre-built agent that you spawn and run from the CLI.

First, inspect the available templates and confirm the MPP template is registered:

# List all available AgentKit templates.
tenzro agent list-templates --rpc $TENZRO_RPC

# Inspect the MPP payment template in detail.
tenzro agent get-template \
  --template-id ref-mpp-payment-agent-v1 \
  --rpc $TENZRO_RPC

The template metadata shows its required parameters, supported payment assets, and the canonical credential signing format it uses. Now spawn an instance of the template bound to your identity:

# Spawn an MPP payment agent from the template.
# This creates a running agent instance bound to your DID.
tenzro agent spawn-template \
  --template-id ref-mpp-payment-agent-v1 \
  --param payer_did=$PAYER_DID \
  --param resource="/api/inference" \
  --param amount=1000 \
  --param asset="USDC" \
  --param recipient="0xrecipient" \
  --rpc $TENZRO_RPC

Expected output:

Agent spawned: agent-mpp-a1b2c3d4
Template: ref-mpp-payment-agent-v1
Status: ready
Parameters:
  payer_did: did:tenzro:human:a1b2c3d4-...
  resource: /api/inference
  amount: 1000
  asset: USDC
  recipient: 0xrecipient

Now run the agent. This executes the full MPP flow — challenge creation, credential signing with a real Ed25519 key, verification against the server's canonical message format, settlement, and challenge consumption:

# Execute the MPP payment flow end-to-end.
tenzro agent run-template \
  --template-id ref-mpp-payment-agent-v1 \
  --param payer_did=$PAYER_DID \
  --param resource="/api/inference" \
  --param amount=1000 \
  --param asset="USDC" \
  --param recipient="0xrecipient" \
  --rpc $TENZRO_RPC

Expected output:

[mpp] Created challenge: id=ch-9f8e7d... amount=1000 asset=USDC protocol=mpp
[mpp] Signed credential: cred-inference-1 (Ed25519, canonical message)
[mpp] Credential verified: true (payer_did=did:tenzro:human:a1b2c3d4-...)
[mpp] Settlement receipt: amount=1000 asset=USDC challenge_id=ch-9f8e7d...
[mpp] Challenge consumed after settle: true
MPP payment flow completed successfully.

The canonical credential message

The MPP server's construct_credential_message() is pure concatenation: no JSON, no length prefixes, no domain separators. The message is challenge_id ++ payer_did ++ amount.to_le_bytes() ++ asset. The ref-mpp-payment-agent-v1 template handles this signing format internally, so you do not need to construct it manually. If you are building a custom agent, the exact byte layout matters — any drift between client and server on amount endianness or asset casing will break verification.

You can also drive the MPP flow step-by-step using the tenzro payment CLI commands if you need finer-grained control:

# Step-by-step MPP flow using the CLI.

# 1. Create a payment challenge.
tenzro payment challenge \
  --protocol mpp \
  --resource "/api/inference" \
  --amount 1000 \
  --asset USDC \
  --recipient 0xrecipient \
  --rpc $TENZRO_RPC

# Save the challenge ID from the output.
export CHALLENGE_ID="ch-9f8e7d..."

# 2. Pay the challenge (signs and submits the credential).
tenzro payment pay \
  --challenge-id $CHALLENGE_ID \
  --protocol mpp \
  --rpc $TENZRO_RPC

# 3. Check the session status.
tenzro payment sessions --rpc $TENZRO_RPC

# 4. Retrieve the receipt.
tenzro payment receipt \
  --challenge-id $CHALLENGE_ID \
  --rpc $TENZRO_RPC

Step 3: MPP Tamper Rejection

The happy path is only half the story. A real payment protocol must reject credentials whose signed fields have been altered — even a single byte. To test this, use a JSON-RPC call to submit a credential with a tampered amount directly to the node. The server should return an error, not a successful verification.

First, create a fresh challenge:

# Create a challenge for the tamper test.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tenzro_createPaymentChallenge",
    "params": {
      "protocol": "mpp",
      "resource": "/api/inference",
      "amount": 2000,
      "asset": "USDC",
      "recipient": "0xrecipient"
    }
  }' | jq .

# Save the challenge_id from the response.
export TAMPER_CHALLENGE_ID="ch-..."

Now submit a payment credential via JSON-RPC, but set the amount field to 1 instead of the original 2000. The signature was computed over the original fields, so the server will detect the mismatch:

# Submit a credential with a tampered amount.
# The signature was computed over amount=2000, but we claim amount=1.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tenzro_payMpp",
    "params": {
      "challenge_id": "'$TAMPER_CHALLENGE_ID'",
      "amount": 1,
      "asset": "USDC"
    }
  }' | jq .

Expected output:

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32602,
    "message": "credential verification failed: signature mismatch"
  }
}

Note that the server rejects this at the verification layer, not at the settle layer. That's the correct place — you want the tamper detection to happen before any state change, so a bad credential never even touches the settlement engine.

Step 4: x402 Pay-Resource Flow

x402 is Coinbase's stateless HTTP 402 payment protocol. Unlike MPP, there is no server-side session: the facilitator accepts a payment requirement describing what's owed and a payment payload describing what the payer is tendering, and returns a boolean verification result. The happy path accepts, and failure paths reject — underpayment, wrong chain, and (in production) bad signatures.

Use curl to drive the x402 flow through JSON-RPC. First, check which payment protocols the node supports:

# List supported payment protocols.
tenzro payment info --rpc $TENZRO_RPC

Now submit three x402 payloads: one valid, one underpaid, and one on the wrong chain. The valid payload should be accepted, and the other two should be rejected:

# Valid x402 payload  should be accepted.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tenzro_payX402",
    "params": {
      "chain": "tenzro",
      "asset": "USDC",
      "amount": "1000",
      "payer": "0xpayer",
      "recipient": "0xrecipient",
      "authorization": "auth-blob"
    }
  }' | jq .

Expected output shows the payment was accepted:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "accepted": true,
    "settlement_ref": "x402-settlement-..."
  }
}

Now test the rejection paths:

# Underpaid x402 payload  should be rejected.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tenzro_payX402",
    "params": {
      "chain": "tenzro",
      "asset": "USDC",
      "amount": "500",
      "payer": "0xpayer",
      "recipient": "0xrecipient",
      "authorization": "auth-blob"
    }
  }' | jq .

# Wrong-chain x402 payload  should be rejected.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 5,
    "method": "tenzro_payX402",
    "params": {
      "chain": "ethereum",
      "asset": "USDC",
      "amount": "1000",
      "payer": "0xpayer",
      "recipient": "0xrecipient",
      "authorization": "auth-blob"
    }
  }' | jq .

Both should return "accepted": false. The underpaid payload fails because 500 is less than the required 1,000. The wrong-chain payload fails because the facilitator only supports chains it was configured with (in this case, tenzro).

MPP vs x402 — when to use which

Both protocols work over HTTP 402, but they target different usage patterns:

  • MPP is session-based — the server creates an MppChallenge, the client attaches credentials for repeated use within the session, and settlement happens once. Ideal for streaming inference or multi-step agent flows.
  • x402 is stateless — every request stands alone with its own payment requirement envelope. Ideal for one-shot pay-per-resource fetches and cross-platform interop.

Step 5: Settlement Engine Escrow Release

Payment credentials get you into the gate. Settlement is what actually moves TNZO (or any supported asset) from one account to another on-ledger, applies the 0.5% network fee, and produces a settlement receipt. The settlement engine takes a request with a customer, a provider, a service type, an amount, and a service proof that includes at least one provider signature.

Use JSON-RPC to issue a settlement request and verify the fee math. The 0.5% fee (50 bps) on 10,000 is 50, so the provider should receive exactly 9,950:

# Issue a settlement request for model inference.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 6,
    "method": "tenzro_settle",
    "params": {
      "provider": "0xprovider_address",
      "customer": "0xcustomer_address",
      "service_type": "model_inference",
      "model_id": "tenzro-inference-model",
      "tokens": 1234,
      "amount": 10000,
      "proof_type": "cryptographic",
      "proof_data": "6167656e7469632d696e666572656e63652d72656365697074"
    }
  }' | jq .

Expected output:

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "status": "Completed",
    "amount": 10000,
    "fee": 50,
    "provider_received": 9950,
    "settlement_id": "settle-..."
  }
}

Verify the balances after settlement to confirm the fee distribution:

# Check the settlement details.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 7,
    "method": "tenzro_getSettlement",
    "params": {
      "settlement_id": "settle-..."
    }
  }' | jq .

# Check balances via the CLI.
tenzro wallet balance --address 0xcustomer_address --rpc $TENZRO_RPC
tenzro wallet balance --address 0xprovider_address --rpc $TENZRO_RPC

The fee math is deliberate: the customer's debit reflects the full settlement amount (10,000), the provider receives amount - fee (9,950), and the remaining 50 units flow to the network treasury. This is how the network captures its commission on every model inference and TEE service payment — on-chain, by construction, with no off-chain reconciliation.

Step 6: Nanopayment Channels

For per-token billing, on-chain settlement per token is prohibitively expensive. Nanopayment channels solve this by opening a channel with a deposit, sending many nanopayments off-chain (instant, zero gas), and settling periodically with a single on-chain transaction. This is ideal for AI inference where an agent may consume thousands of tokens per session.

The flow is: open a channel with a TNZO deposit, send nanopayments for each inference token consumed (off-chain, signed state updates), and flush the accumulated batch to the ledger when ready. One on-chain transaction replaces thousands of individual payments.

import { TenzroClient } from "@tenzro/sdk";

const client = new TenzroClient("https://rpc.tenzro.network");
const nano = client.nanopayment();

// Open channel with deposit
const channel = await nano.openChannel(
  agentAddress,
  providerAddress,
  "10000000000000000000", // 10 TNZO deposit
  "TNZO"
);

// Send nanopayments per inference token (off-chain, instant)
for (const token of inferenceTokens) {
  await nano.sendNanopayment(channel.channelId, tokenPrice, `token-${token.id}`);
}

// Settle batch on-chain (1 tx instead of 1000)
const settlement = await nano.flushBatch(channel.channelId);
console.log(`Settled ${settlement.batchCount} payments for ${settlement.totalAmount}`);

The channel deposit acts as collateral: the provider knows the agent has locked 10 TNZO, so it can safely deliver tokens without waiting for on-chain confirmation on each one. If the agent disappears mid-session, the provider can close the channel and claim the accumulated amount. If the provider tries to claim more than was agreed, the agent can dispute within the challenge period using the latest signed state update.

When to use nanopayment channels vs MPP

  • MPP settles once per session. Good for moderate token volumes (tens to hundreds of tokens per session).
  • Nanopayment channels settle on a batch schedule you control. Good for high-frequency consumption (thousands to millions of tokens) where even one settlement per session adds up across many sessions per day.

Step 7: Circuit Breakers for Provider Resilience

In production, agents interact with multiple inference providers. When a provider goes down, you do not want every agent in the fleet to keep hammering it with requests and accumulating timeouts. Circuit breakers protect agents from cascading provider failures by tracking error rates and automatically routing around unhealthy providers.

A circuit breaker has three states: Closed (normal operation, requests flow through), Open (provider is down, requests are blocked immediately without attempting), and Half-Open (testing recovery by allowing a limited number of probe requests). When the failure count exceeds the threshold, the breaker opens. After a recovery timeout, it enters half-open to test whether the provider has recovered.

import { TenzroClient } from "@tenzro/sdk";

const client = new TenzroClient("https://rpc.tenzro.network");
const cb = client.circuitBreaker();

// Check provider health before routing
const health = await cb.getProviderHealth("provider-001");
if (health.state === "open") {
  console.log("Provider down, routing to backup...");
  // Use alternative provider
}

// Configure circuit breaker thresholds
await cb.configureBreaker("provider-001", {
  failureThreshold: 5,
  recoveryTimeoutSecs: 30,
  halfOpenMaxCalls: 2,
});

The failureThreshold is the number of consecutive failures before the breaker opens. The recoveryTimeoutSecs is how long the breaker stays open before transitioning to half-open. The halfOpenMaxCalls is the number of probe requests allowed in half-open state before the breaker decides whether to close (if probes succeed) or re-open (if probes fail).

Circuit breakers compose naturally with the inference router's strategy selection. When the breaker for a provider opens, the router automatically falls back to the next provider in the ranking (by price, latency, or reputation). The agent does not need to implement fallback logic itself.

Step 8: Full End-to-End Script

Here is the complete workflow as a single shell script. Copy it, set your RPC endpoint, and run the whole agentic commerce pipeline in one pass:

#!/usr/bin/env bash
set -euo pipefail

# Configuration
TENZRO_RPC="${TENZRO_RPC:-https://rpc.tenzro.network}"
echo "Using RPC endpoint: $TENZRO_RPC"

echo ""
echo "=== Step 1: Identity Provisioning ==="
# One-click join: identity + wallet + hardware profile.
tenzro join --rpc $TENZRO_RPC

# Resolve the identity to get the DID.
PAYER_DID=$(tenzro identity list --rpc $TENZRO_RPC --format json | jq -r '.[0].did')
echo "Payer DID: $PAYER_DID"

# Fund from faucet.
WALLET_ADDR=$(tenzro wallet list --rpc $TENZRO_RPC --format json | jq -r '.[0].address')
curl -s -X POST "${TENZRO_RPC%/8545}/api/faucet" \
  -H "Content-Type: application/json" \
  -d "{\"address\": \"$WALLET_ADDR\"}" | jq .

echo ""
echo "=== Step 2: MPP Payment (AgentKit) ==="
# Run the full MPP flow via the AgentKit template.
tenzro agent run-template \
  --template-id ref-mpp-payment-agent-v1 \
  --param payer_did=$PAYER_DID \
  --param resource="/api/inference" \
  --param amount=1000 \
  --param asset="USDC" \
  --param recipient="0xrecipient" \
  --rpc $TENZRO_RPC

echo ""
echo "=== Step 3: MPP Tamper Rejection ==="
# Create a challenge, then attempt to pay with wrong amount.
CHALLENGE=$(curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1,
    "method": "tenzro_createPaymentChallenge",
    "params": {
      "protocol": "mpp",
      "resource": "/api/inference",
      "amount": 2000,
      "asset": "USDC",
      "recipient": "0xrecipient"
    }
  }')
TAMPER_CID=$(echo $CHALLENGE | jq -r '.result.challenge_id')
echo "Challenge created: $TAMPER_CID"

# Submit tampered credential (amount=1 instead of 2000).
TAMPER_RESULT=$(curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d "{
    \"jsonrpc\": \"2.0\", \"id\": 2,
    \"method\": \"tenzro_payMpp\",
    \"params\": {
      \"challenge_id\": \"$TAMPER_CID\",
      \"amount\": 1,
      \"asset\": \"USDC\"
    }
  }")
echo "Tamper rejection result:"
echo $TAMPER_RESULT | jq .

echo ""
echo "=== Step 4: x402 Pay-Resource ==="
# Valid payload.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 3,
    "method": "tenzro_payX402",
    "params": {
      "chain": "tenzro", "asset": "USDC", "amount": "1000",
      "payer": "0xpayer", "recipient": "0xrecipient",
      "authorization": "auth-blob"
    }
  }' | jq '.result.accepted'

# Underpaid payload.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 4,
    "method": "tenzro_payX402",
    "params": {
      "chain": "tenzro", "asset": "USDC", "amount": "500",
      "payer": "0xpayer", "recipient": "0xrecipient",
      "authorization": "auth-blob"
    }
  }' | jq '.result.accepted'

# Wrong-chain payload.
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 5,
    "method": "tenzro_payX402",
    "params": {
      "chain": "ethereum", "asset": "USDC", "amount": "1000",
      "payer": "0xpayer", "recipient": "0xrecipient",
      "authorization": "auth-blob"
    }
  }' | jq '.result.accepted'

echo ""
echo "=== Step 5: Settlement Escrow Release ==="
curl -s -X POST $TENZRO_RPC \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 6,
    "method": "tenzro_settle",
    "params": {
      "provider": "0xprovider_address",
      "customer": "0xcustomer_address",
      "service_type": "model_inference",
      "model_id": "tenzro-inference-model",
      "tokens": 1234,
      "amount": 10000,
      "proof_type": "cryptographic",
      "proof_data": "6167656e7469632d696e666572656e63652d72656365697074"
    }
  }' | jq .

echo ""
echo "All agentic commerce workflows finished."

Save this as agentic-commerce.sh, make it executable with chmod +x agentic-commerce.sh, and run it. The whole sequence should complete in a few seconds against a live node.

What You Learned

Next Steps

Now that you've exercised every layer of the agentic commerce stack from the command line, the next step is to compose them into production workflows:

CLI Reference

All commands used in this tutorial are available in the tenzro CLI. For complete usage information:

tenzro --help
tenzro payment --help
tenzro identity --help
tenzro agent --help
tenzro wallet --help