Sitemap
Rohan Raghuwanshi

Rohan Raghuwanshi

Not just another engineer.

Building a Monetised AI Agent the Right Way

10 min readJan 2, 2026

--

Press enter or click to view image in full size

Introduction

The ASI ecosystem is unlocking a new era of autonomous, revenue-generating AI agents. With tools like uAgents, Agentverse, and standardized protocols for chat and payments, you can now deploy agents that don’t just respond — they operate, transact, and monetize independently.

In this guide, we’ll walk through building a paid AI agent on ASI that runs on Agentverse, communicates via standard agent protocols, and charges users programmatically before executing tasks.

We’ll use a real-world example throughout the tutorial:

An image generation agent deployed on Agentverse that requests payment, verifies it, and delivers results autonomously.

This tutorial is designed for developers building in the Fetch.ai / ASI ecosystem who want to move beyond demos and into production-ready agent services.

Why ASI + Agentverse?

Agentverse is the execution and discovery layer of the ASI ecosystem. It allows agents to:

  • Communicate using standardized agent protocols
  • Discover and interact with other agents
  • Persist sessions and assets
  • Operate autonomously without centralized servers
  • Monetize services in a trust-minimized way

By combining uAgents, Agentverse storage, and payment protocols, you can build agents that behave like independent digital businesses.

You can find more about these here :

https://innovationlab.fetch.ai/resources/docs/intro

What We’re Building

We’ll build an Image Generation Agent that:

  1. Runs as a Fetch.ai uAgent
  2. Is deployed and discoverable on Agentverse
  3. Communicates using the Chat Protocol
  4. Requests payment using the Payment Protocol
  5. Verifies payment programmatically
  6. Generates an image

What is Skyfire?

Skyfire is an identity and payment protocol designed specifically for AI agents. It enables:

  • Token-based payments: Buyers pay using JWT tokens (kya+pay tokens) that combine identity verification with payment authorization
  • Programmatic transactions: Agents can request, verify, and charge payments automatically
  • USDC payments: All transactions use USDC, providing a stable, reliable payment method
  • Identity verification: Built-in KYA (Know Your Agent) ensures secure, verified transactions
Press enter or click to view image in full size

Prerequisites

Before we begin, make sure you have:

  • Python 3.10+ installed
  • A Skyfire seller account (sign up at app.skyfire.xyz)
  • Your Skyfire credentials:
    SKYFIRE_API_KEY
    SKYFIRE_SERVICE_ID
    SELLER_ACCOUNT_ID
  • Basic familiarity with Python and async programming

Project Overview

We’ll build an Image Generation Agent that:

  1. Receives chat messages from users
  2. Requests a small payment (0.001 USDC) via Skyfire
  3. Verifies the payment token
  4. Generates an image using Pollinations API
  5. Returns the image as a resource through the chat protocol
Press enter or click to view image in full size

Project Structure

skyfire-integration/
├── agent.py # Main agent setup
├── chat_proto.py # Chat protocol handlers
├── payment_proto.py # Payment protocol implementation
├── skyfire.py # Skyfire JWT verification & charging
├── .env # Environment variables
├── requirements.txt # Python dependencies
└── README.md

Step 1: Setting Up the Environment

First, let’s create our project structure and install dependencies.

We start by creating a standard uAgent configured for Agentverse.

Key points:

  • mailbox=True enables Agentverse messaging
  • publish_manifest=True makes the agent discoverable

This agent will act as an autonomous service provider on ASI.

Install Dependencies

Create a requirements.txt:

uagents>=0.23.2
uagents-core>=0.23.2
aiohttp>=3.12.15
python-jose[cryptography]>=3.5.0
python-dotenv>=1.1.1
requests>=2.32.5

Install them:

pip install -r requirements.txt

Configure Environment Variables

Create a .env file with your Skyfire credentials:

# Skyfire Configuration
SKYFIRE_API_KEY=your_skyfire_api_key_here
SKYFIRE_SERVICE_ID=your_service_id_here
SELLER_ACCOUNT_ID=your_seller_account_id_here

# Optional: Override defaults
JWKS_URL=https://app.skyfire.xyz/.well-known/jwks.json
JWT_ISSUER=https://app.skyfire.xyz/
SKYFIRE_TOKENS_API_URL=https://api.skyfire.xyz/api/v1/tokens/charge

# Agentverse Configuration (for agent communication)
AGENTVERSE_URL=https://agentverse.ai
AGENTVERSE_API_KEY=your_agentverse_api_key_here

Step 2: Implementing Skyfire Helpers

