Skip to main content

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 action
  • x-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

  1. Always check the success field: This indicates whether the operation succeeded
  2. Use appropriate HTTP status codes: Follow REST conventions (200, 400, 404, 500, etc.)
  3. Provide helpful error messages: Give users clear guidance on what went wrong
  4. Include context in error responses: Use the data field to provide debugging information
  5. 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

AspectData ModeTransaction Mode
Response TypeJSON dataBlockchain transaction
User ActionView resultsSign transaction
Blockchain InteractionNoneRequired
Response TimeFast (<2s typical)Depends on chain
Use CaseData fetching, calculationsToken transfers, contract calls
Error HandlingHTTP status codes + custom messagesTransaction failures
CostFree (server costs only)Gas fees

Additional Resources

Support

For questions or issues with data-mode Dynamic Actions:

  1. Check the examples directory for reference implementations
  2. Review the mock-server.js for testing patterns
  3. Open an issue on GitHub