Dynamic Actions - Data Mode
Dynamic Actions with responseType: 'data'
enable server-side processing that returns pure data responses without blockchain transactions. This mode is perfect for fetching information, performing calculations, or integrating external APIs into your mini-apps.
Overview
Unlike transaction-mode Dynamic Actions that return blockchain transactions to sign, data-mode actions return JSON responses containing any data you need to display to users.
Key Features
- No blockchain interaction required
- Fetch and display real-time data
- Integrate external APIs seamlessly
- Perform server-side computations
- Rich error messaging and status codes
- Type-safe response handling
Common Use Cases
- User profile and statistics lookup
- Real-time market data and analytics
- Weather, news, and external API integrations
- Cross-chain balance aggregation
- Off-chain calculations and computations
- Data validation and verification services
- Portfolio insights and recommendations
Action Configuration
To create a data-mode Dynamic Action, set responseType: 'data'
in your action definition:
import { createMetadata } from '@sherrylinks/sdk';
const metadata = createMetadata({
title: 'User Profile Lookup',
description: 'Fetch user information and statistics',
actions: [
{
type: 'dynamic',
label: 'Get User Info',
chains: {
source: 1, // Ethereum mainnet
},
path: '/api/user-info',
responseType: 'data', // 👈 Enable data mode
params: [
{
name: 'userId',
label: 'User ID',
type: 'string',
required: true,
},
],
},
],
});
Response Interface
Your backend must return a DynamicActionDataResponse
object:
interface DynamicActionDataResponse {
success: boolean; // Required: Indicates if the action succeeded
data?: any; // Optional: Any data you want to return
message?: string; // Optional: Success message to display
error?: string; // Optional: Error message (when success: false)
statusCode?: number; // Optional: HTTP status code
metadata?: {
// Optional: Additional context
timestamp?: number;
requestId?: string;
executionTime?: number;
[key: string]: any;
};
}
Success Response Example
{
"success": true,
"message": "User information retrieved successfully",
"data": {
"userId": "user123",
"username": "johndoe",
"email": "[email protected]",
"followers": 1250,
"following": 340,
"verified": true
},
"statusCode": 200,
"metadata": {
"timestamp": 1735891234567,
"requestId": "req_abc123",
"executionTime": 85
}
}
Error Response Example
{
"success": false,
"error": "User not found",
"message": "The requested user does not exist",
"data": {
"userId": "invalid123"
},
"statusCode": 404,
"metadata": {
"timestamp": 1735891234567,
"requestId": "req_xyz789"
}
}
Backend Implementation
Request Headers
Your backend receives these special headers with each request:
x-user-address
: The wallet address of the user performing the actionx-source-chain
: The chain ID where the action originated
Request Body
Parameters are sent in the request body:
{
"params": {
"userId": "user123",
"username": "johndoe"
}
}
Example: Next.js API Route
import { NextRequest, NextResponse } from 'next/server';
import type { DynamicActionDataResponse } from '@sherrylinks/sdk';
export async function POST(req: NextRequest) {
try {
// Extract user context from headers
const userAddress = req.headers.get('x-user-address');
const sourceChain = req.headers.get('x-source-chain');
// Parse parameters
const body = await req.json();
const { userId, username } = body.params || body;
// Validate inputs
if (!userId) {
const errorResponse: DynamicActionDataResponse = {
success: false,
error: 'Missing required parameter: userId',
message: 'Validation failed',
statusCode: 400,
};
return NextResponse.json(errorResponse, { status: 400 });
}
// Fetch user data from your database/API
const userData = await fetchUserData(userId);
if (!userData) {
const notFoundResponse: DynamicActionDataResponse = {
success: false,
error: 'User not found',
message: `No user found with ID: ${userId}`,
statusCode: 404,
};
return NextResponse.json(notFoundResponse, { status: 404 });
}
// Return success response
const successResponse: DynamicActionDataResponse = {
success: true,
message: `User information for ${userData.username}`,
data: {
userId: userData.id,
username: userData.username,
email: userData.email,
followers: userData.followers,
following: userData.following,
verified: userData.verified,
},
statusCode: 200,
metadata: {
timestamp: Date.now(),
requestId: generateRequestId(),
userAddress,
sourceChain,
},
};
return NextResponse.json(successResponse, { status: 200 });
} catch (error) {
const errorResponse: DynamicActionDataResponse = {
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
message: 'An unexpected error occurred',
statusCode: 500,
};
return NextResponse.json(errorResponse, { status: 500 });
}
}
Example: Express.js Route
import express from 'express';
const router = express.Router();
router.post('/api/weather', async (req, res) => {
try {
const userAddress = req.headers['x-user-address'];
const sourceChain = req.headers['x-source-chain'];
const { city } = req.body.params || req.body;
if (!city) {
return res.status(400).json({
success: false,
error: 'Missing required parameter: city',
message: 'Please provide a city name',
statusCode: 400,
});
}
// Fetch weather data from external API
const weatherData = await fetchWeatherAPI(city);
res.json({
success: true,
message: `Weather data for ${city}`,
data: {
city: weatherData.city,
temperature: weatherData.temp,
condition: weatherData.condition,
humidity: weatherData.humidity,
},
statusCode: 200,
metadata: {
timestamp: Date.now(),
source: 'OpenWeather API',
},
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
message: 'Failed to fetch weather data',
statusCode: 500,
});
}
});
export default router;
Type Guards and Error Handling
The SDK provides type guards to safely distinguish between data and transaction responses:
import {
isDataResponse,
isTransactionResponse,
type DynamicActionDataResponse,
} from '@sherrylinks/sdk';
// In your application code
const response = await executeDynamic(action, values);
if (isDataResponse(response)) {
const dataResponse = response as DynamicActionDataResponse;
if (dataResponse.success) {
// Handle success
console.log('Data received:', dataResponse.data);
console.log('Message:', dataResponse.message);
} else {
// Handle error
console.error('Error:', dataResponse.error);
console.error('Status:', dataResponse.statusCode);
}
}
Error Handling Best Practices
- Always check the
success
field: This indicates whether the operation succeeded - Use appropriate HTTP status codes: Follow REST conventions (200, 400, 404, 500, etc.)
- Provide helpful error messages: Give users clear guidance on what went wrong
- Include context in error responses: Use the
data
field to provide debugging information - Handle network errors: The SDK sets
statusCode: 0
for network/client errors
Complete Examples
Example 1: Calculator
const calculatorMetadata = createMetadata({
title: 'Calculator',
description: 'Perform mathematical operations',
actions: [
{
type: 'dynamic',
label: 'Calculate',
chains: { source: 1 },
path: '/api/calculate',
responseType: 'data',
params: [
{
name: 'operation',
label: 'Operation',
type: 'select',
required: true,
options: [
{ label: 'Add', value: 'add' },
{ label: 'Subtract', value: 'subtract' },
{ label: 'Multiply', value: 'multiply' },
{ label: 'Divide', value: 'divide' },
],
},
{
name: 'a',
label: 'First Number',
type: 'number',
required: true,
},
{
name: 'b',
label: 'Second Number',
type: 'number',
required: true,
},
],
},
],
});
Backend implementation:
export async function POST(req: NextRequest) {
const { operation, a, b } = req.body.params;
if (!operation || a === undefined || b === undefined) {
return NextResponse.json(
{
success: false,
error: 'Missing required parameters: operation, a, b',
statusCode: 400,
},
{ status: 400 },
);
}
const numA = parseFloat(a);
const numB = parseFloat(b);
let result;
switch (operation) {
case 'add':
result = numA + numB;
break;
case 'subtract':
result = numA - numB;
break;
case 'multiply':
result = numA * numB;
break;
case 'divide':
if (numB === 0) {
return NextResponse.json(
{
success: false,
error: 'Cannot divide by zero',
statusCode: 400,
},
{ status: 400 },
);
}
result = numA / numB;
break;
default:
return NextResponse.json(
{
success: false,
error: `Invalid operation: ${operation}`,
statusCode: 400,
},
{ status: 400 },
);
}
return NextResponse.json({
success: true,
message: `${numA} ${operation} ${numB} = ${result}`,
data: { operation, a: numA, b: numB, result },
statusCode: 200,
});
}
Example 2: Portfolio Analytics
const portfolioMetadata = createMetadata({
title: 'Portfolio Analytics',
description: 'Get insights about your crypto portfolio',
actions: [
{
type: 'dynamic',
label: 'Analyze Portfolio',
chains: { source: 1 },
path: '/api/portfolio/analyze',
responseType: 'data',
params: [
{
name: 'timeframe',
label: 'Analysis Period',
type: 'select',
required: true,
options: [
{ label: 'Last 24 Hours', value: '24h' },
{ label: 'Last Week', value: '7d' },
{ label: 'Last Month', value: '30d' },
{ label: 'Last Year', value: '1y' },
],
},
],
},
],
});
Backend implementation:
export async function POST(req: NextRequest) {
const userAddress = req.headers.get('x-user-address');
const { timeframe } = req.body.params;
if (!userAddress) {
return NextResponse.json(
{
success: false,
error: 'User address is required',
statusCode: 400,
},
{ status: 400 },
);
}
// Fetch portfolio data from various chains
const portfolioData = await aggregatePortfolio(userAddress, timeframe);
// Calculate analytics
const analytics = {
totalValue: portfolioData.totalUSD,
change24h: portfolioData.change24h,
changePercent: portfolioData.changePercent,
topHolding: portfolioData.tokens[0],
diversification: calculateDiversification(portfolioData.tokens),
riskScore: calculateRiskScore(portfolioData),
recommendations: generateRecommendations(portfolioData),
};
return NextResponse.json({
success: true,
message: `Portfolio analysis for ${timeframe} period`,
data: analytics,
statusCode: 200,
metadata: {
timestamp: Date.now(),
chains: portfolioData.chainsAnalyzed,
tokenCount: portfolioData.tokens.length,
},
});
}
Example 3: External API Integration (Weather)
const weatherMetadata = createMetadata({
title: 'Weather Lookup',
description: 'Get current weather for any city',
actions: [
{
type: 'dynamic',
label: 'Get Weather',
chains: { source: 1 },
path: '/api/weather',
responseType: 'data',
params: [
{
name: 'city',
label: 'City Name',
type: 'string',
required: true,
placeholder: 'San Francisco',
},
],
},
],
});
Backend implementation:
export async function POST(req: NextRequest) {
const { city } = req.body.params;
if (!city) {
return NextResponse.json(
{
success: false,
error: 'City name is required',
statusCode: 400,
},
{ status: 400 },
);
}
try {
// Call external weather API
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.WEATHER_API_KEY}`,
);
if (!response.ok) {
return NextResponse.json(
{
success: false,
error: 'City not found',
message: `Could not find weather data for: ${city}`,
statusCode: 404,
},
{ status: 404 },
);
}
const weather = await response.json();
return NextResponse.json({
success: true,
message: `Weather data for ${weather.name}`,
data: {
city: weather.name,
country: weather.sys.country,
temperature: Math.round(weather.main.temp - 273.15), // Convert to Celsius
condition: weather.weather[0].main,
description: weather.weather[0].description,
humidity: weather.main.humidity,
windSpeed: weather.wind.speed,
},
statusCode: 200,
metadata: {
timestamp: Date.now(),
source: 'OpenWeather API',
},
});
} catch (error) {
return NextResponse.json(
{
success: false,
error: error.message,
message: 'Failed to fetch weather data',
statusCode: 500,
},
{ status: 500 },
);
}
}
Testing with Mock Server
For local development and testing, you can create a mock server. Here's a complete example:
// mock-server.js
import http from 'http';
const PORT = 3001;
function sendJSON(res, statusCode, data) {
res.writeHead(statusCode, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, x-user-address, x-source-chain',
});
res.end(JSON.stringify(data));
}
const handlers = {
'/api/user-info': (req, res, body) => {
const { userId, username } = body.params || body;
sendJSON(res, 200, {
success: true,
message: `User information for ${userId}`,
data: {
userId,
username,
email: `${username}@example.com`,
followers: 1250,
verified: true,
},
statusCode: 200,
});
},
'/api/calculate': (req, res, body) => {
const { operation, a, b } = body.params || body;
const numA = parseFloat(a);
const numB = parseFloat(b);
let result;
switch (operation) {
case 'add':
result = numA + numB;
break;
case 'subtract':
result = numA - numB;
break;
case 'multiply':
result = numA * numB;
break;
case 'divide':
if (numB === 0) {
return sendJSON(res, 400, {
success: false,
error: 'Cannot divide by zero',
statusCode: 400,
});
}
result = numA / numB;
break;
}
sendJSON(res, 200, {
success: true,
message: `${numA} ${operation} ${numB} = ${result}`,
data: { operation, a: numA, b: numB, result },
statusCode: 200,
});
},
};
const server = http.createServer((req, res) => {
if (req.method === 'OPTIONS') {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, x-user-address, x-source-chain',
});
res.end();
return;
}
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const parsedBody = JSON.parse(body);
const handler = handlers[req.url];
if (handler) {
handler(req, res, parsedBody);
} else {
sendJSON(res, 404, {
success: false,
error: 'Endpoint not found',
statusCode: 404,
});
}
} catch (error) {
sendJSON(res, 400, {
success: false,
error: 'Invalid JSON',
statusCode: 400,
});
}
});
}
});
server.listen(PORT, () => {
console.log(`Mock server running at http://localhost:${PORT}`);
});
Run with:
node mock-server.js
Best Practices
1. Response Design
✅ Do:
- Always include the
success
field - Provide clear, user-friendly messages
- Use appropriate HTTP status codes
- Include helpful error details
❌ Don't:
- Return transaction objects in data mode
- Omit error messages when
success: false
- Use generic error messages like "Error occurred"
2. Performance
- Keep response times under 2 seconds
- Cache expensive computations
- Use appropriate timeouts for external APIs
- Implement rate limiting on your backend
3. Security
- Validate and sanitize all inputs
- Never expose sensitive data in responses
- Use the
x-user-address
header for user context - Implement proper authentication when needed
4. Error Handling
- Handle all possible error scenarios
- Provide actionable error messages
- Include status codes for client-side handling
- Log errors for debugging
5. Testing
- Test both success and error paths
- Verify parameter validation
- Test with invalid/missing parameters
- Use mock servers for development
Comparison: Data vs Transaction Mode
Aspect | Data Mode | Transaction Mode |
---|---|---|
Response Type | JSON data | Blockchain transaction |
User Action | View results | Sign transaction |
Blockchain Interaction | None | Required |
Response Time | Fast (<2s typical) | Depends on chain |
Use Case | Data fetching, calculations | Token transfers, contract calls |
Error Handling | HTTP status codes + custom messages | Transaction failures |
Cost | Free (server costs only) | Gas fees |
Additional Resources
- Dynamic Actions (Transaction Mode) - For blockchain transactions
- Flow Actions - For multi-step workflows with both data and transactions
- Parameter Types - Complete parameter configuration reference
- Type Guards Reference - Response type discrimination utilities
Support
For questions or issues with data-mode Dynamic Actions:
- Check the examples directory for reference implementations
- Review the mock-server.js for testing patterns
- Open an issue on GitHub