The core of our Skyfire integration is the skyfire.py module, which handles JWT verification and charging.

skyfire.py

import logging
import os
from typing import Optional, Any
import aiohttp
from jose import JWTError, jwk, jwt

# Production endpoints
APP_BASE = "https://app.skyfire.xyz"
API_BASE = "https://api.skyfire.xyz"

# JWKS/JWT configuration
JWKS_URL = os.getenv("JWKS_URL") or f"{APP_BASE}/.well-known/jwks.json"
JWT_ISSUER = (os.getenv("JWT_ISSUER") or APP_BASE).rstrip("/")
JWT_AUDIENCE = os.getenv("JWT_AUDIENCE") or os.getenv("SELLER_ACCOUNT_ID")
JWT_ALGORITHM = "ES256"

# Charge API endpoint
SKYFIRE_TOKENS_API_URL = os.getenv(
"SKYFIRE_TOKENS_API_URL",
f"{API_BASE}/api/v1/tokens/charge"
)

# Skyfire credentials
SKYFIRE_API_KEY = os.getenv("SKYFIRE_API_KEY")
SKYFIRE_SERVICE_ID = os.getenv("SKYFIRE_SERVICE_ID")
SELLER_ACCOUNT_ID = os.getenv("SELLER_ACCOUNT_ID")


