The Merch MVP backend service is critical for managing secure claim verification, NFT metadata, and interaction with the Ethereum Attestation Service (EAS). All POST endpoints require a secure API key passed in the X-API-KEY header. Unauthorized requests (missing/invalid key) must return HTTP 401 with the payload: {"error": "Unauthorized"}.
- Image Management: Handle image uploads from Creators and store them in IPFS/Pinata
- Event Management: Generate and manage claim codes for events created by Users
- Claim Management: Securely store and verify single-use claim codes for events, managing both on-chain and off-chain claim status
- CRITICAL: Signature Generation: Generate the unique digital signature required by the public MerchManager.mintSBTWithAttestation function
- Off-Chain Reserve: Manage claims associated with non-crypto identifiers (email) for later on-chain redemption
- Metadata Hosting: Serve the JSON metadata files for both SBT and ERC-721 tokens via IPFS/Arweave
- EAS Attestation: Handle the non-user-facing gas payment and execution for issuing EAS attestations
| Endpoint | Method | Description | Input Parameters | Output Payload |
|---|---|---|---|---|
/api/createMerch |
POST | Creator endpoint. Uploads merch image to IPFS and stores URL in database. Requires X-API-KEY. | image (file), description (string) | merchId (string), imageURL (string), success (boolean) |
/api/createEvent |
POST | Event creation. Uploads event image and generates claim codes. Requires X-API-KEY. | eventImage (file), eventData (object), claimCodeCount (number) | eventId (string), claimCodes (array), imageURL (string) |
/verify-code |
POST | Verifies claim code. Returns Signature for public contract call. Requires X-API-KEY. | code (string), walletAddress (string) | eventId (string (hex-encoded bytes32)), tokenURI (string), signature (bytes: ECDSA signature), is_valid (boolean) |
/claim-offchain |
POST | Records a claim off-chain, reserving the Merch for later on-chain redemption. Used for gas-less or new users. Requires X-API-KEY. | code (string), userIdentifier (string: wallet or email), type (string: 'wallet' or 'email') | reservationId (string), message (string) |
/attest-claim |
POST | Critical EAS Endpoint. Issues the EAS Attestation after a successful on-chain transaction. Requires X-API-KEY. | txHash (string), walletAddress (string), eventId (string (hex-encoded bytes32)), isPremium (bool), imageURL (string) | attestationUID (string) |
/token-metadata/:id |
GET | Serves the JSON metadata for a specific Merch NFT ID. Does not require API key. | id (string) | Standard ERC-721 JSON metadata |
Purpose: Upload merch image to IPFS and store in database
Authentication: Requires X-API-KEY header
Request Body (multipart/form-data):
image: file (required)
description: string (optional)
Response (Success - 200):
{
"merchId": "uuid-string",
"imageURL": "https://ipfs.io/ipfs/QmHash...",
"success": true
}Response (Error - 400):
{
"error": "Invalid image file or missing required fields"
}Implementation Logic:
- Validate API key and image file
- Upload image to IPFS/Pinata
- Store image URL and metadata in database
- Return success with IPFS URL
Purpose: Create event with image and generate claim codes
Authentication: Requires X-API-KEY header
Request Body (multipart/form-data):
eventImage: file (required)
eventData: {
name: string,
description: string,
date: string,
location: string
}
claimCodeCount: number (required)
Response (Success - 200):
{
"eventId": "uuid-string",
"claimCodes": ["CODE123", "CODE456", "CODE789"],
"imageURL": "https://ipfs.io/ipfs/QmEventHash...",
"success": true
}Implementation Logic:
- Validate API key and event data
- Upload event image to IPFS/Pinata
- Generate specified number of claim codes
- Store event and claim codes in database
- Return event ID and claim codes
Purpose: Verify claim code and generate signature for public contract call
Authentication: Requires X-API-KEY header
Request Body:
{
"code": "string",
"walletAddress": "string"
}Response (Success - 200):
{
"eventId": "0x1234567890abcdef...",
"tokenURI": "https://ipfs.io/ipfs/QmHash...",
"signature": "0xabcdef1234567890...",
"is_valid": true
}Response (Error - 400):
{
"error": "Invalid claim code",
"is_valid": false
}Response (Error - 401):
{
"error": "Unauthorized"
}Implementation Logic:
- Validate API key from
X-API-KEYheader - Check if claim code exists and is unused
- Verify wallet address format
- Generate ECDSA signature using backend issuer private key
- Mark claim code as used
- Return signature and metadata for frontend contract call
Purpose: Reserve a claim off-chain for gasless users
Authentication: Requires X-API-KEY header
Request Body:
{
"code": "string",
"userIdentifier": "string",
"type": "wallet" | "email"
}Response (Success - 200):
{
"reservationId": "uuid-string",
"message": "Claim reserved successfully. Return to complete on-chain minting."
}Response (Error - 400):
{
"error": "Invalid claim code or user identifier"
}Implementation Logic:
- Validate API key and request parameters
- Check claim code availability
- Store reservation record in database
- Generate unique reservation ID
- Return confirmation message
Purpose: Issue EAS attestation after successful on-chain transaction
Authentication: Requires X-API-KEY header
Request Body:
{
"txHash": "string",
"walletAddress": "string",
"eventId": "string",
"isPremium": boolean
}Response (Success - 200):
{
"attestationUID": "0xabcdef1234567890..."
}Response (Error - 400):
{
"error": "Transaction verification failed"
}Implementation Logic:
- Verify transaction hash and receipt
- Extract SBT token ID from transaction logs
- Create attestation data object
- Call EAS contract to issue attestation
- Return attestation UID
Purpose: Serve NFT metadata for token display
Authentication: No API key required (public endpoint)
Parameters:
id: Token ID (string)
Response (Success - 200):
{
"name": "Merch Basic #123",
"description": "Proof of Attendance for ETH Ecuador Event",
"image": "https://ipfs.io/ipfs/QmImageHash...",
"attributes": [
{
"trait_type": "Event",
"value": "ETH Ecuador 2024"
},
{
"trait_type": "Type",
"value": "Basic"
}
]
}Response (Error - 404):
{
"error": "Token metadata not found"
}- Frontend Request: Mini-App calls
/verify-codewith user wallet address and claim code - Backend Verification: Backend validates the claim code against the database
- Signature Generation: If valid, the Backend Issuer Wallet signs a unique message that includes: eventId, walletAddress, and tokenURI
- Frontend Execution: The Frontend receives the signature and initiates a standard transaction calling the public BasicMerch.mintSBT(..., signature)
- Contract Execution: The contract verifies the signature against the trusted Issuer's address (using ecrecover). If valid, the SBT is minted. The user pays the gas for this public transaction
// Backend signature generation
function generateSignature(walletAddress: string, eventId: string, tokenURI: string): string {
// 1. Create message hash
const messageHash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['address', 'uint256', 'string'],
[walletAddress, eventId, tokenURI]
)
);
// 2. Sign with backend issuer private key
const signature = await backendIssuerWallet.signMessage(
ethers.utils.arrayify(messageHash)
);
return signature;
}CREATE TABLE merch_items (
id UUID PRIMARY KEY,
image_url TEXT NOT NULL,
description TEXT,
created_by VARCHAR(42) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);CREATE TABLE events (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
image_url TEXT NOT NULL,
date TIMESTAMP,
location VARCHAR(255),
created_by VARCHAR(42) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);CREATE TABLE claim_codes (
id SERIAL PRIMARY KEY,
code VARCHAR(255) UNIQUE NOT NULL,
event_id UUID NOT NULL,
token_uri TEXT NOT NULL,
is_used BOOLEAN DEFAULT FALSE,
used_by VARCHAR(42),
used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (event_id) REFERENCES events(id)
);CREATE TABLE offchain_reservations (
id UUID PRIMARY KEY,
claim_code VARCHAR(255) NOT NULL,
user_identifier VARCHAR(255) NOT NULL,
user_type VARCHAR(10) NOT NULL, -- 'wallet' or 'email'
created_at TIMESTAMP DEFAULT NOW(),
redeemed_at TIMESTAMP,
FOREIGN KEY (claim_code) REFERENCES claim_codes(code)
);CREATE TABLE eas_attestations (
id SERIAL PRIMARY KEY,
attestation_uid VARCHAR(66) UNIQUE NOT NULL,
wallet_address VARCHAR(42) NOT NULL,
event_id VARCHAR(66) NOT NULL,
sbt_token_id INTEGER NOT NULL,
is_premium BOOLEAN DEFAULT FALSE,
tx_hash VARCHAR(66) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/merch_db
# Blockchain
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
BACKEND_ISSUER_PRIVATE_KEY=0x...
EAS_CONTRACT_ADDRESS=0x...
EAS_SCHEMA_UID=0x...
# API Security
API_KEY_SECRET=your-secure-api-key
# IPFS/Pinata Storage
PINATA_API_KEY=your-pinata-api-key
PINATA_SECRET_KEY=your-pinata-secret-key
IPFS_GATEWAY_URL=https://ipfs.io/ipfs/
# Image Processing
MAX_IMAGE_SIZE=10485760 # 10MB
ALLOWED_IMAGE_TYPES=image/jpeg,image/png,image/gif,image/webp{
"error": "Error message",
"code": "ERROR_CODE",
"details": "Additional context"
}INVALID_API_KEY: Missing or invalid X-API-KEY headerINVALID_CLAIM_CODE: Claim code not found or already usedINVALID_WALLET_ADDRESS: Malformed wallet addressTRANSACTION_FAILED: On-chain transaction verification failedEAS_ATTESTATION_FAILED: EAS attestation creation failedMETADATA_NOT_FOUND: Token metadata not found
- General endpoints: 100 requests per minute per IP
- Verify-code: 10 requests per minute per wallet address
- Attest-claim: 5 requests per minute per wallet address
- API Key Rotation: Regular key updates for production
- Input Validation: Strict parameter validation
- SQL Injection Prevention: Parameterized queries
- CORS Configuration: Restricted to Base App domains
- Request Logging: Comprehensive audit trail
This API specification ensures secure, efficient backend operations for the Merch MVP while maintaining the signature-based security model and enabling seamless integration with the smart contracts and frontend.