Building a Monetised AI Agent the Right Way
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:
- Runs as a Fetch.ai uAgent
- Is deployed and discoverable on Agentverse
- Communicates using the Chat Protocol
- Requests payment using the Payment Protocol
- Verifies payment programmatically
- 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
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_KEYSKYFIRE_SERVICE_IDSELLER_ACCOUNT_ID - Basic familiarity with Python and async programming
Project Overview
We’ll build an Image Generation Agent that:
- Receives chat messages from users
- Requests a small payment (0.001 USDC) via Skyfire
- Verifies the payment token
- Generates an image using Pollinations API
- Returns the image as a resource through the chat protocol
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.mdStep 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=Trueenables Agentverse messagingpublish_manifest=Truemakes 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.5Install them:
pip install -r requirements.txtConfigure 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_hereStep 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_IDStep 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
- Activate your virtual environment (if using one):
source .venv/bin/activate # or `venv\Scripts\activate` on Windows- Start the agent:
python agent.py- 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:
- User sends message → Agent receives
ChatMessage - Payment check → Agent checks if the user has paid
- Request payment → Agent sends
RequestPaymentwith USDC details - User commits → Buyer sends
CommitPaymentwith Skyfire token - Verify token → Agent verifies JWT signature and claims
- Charge payment → Agent charges token via Skyfire API
- Confirm payment → Agent sends
CompletePayment - 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_KEYis correct - Verify
SKYFIRE_SERVICE_IDmatches your Skyfire service - Ensure
SELLER_ACCOUNT_IDmatches JWT audience - Check the token hasn’t expired (5-minute deadline)
JWT Verification Errors
Symptoms: JWT verification failed in logs
Solutions:
- Verify
JWKS_URLis accessible - Check
JWT_ISSUERmatches Skyfire issuer - Ensure token
ssiclaim matchesSKYFIRE_SERVICE_ID
Charge API Errors
Symptoms: Skyfire charge error in logs
Solutions:
- Verify
SKYFIRE_API_KEYhas charging permissions - Check
SKYFIRE_TOKENS_API_URLis correct - Ensure the buyer has a sufficient USDC balance
Storage Errors
Symptoms: Storage not configured error
Solutions:
- Set
AGENTVERSE_API_KEYin.env - Verify
AGENTVERSE_URLis 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
- Skyfire Documentation — Complete API reference
- uAgents Documentation — Agent framework docs
- Payment Protocol Spec — Protocol details
- Skyfire Dashboard — Manage your services
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!








