-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Overview
Implement a daily limit of 10 AI usages per user for the "Fill with AI" feature. The system must use a cryptographic challenge-response mechanism to verify the user's identity and track usage accurately.
Requirements
- Limit "Fill with AI" usage to 10 requests per day per user
- Generate a UUIDv4 challenge when user opens the AI modal
- Require cryptographic signature of the challenge before processing AI request
- Verify signature authenticity and user existence on backend
- Reset usage counters daily at midnight UTC
- Cover the feature with tests
Backend Implementation
1. Database Changes
File: /backend/migrations/YYYYMMDDHHMMSS_add_ai_usage_tracking.js
exports.up = function(knex) {
return knex.schema
.createTable('ai_challenges', table => {
table.uuid('id').primary();
table.string('user_address', 42).notNullable();
table.timestamp('created_at').defaultTo(knex.fn.now());
table.boolean('used').defaultTo(false);
table.index(['user_address', 'used']);
})
.createTable('ai_usage', table => {
table.increments('id');
table.string('user_address', 42).notNullable();
table.date('usage_date').notNullable();
table.integer('count').defaultTo(1);
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.unique(['user_address', 'usage_date']);
});
};
exports.down = function(knex) {
return knex.schema
.dropTable('ai_challenges')
.dropTable('ai_usage');
};2. Backend Models
File: /backend/models/aiUsage.js
const knex = require('../db/knex');
const { v4: uuidv4 } = require('uuid');
const { verifySignature } = require('../utils/crypto');
class AIUsage {
/**
* Generate a new challenge for user authentication
* @param {string} userAddress - The blockchain address of the user
* @returns {Promise<string>} - The generated challenge UUID
*/
static async generateChallenge(userAddress) {
const challengeId = uuidv4();
await knex('ai_challenges').insert({
id: challengeId,
user_address: userAddress,
used: false
});
return challengeId;
}
/**
* Verify challenge signature and check daily usage limits
* @param {string} challengeId - The UUID challenge
* @param {string} signature - The cryptographic signature
* @param {string} userAddress - The user's blockchain address
* @returns {Promise<boolean>} - Whether the request is authorized
*/
static async verifyAndTrackUsage(challengeId, signature, userAddress) {
// Get challenge from database
const challenge = await knex('ai_challenges')
.where({ id: challengeId, used: false })
.first();
if (!challenge) {
throw new Error('Invalid or expired challenge');
}
// Verify the challenge was created for this user
if (challenge.user_address !== userAddress) {
throw new Error('Challenge does not match user');
}
// Verify signature
const isValidSignature = verifySignature(challengeId, signature, userAddress);
if (!isValidSignature) {
throw new Error('Invalid signature');
}
// Mark challenge as used
await knex('ai_challenges')
.where({ id: challengeId })
.update({ used: true });
// Check and update usage count
const today = new Date().toISOString().split('T')[0];
const currentUsage = await knex('ai_usage')
.where({
user_address: userAddress,
usage_date: today
})
.first();
if (currentUsage) {
// Check if limit reached
if (currentUsage.count >= 10) {
throw new Error('Daily AI usage limit reached');
}
// Increment usage count
await knex('ai_usage')
.where({
user_address: userAddress,
usage_date: today
})
.update({
count: currentUsage.count + 1,
updated_at: knex.fn.now()
});
} else {
// Create new usage record
await knex('ai_usage').insert({
user_address: userAddress,
usage_date: today,
count: 1
});
}
return true;
}
/**
* Get current usage count for a user
* @param {string} userAddress - The blockchain address of the user
* @returns {Promise<number>} - Current usage count
*/
static async getCurrentUsage(userAddress) {
const today = new Date().toISOString().split('T')[0];
const usage = await knex('ai_usage')
.where({
user_address: userAddress,
usage_date: today
})
.first();
return usage ? usage.count : 0;
}
}
module.exports = AIUsage;3. Utility Functions
File: /backend/utils/crypto.js
const ethers = require('ethers');
/**
* Verifies a signature against a message and address
* @param {string} message - The original message (challenge)
* @param {string} signature - The cryptographic signature
* @param {string} address - The blockchain address
* @returns {boolean} - Whether the signature is valid
*/
function verifySignature(message, signature, address) {
try {
// Create the Ethereum-specific signing message
const ethMessage = `\x19Ethereum Signed Message:\n${message.length}${message}`;
// Hash the message
const msgHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(ethMessage));
// Recover the signing address
const recoveredAddress = ethers.utils.recoverAddress(msgHash, signature);
// Check if the recovered address matches the expected address
return recoveredAddress.toLowerCase() === address.toLowerCase();
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
module.exports = {
verifySignature
};4. API Endpoints
File: /backend/routes/ai.js
const express = require('express');
const router = express.Router();
const AIUsage = require('../models/aiUsage');
const auth = require('../middleware/auth');
/**
* Generate a challenge for AI usage authentication
*/
router.post('/challenge', auth, async (req, res) => {
try {
const userAddress = req.user.address;
// Generate a new challenge
const challengeId = await AIUsage.generateChallenge(userAddress);
// Get current usage
const currentUsage = await AIUsage.getCurrentUsage(userAddress);
res.json({
success: true,
challengeId,
currentUsage,
dailyLimit: 10,
remainingUsage: Math.max(0, 10 - currentUsage)
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* Verify challenge and process AI request
*/
router.post('/process', auth, async (req, res) => {
try {
const { challengeId, signature, aiPrompt } = req.body;
const userAddress = req.user.address;
if (!challengeId || !signature || !aiPrompt) {
return res.status(400).json({
success: false,
error: 'Missing required parameters'
});
}
// Verify signature and track usage
await AIUsage.verifyAndTrackUsage(challengeId, signature, userAddress);
// Process AI request
// ... existing AI processing logic ...
res.json({
success: true,
// AI response data
});
} catch (error) {
res.status(error.message === 'Daily AI usage limit reached' ? 429 : 400).json({
success: false,
error: error.message
});
}
});
/**
* Get current AI usage stats
*/
router.get('/usage', auth, async (req, res) => {
try {
const userAddress = req.user.address;
const currentUsage = await AIUsage.getCurrentUsage(userAddress);
res.json({
success: true,
currentUsage,
dailyLimit: 10,
remainingUsage: Math.max(0, 10 - currentUsage)
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;Frontend Implementation
1. AI Service
File: /app/services/AIService.ts
import { ethers } from 'ethers';
import api from './api';
/**
* Service for handling AI operations with usage limits
*/
class AIService {
/**
* Request a new challenge for AI usage
* @returns Challenge data including UUID and usage statistics
*/
async requestChallenge(): Promise<{
challengeId: string;
currentUsage: number;
dailyLimit: number;
remainingUsage: number;
}> {
const response = await api.post('/api/ai/challenge');
return response.data;
}
/**
* Sign a challenge using the user's wallet
* @param challengeId - The UUID challenge to sign
* @returns The signature
*/
async signChallenge(challengeId: string): Promise<string> {
try {
if (!window.ethereum) {
throw new Error('No Ethereum wallet found');
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const signature = await signer.signMessage(challengeId);
return signature;
} catch (error) {
console.error('Error signing challenge:', error);
throw error;
}
}
/**
* Process an AI request with required authentication
* @param challengeId - The UUID challenge
* @param signature - The cryptographic signature
* @param aiPrompt - The AI prompt to process
* @returns The AI response
*/
async processAIRequest(
challengeId: string,
signature: string,
aiPrompt: string
): Promise<any> {
const response = await api.post('/api/ai/process', {
challengeId,
signature,
aiPrompt
});
return response.data;
}
/**
* Get current AI usage statistics
* @returns Usage statistics
*/
async getUsageStats(): Promise<{
currentUsage: number;
dailyLimit: number;
remainingUsage: number;
}> {
const response = await api.get('/api/ai/usage');
return response.data;
}
}
export default new AIService();2. AI Modal Component
File: /app/components/FillWithAIModal.tsx
import React, { useState, useEffect } from 'react';
import { Modal, Button, Alert, ProgressBar, Spinner } from 'react-bootstrap';
import AIService from '../services/AIService';
interface FillWithAIModalProps {
show: boolean;
onHide: () => void;
onSubmit: (generatedContent: string) => void;
}
export const FillWithAIModal: React.FC<FillWithAIModalProps> = ({
show,
onHide,
onSubmit
}) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [prompt, setPrompt] = useState('');
const [usage, setUsage] = useState({
currentUsage: 0,
dailyLimit: 10,
remainingUsage: 10
});
const [challengeId, setChallengeId] = useState<string | null>(null);
// Load challenge and usage stats when modal opens
useEffect(() => {
if (show) {
loadChallenge();
}
}, [show]);
const loadChallenge = async () => {
try {
setLoading(true);
setError(null);
const challengeData = await AIService.requestChallenge();
setChallengeId(challengeData.challengeId);
setUsage({
currentUsage: challengeData.currentUsage,
dailyLimit: challengeData.dailyLimit,
remainingUsage: challengeData.remainingUsage
});
} catch (error) {
setError('Failed to load AI challenge. Please try again.');
console.error('Challenge error:', error);
} finally {
setLoading(false);
}
};
const handleSubmit = async () => {
if (!prompt.trim()) {
setError('Please enter a prompt');
return;
}
if (!challengeId) {
setError('Authentication challenge not loaded');
return;
}
if (usage.remainingUsage <= 0) {
setError('Daily AI usage limit reached. Try again tomorrow.');
return;
}
try {
setLoading(true);
setError(null);
// Sign the challenge
const signature = await AIService.signChallenge(challengeId);
// Process the AI request
const result = await AIService.processAIRequest(
challengeId,
signature,
prompt
);
// Update usage stats
setUsage(prev => ({
...prev,
currentUsage: prev.currentUsage + 1,
remainingUsage: Math.max(0, prev.remainingUsage - 1)
}));
// Pass generated content to parent
onSubmit(result.generatedContent);
// Close modal
onHide();
} catch (error) {
if (error.response?.status === 429) {
setError('Daily AI usage limit reached. Try again tomorrow.');
} else if (error.message.includes('wallet')) {
setError('Wallet connection failed. Please make sure your wallet is connected.');
} else {
setError(`AI generation failed: ${error.response?.data?.error || error.message}`);
}
console.error('AI process error:', error);
} finally {
setLoading(false);
}
};
return (
<Modal show={show} onHide={onHide} centered>
<Modal.Header closeButton>
<Modal.Title>Fill with AI</Modal.Title>
</Modal.Header>
<Modal.Body>
{error && <Alert variant="danger">{error}</Alert>}
<div className="mb-3">
<label className="form-label">Daily Usage</label>
<ProgressBar
now={(usage.currentUsage / usage.dailyLimit) * 100}
variant={usage.remainingUsage > 2 ? 'success' : 'warning'}
/>
<small className="text-muted mt-1 d-block">
{usage.remainingUsage} of {usage.dailyLimit} AI generations remaining today
</small>
</div>
<div className="mb-3">
<label htmlFor="ai-prompt" className="form-label">Prompt</label>
<textarea
id="ai-prompt"
className="form-control"
rows={4}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
disabled={loading || usage.remainingUsage <= 0}
placeholder="Enter your prompt for AI content generation..."
/>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onHide} disabled={loading}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={loading || !prompt.trim() || usage.remainingUsage <= 0}
>
{loading ? (
<>
<Spinner animation="border" size="sm" className="me-2" />
Processing...
</>
) : (
'Generate'
)}
</Button>
</Modal.Footer>
</Modal>
);
};
export default FillWithAIModal;3. App.js Integration
File: /app/App.tsx or wherever the AI modal is used
// Import the updated modal
import FillWithAIModal from './components/FillWithAIModal';
// Example usage
const [showAIModal, setShowAIModal] = useState(false);
const handleAIGenerated = (content) => {
// Handle the generated content
console.log('AI generated content:', content);
// Update your form or content
};
// In your JSX:
<Button onClick={() => setShowAIModal(true)}>
Fill with AI
</Button>
<FillWithAIModal
show={showAIModal}
onHide={() => setShowAIModal(false)}
onSubmit={handleAIGenerated}
/>Testing Requirements
-
Unit Tests
- Test challenge generation
- Test signature verification
- Test usage limit enforcement
- Test daily reset functionality
-
Integration Tests
- Test full flow from challenge generation to AI response
- Test with different user addresses
- Test limit reached scenarios
-
Manual Testing
- Verify UI feedback when limit reached
- Test with actual wallet signatures
- Verify daily reset works properly
Deployment Considerations
-
Database Migration
- Run the migration on production/staging
- Verify table creation and indexes
-
Feature Flagging
- Consider implementing behind a feature flag for gradual rollout
- Monitor usage patterns after deployment
-
Admin Dashboard
- Consider adding admin ability to view/reset user limits
Success Criteria
- User can only use AI feature 10 times per day
- Challenge-based cryptographic verification works correctly
- Frontend provides clear feedback on remaining usage
- Daily usage resets properly at midnight UTC
- System correctly rejects requests beyond the limit
Timeline
- Database changes: 0.5 day
- Backend implementation: 1 day
- Frontend implementation: 1 day
- Testing: 0.5 day
Notes
- Consider caching challenge verification to improve performance
- Plan for scaling if AI feature usage grows significantly
- Consider implementing a premium tier with higher limits in the future
- Add monitoring on usage patterns to detect potential abuse