async def get_jwks_from_url(https://medium.com/jwks_url:%20str):
"""Fetch JWKS (JSON Web Key Set) from Skyfire."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(jwks_url) as response:
response.raise_for_status()
return await response.json()
except aiohttp.ClientError as e:
raise Exception(f"Failed to fetch JWKS: {e}")


def get_signing_key(jwks_data: dict[str, Any], kid: str):
"""Extract the signing key from JWKS using key ID."""
for key in jwks_data.get("keys", []):
if key.get("kid") == kid:
return jwk.construct(key, algorithm=JWT_ALGORITHM)
raise Exception(f"Unable to find key with kid: {kid}")


async def verify_token_claims(
skyfire_token: str,
logger: logging.Logger
) -> bool:
"""
Verify a Skyfire JWT token.

Steps:
1. Extract key ID (kid) from token header
2. Fetch JWKS from Skyfire
3. Decode and verify JWT signature
4. Validate issuer, audience, and service ID
"""
try:
# Get key ID from token header
unverified_header = jwt.get_unverified_header(skyfire_token)
kid = unverified_header.get("kid")
if not kid:
raise JWTError("Token header missing 'kid'")

# Fetch JWKS and get signing key
jwks_data = await get_jwks_from_url(https://medium.com/JWKS_URL)
signing_key = get_signing_key(jwks_data, kid)

# Decode and verify token
audience = JWT_AUDIENCE or SELLER_ACCOUNT_ID or ""
claims = jwt.decode(
skyfire_token,
signing_key,
algorithms=[JWT_ALGORITHM],
audience=audience,
issuer=JWT_ISSUER,
)

# Verify service ID matches
ssi = claims.get("ssi")
if SKYFIRE_SERVICE_ID and ssi != SKYFIRE_SERVICE_ID:
raise JWTError(
f"Token is not issued for this service: {SKYFIRE_SERVICE_ID}"
)

return True
except JWTError as err:
logger.error(f"Skyfire token JWT verification failed: {err}")
return False
except Exception as err:
logger.error(f"Skyfire token verification error: {err}")
return False


async def charge_token(
token: str,
amount_to_charge: str,
logger: logging.Logger
) -> bool:
"""
Charge a verified Skyfire token via the Skyfire API.

This deducts the specified amount from the buyer's wallet.
"""
try:
async with aiohttp.ClientSession() as session:
payload = {
"token": token,
"chargeAmount": amount_to_charge
}
async with session.post(
SKYFIRE_TOKENS_API_URL,
json=payload,
headers={
"skyfire-api-key": SKYFIRE_API_KEY or "",
"skyfire-api-version": "2",
"content-type": "application/json",
},
) as resp:
logger.info(f"Skyfire charge API status: {resp.status}")
body = await resp.text()
logger.info(f"Skyfire charge API body: {body[:500]}")
resp.raise_for_status()
return True
except aiohttp.ClientError as err:
logger.error(f"Skyfire charge error: {err}")
return False


async def verify_and_charge(
token: str,
amount_usdc: str,
logger: logging.Logger
) -> bool:
"""
Complete payment flow: verify token, then charge it.

This is the main function you'll call in your payment handler.
"""
# Check configuration
if not (
SKYFIRE_API_KEY
and SKYFIRE_SERVICE_ID
and (SELLER_ACCOUNT_ID or JWT_AUDIENCE)
):
logger.error("Skyfire seller variables not configured")
return False

# Verify token first
ok = await verify_token_claims(token, logger)
if not ok:
return False

# Charge the token
return await charge_token(token, amount_usdc, logger)


def get_skyfire_service_id() -> Optional[str]:
"""Get the configured Skyfire service ID."""
return SKYFIRE_SERVICE_ID

Step 3: Implementing the Payment Protocol

Rather than embedding payments into business logic, ASI provides a dedicated Payment Protocol.

This gives us:

  • Standardized payment messages
  • Clear lifecycle events (RequestPayment, CommitPayment, CompletePayment)
  • Protocol-level interoperability across clients and agents

The agent simply declares:

  • Accepted currency
  • Required amount
  • Payment method

Everything else flows through protocol messages.

payment_proto.py

import os
from uuid import uuid4
from uagents import Context, Protocol
from uagents_core.contrib.protocols.payment import (
Funds,
RequestPayment,
RejectPayment,
CommitPayment,
CancelPayment,
CompletePayment,
payment_protocol_spec,
)
from skyfire import verify_and_charge, get_skyfire_service_id
from chat_proto import create_text_chat

# Global agent wallet reference
_agent_wallet = None

def set_agent_wallet(wallet):
"""Set the agent wallet for payment operations."""
global _agent_wallet
_agent_wallet = wallet

# Initialize payment protocol with seller role
payment_proto = Protocol(spec=payment_protocol_spec, role="seller")
# Define accepted payment method
USDC_FUNDS = Funds(
currency="USDC",
amount="0.001",
payment_method="skyfire"
)

async def request_payment_from_user(ctx: Context, user_address: str):
"""
Send a payment request to the user.

This creates a RequestPayment message with:
- Accepted funds (USDC via Skyfire)
- Payment amount
- Deadline (5 minutes)
- Service metadata
"""
accepted_funds = [USDC_FUNDS]
skyfire_service_id = get_skyfire_service_id()
metadata = {}
if skyfire_service_id:
metadata["skyfire_service_id"] = skyfire_service_id
if _agent_wallet:
metadata["provider_agent_wallet"] = str(_agent_wallet.address())
payment_request = RequestPayment(
accepted_funds=accepted_funds,
recipient=ctx.agent.address,
deadline_seconds=300, # 5 minutes
reference=str(uuid4()),
description=(
"Image Generation Service: Please complete payment to generate "
"your image. After payment, send your image prompt."
),
metadata=metadata,
)
await ctx.send(user_address, payment_request)

@payment_proto.on_message(CommitPayment)
async def handle_commit_payment(ctx: Context, sender: str, msg: CommitPayment):
"""
Handle payment commitment from buyer.

When a buyer commits to payment, we:
1. Verify the Skyfire token
2. Charge the specified amount
3. Mark the session as paid
4. Send CompletePayment confirmation
"""
ctx.logger.info(f"Received payment commitment from {sender}: {msg}")
payment_verified = False

# Verify and charge Skyfire payment
if msg.funds.payment_method == "skyfire" and msg.funds.currency == "USDC":
payment_verified = await verify_and_charge(
msg.transaction_id, # This is the Skyfire token
"0.001", # Amount in USDC
ctx.logger
)
if payment_verified:
session_id = str(ctx.session)

# Mark session as paid
ctx.storage.set(f"{sender}:{session_id}:awaiting_prompt", True)
ctx.storage.set(f"{sender}:{session_id}:verified_payment", True)

# Confirm payment completion
await ctx.send(sender, CompletePayment(transaction_id=msg.transaction_id))

# Prompt user for image description
await ctx.send(
sender,
create_text_chat("Payment verified! Please send your image prompt.")
)
else:
# Payment verification failed
await ctx.send(
sender,
RejectPayment(reason="Payment verification failed")
)

@payment_proto.on_message(RejectPayment)
async def handle_reject_payment(ctx: Context, sender: str, msg: RejectPayment):
"""Handle payment rejection from buyer."""
await ctx.send(
sender,
create_text_chat(
"Payment was rejected. If you'd like to try again, "
"please send a new message."
)
)

Step 4: Implementing the Chat Protocol

The Chat Protocol is the primary interface between users and agents on ASI.

Get Rohan Raghuwanshi’s stories in your inbox

Join Medium for free to get updates from this writer.

Our agent listens for incoming ChatMessage events and:

  • Maintains session state
  • Determines whether payment has been completed
  • Routes the conversation accordingly

This allows stateful, multi-step interactions, which are essential for paid agent workflows.

chat_proto.py

from datetime import datetime, timezone
from uuid import uuid4
from uagents import Context, Protocol
from uagents_core.contrib.protocols.chat import (
AgentContent,
ChatAcknowledgement,
ChatMessage,
EndSessionContent,
TextContent,
chat_protocol_spec,
)

def create_text_chat(text: str, end_session: bool = False) -> ChatMessage:
"""Helper function to create text chat messages."""
content: list[AgentContent] = [TextContent(type="text", text=text)]
if end_session:
content.append(EndSessionContent(type="end-session"))
return ChatMessage(
timestamp=datetime.now(timezone.utc),
msg_id=uuid4(),
content=content,
)

# Initialize chat protocol
chat_proto = Protocol(spec=chat_protocol_spec)

@chat_proto.on_message(ChatMessage)
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
"""
Handle incoming chat messages.

Flow:
1. If user hasn't paid, request payment
2. If user has paid, process the image generation request
"""
from payment_proto import request_payment_from_user

session_id = str(ctx.session)

# Check if user has verified payment
has_paid = ctx.storage.get(f"{sender}:{session_id}:verified_payment")

for item in msg.content:
if isinstance(item, TextContent):
if not has_paid:
# Request payment first
await ctx.send(
sender,
create_text_chat(
"Please complete a small payment (0.001 USDC) "
"to generate your image."
)
)
await request_payment_from_user(ctx, sender)
else:
# User has paid, process the prompt
prompt = item.text
ctx.storage.set(f"prompt:{sender}:{session_id}", prompt)

# Generate and send image
await generate_image_after_payment(ctx, sender)

Step 5: Image Generation Logic

Once payment is confirmed, the agent:

  • Accepts the user’s prompt
  • Calls an external image generation service
  • Retrieves binary image data
  • Uploads the asset to Agentverse Storage
  • Sends the image back as a ResourceContent

This ensures:

  • Large payloads aren’t sent through chat
  • Assets are permissioned per user
  • Results persist beyond a single session

Add the image generation function to payment_proto.py:

async def generate_image_after_payment(ctx: Context, user_address: str):
"""Generate image after payment verification."""
from chat_proto import create_text_chat
from urllib.parse import quote
import requests
import os

session_id = str(ctx.session)
prompt = ctx.storage.get(f"prompt:{user_address}:{session_id}")

if not prompt:
await ctx.send(
user_address,
create_text_chat("Error: No prompt found")
)
return

ctx.logger.info(f"Generating image for prompt: {prompt}")

try:
# Generate image via Pollinations
pollinations_url = (
f"https://image.pollinations.ai/prompt/"
f"{quote(prompt)}?width=512&height=512"
)
resp = requests.get(pollinations_url, timeout=90)

if resp.status_code != 200 or not resp.content:
await ctx.send(
user_address,
create_text_chat("Image generation failed")
)
return

image_bytes = resp.content
mime_type = resp.headers.get("Content-Type", "image/png")

# Upload to Agentverse storage and send as resource
api_key = os.getenv("AGENTVERSE_API_KEY")
base_url = os.getenv("AGENTVERSE_URL", "https://agentverse.ai")
storage_url = f"{base_url}/v1/storage"

if not api_key:
await ctx.send(
user_address,
create_text_chat(
"Storage not configured. Please set AGENTVERSE_API_KEY."
)
)
return

from uagents_core.storage import ExternalStorage
from uagents_core.contrib.protocols.chat import (
ResourceContent,
Resource,
)

storage = ExternalStorage(api_token=api_key, storage_url=storage_url)
asset_id = storage.create_asset(
name=str(ctx.session),
content=image_bytes,
mime_type=mime_type
)
storage.set_permissions(asset_id=asset_id, agent_address=user_address)
asset_uri = f"agent-storage://{storage_url}/{asset_id}"

# Send image as resource
await ctx.send(
user_address,
ChatMessage(
timestamp=datetime.now(timezone.utc),
msg_id=uuid4(),
content=[
ResourceContent(
type="resource",
resource_id=asset_id,
resource=Resource(
uri=asset_uri,
metadata={
"mime_type": mime_type,
"role": "generated-image",
},
),
)
],
),
)
except Exception as e:
ctx.logger.error(f"Image generation error: {e}")
await ctx.send(
user_address,
create_text_chat(f"Error generating image: {e}")
)

Step 6: Creating the Main Agent

Finally, let’s tie everything together in the main agent file.

agent.py

import os
import dotenv
from uagents import Agent, Context

dotenv.load_dotenv()
from chat_proto import chat_proto
from payment_proto import payment_proto, set_agent_wallet
# Initialize agent
agent = Agent(
name="ImageGenerationAgent",
port=8021,
mailbox=True,
agentverse=os.getenv("AGENTVERSE_URL", "https://agentverse.ai"),
)
# Set agent wallet for payments
set_agent_wallet(agent.wallet)

@agent.on_event("startup")
async def startup(ctx: Context):
"""Log agent startup information."""
ctx.logger.info(f"Image Generation Agent started: {agent.wallet.address()}")
ctx.logger.info("Skyfire payment integration enabled")

# Include protocols
agent.include(chat_proto, publish_manifest=True)
agent.include(payment_proto, publish_manifest=True)

if __name__ == "__main__":
agent.run()

Step 7: Running Your Agent

  1. Activate your virtual environment (if using one):
source .venv/bin/activate  # or `venv\Scripts\activate` on Windows
  1. Start the agent:
python agent.py
  1. Test the integration:
  • Connect to your agent via Agentverse using the Agentverse Inspector
  • Send a chat message
  • Complete the payment request
  • Send your image prompt
  • Receive the generated image!

Understanding the Payment Flow

Let’s break down what happens during a payment:

  1. User sends message → Agent receives ChatMessage
  2. Payment check → Agent checks if the user has paid
  3. Request payment → Agent sends RequestPayment with USDC details
  4. User commits → Buyer sends CommitPayment with Skyfire token
  5. Verify token → Agent verifies JWT signature and claims
  6. Charge payment → Agent charges token via Skyfire API
  7. Confirm payment → Agent sends CompletePayment
  8. Process request → Agent generates and returns image

Customization Options

Change Payment Amount

Modify the amount in payment_proto.py:

USDC_FUNDS = Funds(
currency="USDC",
amount="0.01", # Change to your desired amount
payment_method="skyfire"
)

# Also update in handle_commit_payment:
payment_verified = await verify_and_charge(
msg.transaction_id,
"0.01", # Match the amount above
ctx.logger
)

Add Multiple Payment Tiers

BASIC_FUNDS = Funds(currency="USDC", amount="0.001", payment_method="skyfire")
PREMIUM_FUNDS = Funds(currency="USDC", amount="0.01", payment_method="skyfire")

# In request_payment_from_user:
accepted_funds = [BASIC_FUNDS] if is_basic_user else [PREMIUM_FUNDS]

Customize Payment Description

payment_request = RequestPayment(
accepted_funds=accepted_funds,
recipient=ctx.agent.address,
deadline_seconds=300,
reference=str(uuid4()),
description="Your custom payment description here",
metadata=metadata,
)

Troubleshooting

Payment Verification Fails

Symptoms: Payment verification failed error

Solutions:

  • Check SKYFIRE_API_KEY is correct
  • Verify SKYFIRE_SERVICE_ID matches your Skyfire service
  • Ensure SELLER_ACCOUNT_ID matches JWT audience
  • Check the token hasn’t expired (5-minute deadline)

JWT Verification Errors

Symptoms: JWT verification failed in logs

Solutions:

  • Verify JWKS_URL is accessible
  • Check JWT_ISSUER matches Skyfire issuer
  • Ensure token ssi claim matches SKYFIRE_SERVICE_ID

Charge API Errors

Symptoms: Skyfire charge error in logs

Solutions:

  • Verify SKYFIRE_API_KEY has charging permissions
  • Check SKYFIRE_TOKENS_API_URL is correct
  • Ensure the buyer has a sufficient USDC balance

Storage Errors

Symptoms: Storage not configured error

Solutions:

  • Set AGENTVERSE_API_KEY in .env
  • Verify AGENTVERSE_URL is accessible
  • Check that the API key has storage permissions

Next Steps

Now that you have a working AI agent with Skyfire integration, you can:

  • Add subscription tiers — Different payment amounts for different service levels
  • Implement usage limits — Track usage per user after payment
  • Add refund logic — Handle payment reversals
  • Integrate with other services — Use Skyfire for any paid API or service

Resources

Conclusion

Congratulations! You’ve successfully built a complete Skyfire integration. You now have:

  • A working AI agent generating revenue for you
  • JWT verification and charging logic
  • Integration with the uAgents payment protocol
  • A real-world example (image generation agent)

ASI and Skyfire make it easy to monetize your AI agents and services. With just a few files and some configuration, you can accept payments programmatically and securely.

Ready to build your own?

Start with the code examples above and customize them for your use case. Happy building!

--

--