Version: 1.0.0
Date: December 14, 2025
Hackathon: Santa's Ho's HackNight @ Dedalus Labs Break In
Theme: Sleighs, Logistics, and Routing
Duration: 6 hours (4:45 PM – 9:00 PM)
ICBG is a web-based logistics platform that enables Santa's operations team to select any neighborhood on a globe, automatically identify real-world households within that region using OpenStreetMap data, intelligently assign gifts to each address, send festive email notifications to recipients, and generate a ready-to-fulfill delivery batch—all in a single action. The platform demonstrates scalable logistics infrastructure with live geographic data from the Overpass API and AI-powered product pairing capabilities, leveraging the Dedalus Labs SDK with MCP (Model Context Protocol) server integration via the AgentMail MCP server from the Dedalus Labs Marketplace for intelligent gift recommendations and automated delivery notifications.
One-Sentence Pitch:
"Click any neighborhood on a globe, query real addresses from OpenStreetMap, assign gifts at scale with AI, notify recipients via email, and generate a ready-to-fulfill delivery batch in one action."
┌─────────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js 16) │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ Mapbox Globe │ │ Control Panel │ │ Order Dashboard │ │
│ │ + Draw Tools │ │ + Area Select │ │ + Delivery Manifest │ │
│ └────────┬────────┘ └────────┬────────┘ └─────────────┬───────────────┘ │
│ │ │ │ │
│ └────────────────────┴─────────────────────────┘ │
│ │ │
│ React Query + Convex React Hooks │
└────────────────────────────────────┼────────────────────────────────────────┘
│ HTTP/REST + WebSocket (Convex)
┌────────────────────────────────────┼────────────────────────────────────────┐
│ BACKEND │
├────────────────────────────────────┼────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Next.js API Routes │ │
│ │ ┌─────────────────┐ ┌─────────────────────────┐ ┌───────────────┐ │ │
│ │ │ Address Service │ │ Gift Pairing Service │ │ Export Service│ │ │
│ │ │ (Overpass API + │ │ (Dedalus Labs SDK + │ │ (CSV from │ │ │
│ │ │ Turf.js) │ │ AgentMail MCP) │ │ Convex) │ │ │
│ │ └─────────────────┘ └─────────────────────────┘ └───────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Convex Backend (Real-time Database) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │ │
│ │ │ orderBatches │ │ orders │ │ addressSelections │ │ │
│ │ │ (batch mgmt) │ │ (order history) │ │ (selection tracking) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └───────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Dedalus Runner │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘
│ MCP Protocol
┌─────────────────┴─────────────────┐
│ AgentMail MCP Server │
│ (vroom08/agentmail-mcp) │
│ Dedalus Labs Marketplace │
└───────────────────────────────────┘
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Framework | Next.js | 16.0.10 | Full-stack React framework with App Router |
| Language | TypeScript | 5.7.x | Type-safe JavaScript |
| Runtime | Node.js | 22.x LTS | Server-side JavaScript runtime |
| Database | Convex | latest | Real-time backend database for persistent order history |
| Mapping | Mapbox GL JS | 3.17.0 | Interactive WebGL maps |
| React Mapping | react-map-gl | 8.1.0 | React wrapper for Mapbox GL JS |
| Draw Tools | @mapbox/mapbox-gl-draw | 1.5.1 | Polygon drawing on maps |
| Geospatial | @turf/turf | 7.3.1 | Geospatial analysis library |
| Address Data | Overpass API | - | OpenStreetMap query API for real-world building/address data |
| AI/MCP | dedalus-labs | 0.1.0-alpha.4 | MCP client SDK for AI agents |
| Styling | Tailwind CSS | 4.x | Utility-first CSS framework |
| State | React Query | 5.x | Server state management |
| ID Generation | nanoid | 5.x | Unique ID generation |
Component: <GlobeMap />
Functionality:
The globe interface serves as the primary interaction surface where Santa's operations team selects delivery regions. The implementation uses Mapbox GL JS with the globe projection for visual impact, combined with the Mapbox Draw plugin for polygon selection.
Technical Implementation:
// Map configuration for globe projection with draw capabilities
const mapConfig = {
projection: "globe",
style: "mapbox://styles/mapbox/dark-v11",
fog: {
color: "rgb(186, 210, 235)",
"high-color": "rgb(36, 92, 223)",
"horizon-blend": 0.02,
"space-color": "rgb(11, 11, 25)",
"star-intensity": 0.6
}
};User Interactions:
When a user clicks the "Select Area" button, the draw mode activates, enabling them to draw a polygon on the map by clicking points to define vertices. Double-clicking completes the polygon. The system then captures the GeoJSON representation of the selected area and passes it to the address identification service.
Constraints:
The system limits selections to a maximum of 50 addresses per query to ensure responsive performance and avoid overloading the Overpass API. Users can select any location worldwide—the OpenStreetMap database provides global coverage. For optimal results, selections should target residential areas with good OSM data coverage (major cities in North America, Europe, and Asia have excellent coverage).
Endpoint: POST /api/addresses/identify
Functionality:
Given a GeoJSON polygon representing a selected area, this service queries the OpenStreetMap Overpass API to identify real-world buildings and addresses within that region. The Overpass API is a read-only database interface optimized for extracting custom subsets of OSM data based on geographic bounds and tag filters.
Overpass API Integration:
The service constructs an Overpass QL (Query Language) query that retrieves:
- Buildings (
building=*) within the polygon bounds - Nodes with address information (
addr:housenumber,addr:street) - Ways (building outlines) with associated address tags
The public Overpass API endpoint (https://overpass-api.de/api/interpreter) is free to use and requires no authentication.
Request Schema:
type AddressIdentifyRequest = {
/** GeoJSON polygon representing the selected area */
polygon: GeoJSON.Polygon;
/** Maximum number of addresses to return (default: 50, max: 50) */
limit?: number;
};Response Schema:
type Address = {
/** Unique identifier derived from OSM node/way ID */
id: string;
/** Human-readable street address constructed from OSM tags */
streetAddress: string;
/** City name from addr:city tag or reverse geocoded */
city: string;
/** State/province from addr:state tag */
state: string;
/** Postal/ZIP code from addr:postcode tag */
postalCode: string;
/** Latitude coordinate (centroid for ways) */
lat: number;
/** Longitude coordinate (centroid for ways) */
lng: number;
/** Household metadata inferred from OSM building tags */
metadata?: {
/** Inferred from building type tags (apartments, house, residential) */
householdType?: "family" | "single" | "elderly";
/** Inferred from nearby amenities (playground, school proximity) */
hasChildren?: boolean;
/** Inferred from building:levels and building age tags */
estimatedAge?: "young" | "middle" | "senior";
};
/** Original OSM element type and ID for reference */
osmRef?: {
type: "node" | "way" | "relation";
id: number;
};
};
type AddressIdentifyResponse = {
/** Array of identified addresses */
addresses: Address[];
/** Total count of addresses found */
count: number;
/** Bounding box of the selection [minLng, minLat, maxLng, maxLat] */
boundingBox: [number, number, number, number];
/** Data source attribution (required by OSM license) */
attribution: string;
};Implementation Strategy:
The service converts the GeoJSON polygon to an Overpass QL polygon filter, queries the API, and transforms the OSM response into our address schema. Turf.js is used for computing bounding boxes and centroids.
import * as turf from "@turf/turf";
/** Overpass API public endpoint */
const OVERPASS_API_URL = "https://overpass-api.de/api/interpreter";
/**
* Converts a GeoJSON polygon to Overpass QL poly filter format.
* Overpass expects coordinates as "lat lon lat lon ..." space-separated string.
*
* @param polygon - GeoJSON polygon geometry
* @returns Overpass QL poly filter string
*/
function polygonToOverpassPoly(polygon: GeoJSON.Polygon): string {
const coords = polygon.coordinates[0];
return coords.map(([lng, lat]) => `${lat} ${lng}`).join(" ");
}
/**
* Builds an Overpass QL query to fetch buildings with addresses within a polygon.
* Queries for nodes and ways with addr:* tags or building tags.
*
* @param polygon - GeoJSON polygon defining the search area
* @param limit - Maximum number of results (applied client-side due to Overpass limitations)
* @returns Overpass QL query string
*/
function buildOverpassQuery(polygon: GeoJSON.Polygon): string {
const bbox = turf.bbox(polygon);
const [minLng, minLat, maxLng, maxLat] = bbox;
return `
[out:json]
[timeout:30]
[bbox:${minLat},${minLng},${maxLat},${maxLng}];
(
// Buildings with addresses
way["building"]["addr:housenumber"](poly:"${polygonToOverpassPoly(polygon)}");
// Address nodes
node["addr:housenumber"](poly:"${polygonToOverpassPoly(polygon)}");
// Residential buildings without explicit addresses (use centroid)
way["building"~"^(house|residential|apartments|detached|terrace)$"](poly:"${polygonToOverpassPoly(polygon)}");
);
out body geom;
`.trim();
}
/**
* Transforms an OSM element into our Address schema.
* Extracts address components from OSM tags and computes centroid for ways.
*
* @param element - Raw OSM element from Overpass response
* @returns Transformed Address object
*/
function osmElementToAddress(element: OverpassElement): Address {
const tags = element.tags ?? {};
// Compute centroid for ways (buildings)
let lat: number, lng: number;
if (element.type === "way" && element.geometry) {
const coords = element.geometry.map((g) => [g.lon, g.lat] as [number, number]);
const centroid = turf.centroid(turf.polygon([coords]));
[lng, lat] = centroid.geometry.coordinates;
} else {
lat = element.lat!;
lng = element.lon!;
}
// Construct street address from OSM tags
const houseNumber = tags["addr:housenumber"] ?? "";
const street = tags["addr:street"] ?? "";
const streetAddress =
houseNumber && street ? `${houseNumber} ${street}` : (tags["addr:full"] ?? `Building ${element.id}`);
// Infer household metadata from building tags
const buildingType = tags["building"];
const metadata = inferHouseholdMetadata(buildingType, tags);
return {
id: `osm_${element.type}_${element.id}`,
streetAddress,
city: tags["addr:city"] ?? "",
state: tags["addr:state"] ?? "",
postalCode: tags["addr:postcode"] ?? "",
lat,
lng,
metadata,
osmRef: { type: element.type, id: element.id }
};
}
/**
* Infers household metadata from OSM building and amenity tags.
* Uses heuristics based on building type, size, and nearby features.
*
* @param buildingType - OSM building tag value
* @param tags - Full OSM tags object
* @returns Inferred household metadata
*/
function inferHouseholdMetadata(buildingType: string | undefined, tags: Record<string, string>): Address["metadata"] {
const levels = parseInt(tags["building:levels"] ?? "1", 10);
// Apartments suggest mixed demographics
if (buildingType === "apartments" || levels > 3) {
return { householdType: "family", hasChildren: Math.random() > 0.5, estimatedAge: "middle" };
}
// Single-family homes
if (buildingType === "house" || buildingType === "detached") {
return { householdType: "family", hasChildren: true, estimatedAge: "middle" };
}
// Senior housing indicated by tags
if (tags["social_facility"] === "nursing_home" || tags["amenity"] === "retirement_home") {
return { householdType: "elderly", hasChildren: false, estimatedAge: "senior" };
}
// Default for residential buildings
return { householdType: "family", hasChildren: Math.random() > 0.4, estimatedAge: "middle" };
}
/**
* Queries the Overpass API to identify addresses within the given polygon.
* Fetches real OSM data and transforms it into our Address schema.
*
* @param polygon - GeoJSON polygon defining the selection area
* @param limit - Maximum number of addresses to return
* @returns Promise resolving to array of addresses within the polygon
*/
async function identifyAddressesInPolygon(polygon: GeoJSON.Polygon, limit: number = 50): Promise<Address[]> {
const query = buildOverpassQuery(polygon);
const response = await fetch(OVERPASS_API_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `data=${encodeURIComponent(query)}`
});
if (!response.ok) {
throw new Error(`Overpass API error: ${response.status} ${response.statusText}`);
}
const data: OverpassResponse = await response.json();
// Transform OSM elements to addresses and apply limit
const addresses = data.elements
.map(osmElementToAddress)
.filter((addr) => addr.streetAddress !== "")
.slice(0, limit);
return addresses;
}
/** Raw Overpass API response types */
type OverpassElement = {
type: "node" | "way" | "relation";
id: number;
lat?: number;
lon?: number;
tags?: Record<string, string>;
geometry?: Array<{ lat: number; lon: number }>;
};
type OverpassResponse = {
version: number;
generator: string;
osm3s: { timestamp_osm_base: string; copyright: string };
elements: OverpassElement[];
};OSM Data Attribution:
Per the OpenStreetMap License, all responses must include attribution. The API response includes:
attribution: "© OpenStreetMap contributors";Endpoint: POST /api/gifts/pair
Functionality:
This service represents the core MCP integration requirement for the hackathon. It uses the Dedalus Labs SDK to connect to the AgentMail MCP server from the Dedalus Labs Marketplace to send email notifications when gifts are assigned. The AI agent analyzes each address's characteristics, selects appropriate gifts from the mock product catalog, and sends festive email notifications to recipients.
MCP Server Configuration:
The system connects to the AgentMail MCP server (vroom08/agentmail-mcp) hosted on the Dedalus Labs Marketplace. This server provides email sending capabilities that enable the platform to notify recipients about incoming gift deliveries. This satisfies the hackathon requirement of using at least one MCP server from the Dedalus Marketplace.
Dedalus Integration Pattern:
import Dedalus from "dedalus-labs";
import { DedalusRunner } from "dedalus-labs";
/**
* Initializes the Dedalus client and runner for MCP-powered gift pairing
* and email notifications.
* The runner manages the agent loop and MCP server connections.
*/
const client = new Dedalus({
apiKey: process.env.DEDALUS_API_KEY
});
const runner = new DedalusRunner(client);
/**
* Custom tool for retrieving products from the mock catalog.
* This local tool is combined with the MCP server's capabilities.
*
* @param category - Product category to filter by
* @returns Array of products in the specified category
*/
function getProductsByCategory(category: string): Product[] {
return mockCatalog.filter((p) => p.category === category);
}
/**
* Pairs gifts with addresses using AI-powered recommendations and
* sends email notifications via the AgentMail MCP server.
* Combines local catalog tools with MCP server for intelligent pairing.
*
* @param addresses - Array of addresses requiring gift assignment
* @returns Promise resolving to gift pairings for each address
*/
async function pairGiftsWithAI(addresses: Address[]): Promise<GiftPairing[]> {
const result = await runner.run({
input: buildGiftPairingPrompt(addresses),
model: "openai/gpt-4o-mini",
tools: [getProductsByCategory],
mcpServers: ["vroom08/agentmail-mcp"]
});
return parseGiftPairings(result.finalOutput);
}Request Schema:
type GiftPairRequest = {
/** Array of addresses to pair with gifts */
addresses: Address[];
/** Optional strategy for gift assignment */
strategy?: "ai-recommended" | "round-robin" | "single-product";
/** Category constraint for gifts (optional) */
category?: string;
};Response Schema:
type Product = {
/** Amazon-style product identifier */
asin: string;
/** Product display name */
name: string;
/** Product description */
description: string;
/** Price in USD */
price: number;
/** Product category */
category: "toys" | "books" | "electronics" | "home" | "clothing";
/** Age appropriateness */
ageRange?: "children" | "teen" | "adult" | "all";
};
type GiftPairing = {
/** Reference to the address */
addressId: string;
/** Assigned product */
product: Product;
/** AI-generated reason for this pairing (when using AI strategy) */
pairingReason?: string;
};
type GiftPairResponse = {
/** Array of gift pairings */
pairings: GiftPairing[];
/** Total estimated cost of all gifts */
totalCost: number;
/** Strategy used for pairing */
strategyUsed: string;
};Mock Product Catalog:
The system includes a curated catalog of 20 products across categories, designed to enable meaningful AI-powered recommendations:
const mockCatalog: Product[] = [
{
asin: "B0X001",
name: "LEGO Holiday Train Set",
description: "Classic holiday train with 4 cars",
price: 49.99,
category: "toys",
ageRange: "children"
},
{
asin: "B0X002",
name: "Illustrated Christmas Stories",
description: "Collection of classic tales",
price: 19.99,
category: "books",
ageRange: "children"
},
{
asin: "B0X003",
name: "Smart Home Display",
description: "10-inch smart display",
price: 129.99,
category: "electronics",
ageRange: "adult"
},
{
asin: "B0X004",
name: "Cozy Holiday Throw Blanket",
description: "Soft fleece with festive patterns",
price: 34.99,
category: "home",
ageRange: "all"
}
// ... additional products
];Endpoint: POST /api/notifications/send
Functionality:
This service leverages the AgentMail MCP server from the Dedalus Labs Marketplace to send festive email notifications to gift recipients. When an order batch is confirmed, the system can optionally dispatch personalized "Santa's delivering your present!" emails to notify recipients of their incoming gifts.
Request Schema:
type SendNotificationsRequest = {
/** Array of notification recipients */
recipients: Array<{
/** Recipient email address */
email: string;
/** Recipient display name */
name: string;
/** Delivery address for personalization */
address: string;
}>;
/** Optional batch ID to associate notifications with orders */
batchId?: string;
/** Estimated delivery date for the notification */
estimatedDelivery?: string;
};Response Schema:
type SendNotificationsResponse = {
/** Number of emails successfully queued */
sent: number;
/** Number of emails that failed to send */
failed: number;
/** Detailed results for each recipient */
results: Array<{
email: string;
status: "sent" | "failed";
error?: string;
}>;
};Implementation:
import Dedalus from "dedalus-labs";
import { DedalusRunner } from "dedalus-labs";
const client = new Dedalus({
apiKey: process.env.DEDALUS_API_KEY
});
const runner = new DedalusRunner(client);
/**
* Sends festive delivery notification emails via AgentMail MCP server.
* Each recipient receives a personalized email announcing their gift delivery.
*
* @param recipients - Array of notification recipients with email and address info
* @param estimatedDelivery - Expected delivery date string
* @returns Promise resolving to send results for each recipient
*/
async function sendDeliveryNotifications(
recipients: Array<{ email: string; name: string; address: string }>,
estimatedDelivery: string = "December 23-24, 2025"
): Promise<SendNotificationsResponse> {
const prompt = `
You are Santa's notification coordinator. Send festive email notifications
to the following recipients announcing their gift deliveries.
For each recipient, use the AgentMail tools to send an email with:
- To: The recipient's email address
- Subject: "🎅 Santa's Delivering Your Present!"
- Body: A warm, HTML-formatted festive message including:
- A greeting with their name
- Announcement that a special gift is on the way
- Their delivery address
- Estimated delivery: ${estimatedDelivery}
- Signed "With holiday cheer, Santa's Workshop"
Recipients to notify:
${recipients
.map(
(r) => `
- Name: ${r.name}
- Email: ${r.email}
- Address: ${r.address}
`
)
.join("\n")}
After sending all emails, report back the status of each send operation
in JSON format:
{
"results": [
{ "email": "...", "status": "sent" },
{ "email": "...", "status": "failed", "error": "reason" }
]
}
`.trim();
const result = await runner.run({
input: prompt,
model: "openai/gpt-4o-mini",
mcpServers: ["vroom08/agentmail-mcp"]
});
return parseNotificationResults(result.finalOutput);
}Convex Functions: orderBatches.createBatch, orders.createOrdersBatch
Functionality:
This service generates order records that are persisted to the Convex database. Each order receives a unique identifier with a festive prefix, and the system tracks order status through a simple state machine. Orders are stored durably and can be queried in real-time from any connected client. The order history survives page refreshes and browser restarts.
Client-Side Usage (Convex Mutations):
Orders are created directly from the frontend using Convex mutations, enabling real-time UI updates without REST round-trips.
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { nanoid } from "nanoid";
/**
* Hook for creating orders with Convex persistence.
* Returns a function that creates a batch and all associated orders.
*/
function useCreateOrders() {
const createBatch = useMutation(api.orderBatches.createBatch);
const createOrdersBatch = useMutation(api.orders.createOrdersBatch);
return async (pairings: GiftPairing[], addresses: Address[]) => {
const batchId = `BATCH-${nanoid(10).toUpperCase()}`;
const totalCost = pairings.reduce((sum, p) => sum + p.product.price, 0);
// Create batch first
await createBatch({
batchId,
totalCost,
orderCount: pairings.length,
estimatedDeliveryStart: "2025-12-23",
estimatedDeliveryEnd: "2025-12-24"
});
// Create all orders in single mutation
const orders = pairings.map((pairing) => {
const address = addresses.find((a) => a.id === pairing.addressId)!;
return {
orderId: `HOHOHO-${nanoid(10).toUpperCase()}`,
batchId,
shippingAddress: JSON.stringify(address),
productAsin: pairing.product.asin,
productName: pairing.product.name,
productPrice: pairing.product.price,
shippingCost: 0,
pairingReason: pairing.pairingReason,
estimatedDeliveryStart: "2025-12-23",
estimatedDeliveryEnd: "2025-12-24"
};
});
await createOrdersBatch({ orders });
return batchId;
};
}TypeScript Types (derived from Convex schema):
/** Order as stored in Convex database */
type Order = {
/** Convex document ID */
_id: Id<"orders">;
/** Creation timestamp (auto-generated by Convex) */
_creationTime: number;
/** Unique order identifier with festive prefix */
orderId: string;
/** Parent batch identifier */
batchId: string;
/** Current order status */
status: "ORDER_CREATED" | "PENDING_FULFILLMENT" | "SHIPPED" | "DELIVERED";
/** Full shipping address (JSON stringified) */
shippingAddress: string;
/** Product ASIN */
productAsin: string;
/** Product name at order time */
productName: string;
/** Product price at order time */
productPrice: number;
/** Shipping cost */
shippingCost: number;
/** Total cost (product + shipping) */
totalCost: number;
/** AI-generated pairing reason */
pairingReason?: string;
/** Recipient email */
recipientEmail?: string;
/** Whether notification email was sent */
emailSent?: boolean;
/** Delivery window start (ISO string) */
estimatedDeliveryStart: string;
/** Delivery window end (ISO string) */
estimatedDeliveryEnd: string;
};
/** Order batch as stored in Convex database */
type OrderBatch = {
/** Convex document ID */
_id: Id<"orderBatches">;
/** Creation timestamp (auto-generated by Convex) */
_creationTime: number;
/** Unique batch identifier with BATCH- prefix */
batchId: string;
/** Current batch status */
status: "PENDING" | "CONFIRMED" | "EXPORTED" | "FULFILLED";
/** Total cost of all orders */
totalCost: number;
/** Number of orders in batch */
orderCount: number;
/** Selection polygon (JSON stringified GeoJSON) */
selectionPolygon?: string;
/** Delivery window start (ISO string) */
estimatedDeliveryStart: string;
/** Delivery window end (ISO string) */
estimatedDeliveryEnd: string;
/** Optional notes */
notes?: string;
};Order ID Generation:
Order IDs follow a festive pattern that maintains uniqueness while reinforcing the theme:
import { nanoid } from "nanoid";
/**
* Generates a festive order ID with the HOHOHO prefix.
* Uses nanoid for the unique suffix to ensure collision resistance.
*
* @returns A unique order identifier in the format HOHOHO-XXXXXXXXXX
*/
function generateOrderId(): string {
const suffix = nanoid(10).toUpperCase();
return `HOHOHO-${suffix}`;
}Endpoint: GET /api/orders/export
Functionality:
Generates downloadable delivery manifests in CSV format by reading order data from Convex. The manifest includes all order details, routing information, and delivery instructions. Since orders are persisted in Convex, exports can be generated at any time, even after browser sessions end.
Query Parameters:
type ExportParams = {
/** Batch ID to export */
batchId: string;
/** Export format */
format: "csv" | "json";
};Implementation (Server-Side Convex Query):
// app/api/orders/export/route.ts
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { NextRequest, NextResponse } from "next/server";
/**
* Exports order data from Convex as CSV or JSON.
* Reads persisted orders and formats for download.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const batchId = searchParams.get("batchId");
const format = searchParams.get("format") ?? "csv";
if (!batchId) {
return NextResponse.json({ error: "batchId required" }, { status: 400 });
}
// Fetch orders from Convex
const orders = await fetchQuery(api.orders.getOrdersByBatchId, { batchId });
if (format === "json") {
return NextResponse.json({ orders });
}
// Generate CSV
const csv = generateOrdersCSV(orders);
return new NextResponse(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="${batchId}-manifest.csv"`
}
});
}
function generateOrdersCSV(orders: Order[]): string {
const header =
"OrderID,Status,RecipientAddress,City,State,PostalCode,Latitude,Longitude,ProductName,ProductASIN,Price,EstimatedDelivery";
const rows = orders.map((order) => {
const addr = JSON.parse(order.shippingAddress);
return [
order.orderId,
order.status,
`"${addr.streetAddress}"`,
addr.city,
addr.state,
addr.postalCode,
addr.lat,
addr.lng,
`"${order.productName}"`,
order.productAsin,
order.productPrice.toFixed(2),
order.estimatedDeliveryEnd
].join(",");
});
return [header, ...rows].join("\n");
}CSV Schema:
OrderID,Status,RecipientAddress,City,State,PostalCode,Latitude,Longitude,ProductName,ProductASIN,Price,EstimatedDelivery
HOHOHO-ABC123,ORDER_CREATED,"123 Main St",San Francisco,CA,94102,37.7749,-122.4194,LEGO Holiday Train Set,B0X001,49.99,2025-12-24
| Method | Path | Description |
|---|---|---|
POST |
/api/addresses/identify |
Identify addresses within polygon |
POST |
/api/gifts/pair |
AI-powered gift pairing |
POST |
/api/notifications/send |
Send delivery notification emails (AgentMail MCP) |
GET |
/api/orders/export |
Export delivery manifest (reads from Convex) |
GET |
/api/catalog |
Retrieve product catalog |
Order creation and retrieval are handled directly through Convex functions for real-time reactivity:
| Type | Function | Description |
|---|---|---|
mutation |
orderBatches.createBatch |
Create a new order batch |
mutation |
orderBatches.updateBatchStatus |
Update batch fulfillment status |
query |
orderBatches.listBatches |
List all batches (real-time) |
query |
orderBatches.getBatchByBatchId |
Get batch by ID |
query |
orderBatches.getBatchesByStatus |
Filter batches by status |
mutation |
orders.createOrder |
Create a single order |
mutation |
orders.createOrdersBatch |
Batch create multiple orders |
mutation |
orders.updateOrderStatus |
Update order fulfillment status |
mutation |
orders.updateOrderEmailStatus |
Mark email as sent |
query |
orders.listOrders |
List all orders (real-time) |
query |
orders.getOrderByOrderId |
Get order by ID |
query |
orders.getOrdersByBatchId |
Get orders in a batch |
All API routes follow a consistent error response schema:
type APIError = {
/** Error code for programmatic handling */
code: string;
/** Human-readable error message */
message: string;
/** Additional error details (development only) */
details?: Record<string, unknown>;
};Standard error codes:
| Code | HTTP Status | Description |
|---|---|---|
INVALID_POLYGON |
400 | Provided polygon geometry is invalid |
LIMIT_EXCEEDED |
400 | Requested more than 50 addresses |
OVERPASS_TIMEOUT |
504 | Overpass API query timed out (area too large) |
OVERPASS_ERROR |
502 | Overpass API communication failure |
NO_ADDRESSES_FOUND |
404 | No addresses found in selected area (sparse OSM data) |
DEDALUS_ERROR |
502 | MCP server communication failure |
AGENTMAIL_ERROR |
502 | AgentMail MCP server email delivery failure |
INVALID_EMAIL |
400 | One or more recipient email addresses are invalid |
BATCH_NOT_FOUND |
404 | Order batch does not exist |
INTERNAL_ERROR |
500 | Unexpected server error |
<RootLayout>
├── <ConvexClientProvider> // Convex context for real-time data
│ ├── <Header>
│ │ ├── <Logo />
│ │ ├── <StatusIndicator /> (MCP + Convex connection status)
│ │ ├── <OrderHistoryLink /> // Link to /orders page
│ │ └── <ThemeToggle />
│ │
│ ├── <MainContent>
│ │ ├── <GlobeMap>
│ │ │ ├── <MapboxMap /> (react-map-gl)
│ │ │ ├── <DrawControl /> (@mapbox/mapbox-gl-draw)
│ │ │ ├── <SelectionOverlay />
│ │ │ └── <DeliveryRouteLines /> (animated)
│ │ │
│ │ └── <ControlPanel>
│ │ ├── <AreaSelectionControls>
│ │ │ ├── <DrawButton />
│ │ │ ├── <ClearButton />
│ │ │ └── <PolygonInfo />
│ │ │
│ │ ├── <AddressList>
│ │ │ ├── <AddressCard /> (repeated)
│ │ │ └── <AddressCount />
│ │ │
│ │ ├── <GiftPairingSection>
│ │ │ ├── <StrategySelector />
│ │ │ ├── <PairGiftsButton />
│ │ │ ├── <PairingProgress />
│ │ │ └── <EmailNotificationPreview />
│ │ │
│ │ ├── <NotificationStatus>
│ │ │ ├── <EmailSentCount />
│ │ │ ├── <EmailFailedCount />
│ │ │ └── <NotificationLog />
│ │ │
│ │ ├── <OrderSummary>
│ │ │ ├── <TotalCount />
│ │ │ ├── <TotalCost />
│ │ │ ├── <ConfirmOrderButton /> // useMutation to Convex
│ │ │ └── <ExportManifestButton />
│ │ │
│ │ └── <RecentOrders> // Real-time from Convex
│ │ ├── <BatchCard /> (repeated, useQuery)
│ │ └── <ViewAllLink />
│ │
│ └── <Footer>
│ └── <HackathonBadge />
│
├── <OrdersPage> // /orders route
│ ├── <OrderHistoryClient> // usePreloadedQuery for SSR
│ │ ├── <BatchCard /> (repeated)
│ │ │ ├── <BatchStatus />
│ │ │ ├── <OrderCount />
│ │ │ └── <TotalCost />
│ │ └── <BatchDetails>
│ │ ├── <OrderList>
│ │ │ └── <OrderCard /> (repeated)
│ │ └── <ExportBatchButton />
│ └── <EmptyState />
Order history and related data are persisted using Convex, a real-time backend database that provides automatic synchronization, TypeScript-first schemas, and seamless Next.js integration. Convex eliminates the need for manual API layer construction while providing built-in reactivity for live UI updates.
- Real-time by Default: Order status changes propagate instantly to all connected clients
- TypeScript-first: Schema validators generate type-safe queries and mutations
- Zero Infrastructure: Fully managed—no database provisioning or connection pooling
- Next.js Integration: First-class support for App Router, Server Components, and SSR
- Hackathon Speed: Setup takes minutes, not hours
Create the schema in convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
/**
* Convex schema definition for ICBG order persistence.
* Defines tables for order batches, individual orders, and address selections.
* All tables are automatically indexed by _id and _creationTime.
*/
export default defineSchema({
/**
* Order batches group multiple orders from a single area selection.
* Tracks batch-level status and aggregate cost information.
*/
orderBatches: defineTable({
/** Unique batch identifier with BATCH- prefix */
batchId: v.string(),
/** Current batch status in the fulfillment pipeline */
status: v.union(v.literal("PENDING"), v.literal("CONFIRMED"), v.literal("EXPORTED"), v.literal("FULFILLED")),
/** Total cost of all orders in the batch (USD) */
totalCost: v.number(),
/** Number of orders in this batch */
orderCount: v.number(),
/** GeoJSON polygon representing the selected area (stored as JSON string) */
selectionPolygon: v.optional(v.string()),
/** Estimated delivery window start date (ISO string) */
estimatedDeliveryStart: v.string(),
/** Estimated delivery window end date (ISO string) */
estimatedDeliveryEnd: v.string(),
/** Optional notes or metadata for the batch */
notes: v.optional(v.string())
})
.index("by_batchId", ["batchId"])
.index("by_status", ["status"]),
/**
* Individual orders represent a single gift delivery to one address.
* Contains full shipping, product, and cost information.
*/
orders: defineTable({
/** Unique order identifier with HOHOHO- prefix */
orderId: v.string(),
/** Reference to parent batch */
batchId: v.string(),
/** Current order status in fulfillment pipeline */
status: v.union(
v.literal("ORDER_CREATED"),
v.literal("PENDING_FULFILLMENT"),
v.literal("SHIPPED"),
v.literal("DELIVERED")
),
/** Full shipping address object (stored as JSON string for flexibility) */
shippingAddress: v.string(),
/** Product ASIN from the catalog */
productAsin: v.string(),
/** Product name at time of order (denormalized for history) */
productName: v.string(),
/** Product price at time of order (USD) */
productPrice: v.number(),
/** Shipping cost (USD, typically 0 for Santa's free delivery) */
shippingCost: v.number(),
/** Total order cost (product + shipping) */
totalCost: v.number(),
/** AI-generated reason for gift selection (optional) */
pairingReason: v.optional(v.string()),
/** Recipient email for notifications (optional) */
recipientEmail: v.optional(v.string()),
/** Whether email notification was sent successfully */
emailSent: v.optional(v.boolean()),
/** Estimated delivery start date (ISO string) */
estimatedDeliveryStart: v.string(),
/** Estimated delivery end date (ISO string) */
estimatedDeliveryEnd: v.string()
})
.index("by_orderId", ["orderId"])
.index("by_batchId", ["batchId"])
.index("by_status", ["status"]),
/**
* Address selections track polygon selections made on the globe.
* Useful for analytics and re-running deliveries to the same area.
*/
addressSelections: defineTable({
/** Unique selection identifier */
selectionId: v.string(),
/** GeoJSON polygon (stored as JSON string) */
polygon: v.string(),
/** Bounding box [minLng, minLat, maxLng, maxLat] as JSON string */
boundingBox: v.string(),
/** Number of addresses identified in this selection */
addressCount: v.number(),
/** Reference to batch created from this selection (optional) */
batchId: v.optional(v.string()),
/** Human-readable location description (e.g., "Castro District, SF") */
locationDescription: v.optional(v.string())
})
.index("by_selectionId", ["selectionId"])
.index("by_batchId", ["batchId"])
});import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
/**
* Creates a new order batch from a completed area selection and gift pairing.
* Called when user confirms orders after gift pairing completes.
*
* @param batchId - Unique batch identifier (BATCH-XXXXXXXXXX format)
* @param totalCost - Aggregate cost of all orders in the batch
* @param orderCount - Number of individual orders in the batch
* @param selectionPolygon - GeoJSON polygon as JSON string (optional)
* @param estimatedDeliveryStart - ISO date string for delivery window start
* @param estimatedDeliveryEnd - ISO date string for delivery window end
* @returns The Convex document ID of the created batch
*/
export const createBatch = mutation({
args: {
batchId: v.string(),
totalCost: v.number(),
orderCount: v.number(),
selectionPolygon: v.optional(v.string()),
estimatedDeliveryStart: v.string(),
estimatedDeliveryEnd: v.string(),
notes: v.optional(v.string())
},
handler: async (ctx, args) => {
const batchDocId = await ctx.db.insert("orderBatches", {
batchId: args.batchId,
status: "PENDING",
totalCost: args.totalCost,
orderCount: args.orderCount,
selectionPolygon: args.selectionPolygon,
estimatedDeliveryStart: args.estimatedDeliveryStart,
estimatedDeliveryEnd: args.estimatedDeliveryEnd,
notes: args.notes
});
return batchDocId;
}
});
/**
* Retrieves a batch by its public batchId (BATCH-XXXXXXXXXX).
*
* @param batchId - The public batch identifier
* @returns The batch document or null if not found
*/
export const getBatchByBatchId = query({
args: { batchId: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("orderBatches")
.withIndex("by_batchId", (q) => q.eq("batchId", args.batchId))
.unique();
}
});
/**
* Retrieves all batches, ordered by creation time (most recent first).
* Used for the order history dashboard.
*
* @returns Array of all batch documents
*/
export const listBatches = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("orderBatches").order("desc").collect();
}
});
/**
* Retrieves batches filtered by status.
* Useful for operational dashboards showing pending vs. fulfilled batches.
*
* @param status - Batch status to filter by
* @returns Array of batches matching the status
*/
export const getBatchesByStatus = query({
args: {
status: v.union(v.literal("PENDING"), v.literal("CONFIRMED"), v.literal("EXPORTED"), v.literal("FULFILLED"))
},
handler: async (ctx, args) => {
return await ctx.db
.query("orderBatches")
.withIndex("by_status", (q) => q.eq("status", args.status))
.order("desc")
.collect();
}
});
/**
* Updates a batch's status as it moves through the fulfillment pipeline.
*
* @param batchId - The public batch identifier
* @param status - New status to set
*/
export const updateBatchStatus = mutation({
args: {
batchId: v.string(),
status: v.union(v.literal("PENDING"), v.literal("CONFIRMED"), v.literal("EXPORTED"), v.literal("FULFILLED"))
},
handler: async (ctx, args) => {
const batch = await ctx.db
.query("orderBatches")
.withIndex("by_batchId", (q) => q.eq("batchId", args.batchId))
.unique();
if (!batch) {
throw new Error(`Batch not found: ${args.batchId}`);
}
await ctx.db.patch(batch._id, { status: args.status });
}
});import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
/**
* Creates a new individual order within a batch.
* Called for each address/product pairing when batch is confirmed.
*
* @param orderId - Unique order identifier (HOHOHO-XXXXXXXXXX format)
* @param batchId - Parent batch identifier
* @param shippingAddress - Full address object as JSON string
* @param productAsin - Product ASIN from catalog
* @param productName - Product name (denormalized)
* @param productPrice - Product price at order time
* @param pairingReason - AI-generated gift selection reason (optional)
* @param recipientEmail - Email for notifications (optional)
* @returns The Convex document ID of the created order
*/
export const createOrder = mutation({
args: {
orderId: v.string(),
batchId: v.string(),
shippingAddress: v.string(),
productAsin: v.string(),
productName: v.string(),
productPrice: v.number(),
shippingCost: v.number(),
pairingReason: v.optional(v.string()),
recipientEmail: v.optional(v.string()),
estimatedDeliveryStart: v.string(),
estimatedDeliveryEnd: v.string()
},
handler: async (ctx, args) => {
const orderDocId = await ctx.db.insert("orders", {
orderId: args.orderId,
batchId: args.batchId,
status: "ORDER_CREATED",
shippingAddress: args.shippingAddress,
productAsin: args.productAsin,
productName: args.productName,
productPrice: args.productPrice,
shippingCost: args.shippingCost,
totalCost: args.productPrice + args.shippingCost,
pairingReason: args.pairingReason,
recipientEmail: args.recipientEmail,
emailSent: false,
estimatedDeliveryStart: args.estimatedDeliveryStart,
estimatedDeliveryEnd: args.estimatedDeliveryEnd
});
return orderDocId;
}
});
/**
* Creates multiple orders in a single transaction (batch insert).
* More efficient than individual createOrder calls for large batches.
*
* @param orders - Array of order objects to create
* @returns Array of created order document IDs
*/
export const createOrdersBatch = mutation({
args: {
orders: v.array(
v.object({
orderId: v.string(),
batchId: v.string(),
shippingAddress: v.string(),
productAsin: v.string(),
productName: v.string(),
productPrice: v.number(),
shippingCost: v.number(),
pairingReason: v.optional(v.string()),
recipientEmail: v.optional(v.string()),
estimatedDeliveryStart: v.string(),
estimatedDeliveryEnd: v.string()
})
)
},
handler: async (ctx, args) => {
const orderDocIds: string[] = [];
for (const order of args.orders) {
const docId = await ctx.db.insert("orders", {
orderId: order.orderId,
batchId: order.batchId,
status: "ORDER_CREATED",
shippingAddress: order.shippingAddress,
productAsin: order.productAsin,
productName: order.productName,
productPrice: order.productPrice,
shippingCost: order.shippingCost,
totalCost: order.productPrice + order.shippingCost,
pairingReason: order.pairingReason,
recipientEmail: order.recipientEmail,
emailSent: false,
estimatedDeliveryStart: order.estimatedDeliveryStart,
estimatedDeliveryEnd: order.estimatedDeliveryEnd
});
orderDocIds.push(docId);
}
return orderDocIds;
}
});
/**
* Retrieves an order by its public orderId (HOHOHO-XXXXXXXXXX).
*
* @param orderId - The public order identifier
* @returns The order document or null if not found
*/
export const getOrderByOrderId = query({
args: { orderId: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("orders")
.withIndex("by_orderId", (q) => q.eq("orderId", args.orderId))
.unique();
}
});
/**
* Retrieves all orders belonging to a specific batch.
* Used for batch detail views and CSV export.
*
* @param batchId - The parent batch identifier
* @returns Array of orders in the batch
*/
export const getOrdersByBatchId = query({
args: { batchId: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("orders")
.withIndex("by_batchId", (q) => q.eq("batchId", args.batchId))
.collect();
}
});
/**
* Retrieves all orders, ordered by creation time (most recent first).
* Used for the complete order history view.
*
* @returns Array of all order documents
*/
export const listOrders = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("orders").order("desc").collect();
}
});
/**
* Updates an order's status as it moves through fulfillment.
*
* @param orderId - The public order identifier
* @param status - New status to set
*/
export const updateOrderStatus = mutation({
args: {
orderId: v.string(),
status: v.union(
v.literal("ORDER_CREATED"),
v.literal("PENDING_FULFILLMENT"),
v.literal("SHIPPED"),
v.literal("DELIVERED")
)
},
handler: async (ctx, args) => {
const order = await ctx.db
.query("orders")
.withIndex("by_orderId", (q) => q.eq("orderId", args.orderId))
.unique();
if (!order) {
throw new Error(`Order not found: ${args.orderId}`);
}
await ctx.db.patch(order._id, { status: args.status });
}
});
/**
* Marks an order as having sent email notification.
*
* @param orderId - The public order identifier
* @param emailSent - Whether email was sent successfully
*/
export const updateOrderEmailStatus = mutation({
args: {
orderId: v.string(),
emailSent: v.boolean()
},
handler: async (ctx, args) => {
const order = await ctx.db
.query("orders")
.withIndex("by_orderId", (q) => q.eq("orderId", args.orderId))
.unique();
if (!order) {
throw new Error(`Order not found: ${args.orderId}`);
}
await ctx.db.patch(order._id, { emailSent: args.emailSent });
}
});"use client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";
/**
* Convex client instance for the ICBG application.
* Connects to the Convex deployment specified by NEXT_PUBLIC_CONVEX_URL.
*/
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
/**
* Provider component that wraps the application with Convex context.
* Must wrap any components that use Convex hooks (useQuery, useMutation).
*
* @param children - Child components to render within the provider
* @returns JSX element with Convex context provider
*/
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
/**
* Order History Dashboard component demonstrating Convex integration.
* Displays all order batches with real-time updates.
*/
export function OrderHistory() {
// Real-time query - automatically updates when data changes
const batches = useQuery(api.orderBatches.listBatches);
if (batches === undefined) {
return <div className="animate-pulse">Loading order history...</div>;
}
return (
<div className="space-y-4">
<h2 className="text-xl font-bold">Order History</h2>
{batches.length === 0 ? (
<p className="text-gray-500">No orders yet. Select an area to begin!</p>
) : (
<ul className="space-y-2">
{batches.map((batch) => (
<li
key={batch._id}
className="p-4 border rounded-lg hover:bg-gray-50"
>
<div className="flex justify-between items-center">
<span className="font-mono text-sm">{batch.batchId}</span>
<span
className={`px-2 py-1 rounded text-xs ${
batch.status === "FULFILLED"
? "bg-green-100 text-green-800"
: batch.status === "CONFIRMED"
? "bg-blue-100 text-blue-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{batch.status}
</span>
</div>
<div className="mt-2 text-sm text-gray-600">
{batch.orderCount} orders · ${batch.totalCost.toFixed(2)} total
</div>
</li>
))}
</ul>
)}
</div>
);
}
/**
* Component demonstrating order creation with Convex mutation.
* Called when user confirms gift pairings.
*/
export function ConfirmOrdersButton({
pairings,
addresses,
onSuccess,
}: {
pairings: GiftPairing[];
addresses: Address[];
onSuccess: (batchId: string) => void;
}) {
const createBatch = useMutation(api.orderBatches.createBatch);
const createOrdersBatch = useMutation(api.orders.createOrdersBatch);
const handleConfirm = async () => {
const batchId = `BATCH-${nanoid(10).toUpperCase()}`;
const totalCost = pairings.reduce((sum, p) => sum + p.product.price, 0);
// Create the batch first
await createBatch({
batchId,
totalCost,
orderCount: pairings.length,
estimatedDeliveryStart: "2025-12-23",
estimatedDeliveryEnd: "2025-12-24",
});
// Create all orders in batch
const orders = pairings.map((pairing) => {
const address = addresses.find((a) => a.id === pairing.addressId)!;
return {
orderId: `HOHOHO-${nanoid(10).toUpperCase()}`,
batchId,
shippingAddress: JSON.stringify(address),
productAsin: pairing.product.asin,
productName: pairing.product.name,
productPrice: pairing.product.price,
shippingCost: 0,
pairingReason: pairing.pairingReason,
estimatedDeliveryStart: "2025-12-23",
estimatedDeliveryEnd: "2025-12-24",
};
});
await createOrdersBatch({ orders });
onSuccess(batchId);
};
return (
<button
onClick={handleConfirm}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
🎅 Confirm {pairings.length} Orders
</button>
);
}For server-rendered pages with Convex data, use preloadQuery:
// app/orders/page.tsx
import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { OrderHistoryClient } from "./OrderHistoryClient";
/**
* Server Component that preloads order batch data for SSR.
* Data is hydrated on the client and remains reactive.
*/
export default async function OrdersPage() {
const preloadedBatches = await preloadQuery(api.orderBatches.listBatches);
return (
<div className="container mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Santa's Delivery History</h1>
<OrderHistoryClient preloadedBatches={preloadedBatches} />
</div>
);
}// app/orders/OrderHistoryClient.tsx
"use client";
import { Preloaded, usePreloadedQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
/**
* Client Component that consumes preloaded batch data.
* Maintains real-time reactivity after initial SSR hydration.
*/
export function OrderHistoryClient({
preloadedBatches,
}: {
preloadedBatches: Preloaded<typeof api.orderBatches.listBatches>;
}) {
const batches = usePreloadedQuery(preloadedBatches);
return (
<div className="grid gap-4">
{batches.map((batch) => (
<BatchCard key={batch._id} batch={batch} />
))}
</div>
);
}# Mapbox Configuration
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.xxxx # Mapbox public token for client-side maps
# Convex Configuration
NEXT_PUBLIC_CONVEX_URL=https://xxx.convex.cloud # Convex deployment URL (from `npx convex dev`)
CONVEX_DEPLOY_KEY=prod:xxx # Convex deploy key (for production deployments)
# Dedalus Labs Configuration
DEDALUS_API_KEY=sk-xxxx # Dedalus API key for MCP server access
# Application Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3000 # Application base URL
NODE_ENV=development # Environment mode- Create a
.env.localfile in the project root - Copy the template above and fill in your credentials
- Obtain a Mapbox token from mapbox.com/account/access-tokens
- Initialize Convex and obtain deployment URL:
This command will prompt you to log in with GitHub, create a project, and automatically save your
npx convex dev
NEXT_PUBLIC_CONVEX_URLto.env.local. - Obtain a Dedalus API key from dedaluslabs.ai/dashboard
Objectives:
This first hour establishes the foundation. The team must resist feature creep and commit to the defined scope. The deliverable by end of hour is a running Next.js application with Mapbox displaying, Convex database connected, and Dedalus SDK initialized.
Tasks:
- Initialize Next.js 16 project with TypeScript and Tailwind CSS
- Install all dependencies (see package.json below)
- Initialize Convex and create database schema:
This will create the
npx convex dev
convex/folder and save your deployment URL to.env.local - Create Convex schema (
convex/schema.ts) with orderBatches, orders, and addressSelections tables - Set up
ConvexClientProviderand wrap the app layout - Configure remaining environment variables (Mapbox, Dedalus)
- Verify Mapbox token works with a basic map render
- Verify Dedalus SDK connects with a simple test query
- Create folder structure and placeholder components
Verification Checklist:
-
npm run devstarts both Next.js and Convex dev servers without errors - Convex dashboard shows connected deployment at dashboard.convex.dev
- Map renders at localhost:3000
- Console shows "Dedalus connected" message
- TypeScript compiles without errors
- Convex schema compiles and tables appear in dashboard
Objectives:
Build the interactive map with polygon drawing capabilities. By end of hour, users can draw a polygon on the map and the system captures the GeoJSON representation.
Tasks:
- Implement
<GlobeMap />component with globe projection - Add Mapbox Draw control for polygon selection
- Create draw mode toggle button
- Capture polygon GeoJSON on draw completion
- Display selection feedback (highlight selected area)
- Add basic styling for festive theme
Verification Checklist:
- Globe renders with dark theme
- Draw button activates polygon mode
- Polygon completes on double-click
- Console logs GeoJSON of selected polygon
Objectives:
Implement the OpenStreetMap Overpass API integration for real-world address identification. By end of hour, selecting any area on the globe returns a list of real addresses within that region.
Tasks:
- Implement Overpass QL query builder (polygon to OSM query)
- Create
/api/addresses/identifyendpoint with Overpass API integration - Build OSM-to-Address transformer (parse OSM tags into Address schema)
- Implement household metadata inference from building tags
- Connect frontend to address service with loading states
- Display address list in control panel with OSM attribution
- Add address markers to map (handle way centroids)
Verification Checklist:
- Overpass API query executes successfully
- API returns real OSM addresses within polygon
- Address list renders in sidebar with proper formatting
- Markers appear on map for identified addresses
- OSM attribution displays correctly
- Error handling works for empty/sparse areas
Objectives:
Implement the core MCP integration using Dedalus Labs SDK with the AgentMail MCP server from the Dedalus Marketplace. By end of hour, the system uses AI to recommend appropriate gifts for each identified address based on household metadata and sends festive email notifications to recipients.
Tasks:
- Create mock product catalog (20 products)
- Implement Dedalus runner with AgentMail MCP server connection (
vroom08/agentmail-mcp) - Create custom tool for catalog queries
- Build gift pairing prompt engineering with email notification instructions
- Implement
/api/gifts/pairendpoint - Implement
/api/notifications/sendendpoint - Display pairings in UI with AI reasoning
- Show email notification status/preview in UI
Verification Checklist:
- AgentMail MCP server connection established
- AI returns valid product recommendations
- Festive email notifications sent successfully
- Pairing reasons display in UI
- Different households get different recommendations
- Email send status displayed in UI
Objectives:
Create the order generation system with Convex persistence. By end of hour, users can confirm orders, see a complete order summary, and view order history from the database.
Tasks:
- Implement Convex mutations for batch and order creation (
convex/orderBatches.ts,convex/orders.ts) - Create order ID generation with HOHOHO prefix
- Build order summary component using
useMutationhook - Calculate totals and estimated delivery
- Add "Confirm Area Order" button that persists to Convex
- Build order history dashboard using
useQueryhook for real-time updates - Implement CSV export functionality that reads from Convex
Verification Checklist:
- Orders generate with HOHOHO prefix and persist to Convex
- Convex dashboard shows orders in the
orderstable - Summary shows total count and cost
- Order history updates in real-time without page refresh
- CSV downloads successfully from persisted data
- Order status displays correctly
Objectives:
Add visual polish and prepare for the 3-minute demo. Focus on animations and the "wow factor" for judges.
Tasks:
- Add animated delivery lines (depot → houses)
- Implement order counter animation
- Polish UI with festive styling
- Add loading states and transitions
- Test complete flow end-to-end
- Prepare demo script and talking points
Verification Checklist:
- Animated lines draw smoothly
- Counter increments dramatically
- No console errors
- Demo flow works in under 3 minutes
{
"name": "icbg",
"version": "1.0.0",
"description": "Intercontinental ballistic gift dispatch platform for Santa's logistics operations",
"private": true,
"scripts": {
"dev": "npm-run-all --parallel dev:next dev:convex",
"dev:next": "next dev",
"dev:convex": "convex dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
"convex:deploy": "convex deploy"
},
"dependencies": {
"next": "16.0.10",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"convex": "^1.17.0",
"dedalus-labs": "0.1.0-alpha.4",
"mapbox-gl": "3.17.0",
"react-map-gl": "8.1.0",
"@mapbox/mapbox-gl-draw": "1.5.1",
"@turf/turf": "7.3.1",
"@tanstack/react-query": "^5.62.0",
"nanoid": "^5.0.9",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"typescript": "^5.7.2",
"@types/node": "^22.10.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"@types/geojson": "^7946.0.15",
"tailwindcss": "^4.0.0",
"postcss": "^8.4.49",
"eslint": "^9.16.0",
"eslint-config-next": "16.0.10",
"npm-run-all": "^4.1.5"
},
"engines": {
"node": ">=22.0.0"
}
}The project uses the AgentMail MCP server (vroom08/agentmail-mcp) from the Dedalus Labs Marketplace as the primary MCP integration. This server provides email sending capabilities that enable the platform to send festive delivery notifications to gift recipients. The marketplace-hosted server is fully managed by Dedalus Labs, handling hosting, scaling, and availability automatically.
/**
* Configuration for Dedalus Labs SDK integration.
* This setup enables AI-powered gift pairing with email notifications
* via the AgentMail MCP server from the Dedalus Marketplace.
*/
import Dedalus from "dedalus-labs";
import { DedalusRunner } from "dedalus-labs";
/**
* Initializes the Dedalus client with API key from environment.
* The client handles authentication and request signing automatically.
*/
const client = new Dedalus({
apiKey: process.env.DEDALUS_API_KEY
});
/**
* Creates a DedalusRunner instance for orchestrating agent workflows.
* The runner manages tool execution, MCP server communication, and
* multi-turn conversations with the underlying LLM.
*/
const runner = new DedalusRunner(client);
/**
* Local tool function for querying the product catalog.
* Dedalus SDK automatically extracts the function signature and
* docstring to generate the tool schema for the LLM.
*
* @param category - Product category to filter ('toys', 'books', 'electronics', 'home', 'clothing')
* @param ageRange - Target age range ('children', 'teen', 'adult', 'all')
* @returns Array of products matching the criteria
*/
function searchProducts(category: string, ageRange?: string): Product[] {
return mockCatalog.filter((product) => {
const categoryMatch = product.category === category;
const ageMatch = !ageRange || product.ageRange === ageRange || product.ageRange === "all";
return categoryMatch && ageMatch;
});
}
/**
* Generates gift recommendations for a list of addresses using AI,
* then sends email notifications via the AgentMail MCP server.
* Combines local product search tools with MCP server email capabilities.
*
* @param addresses - Array of addresses with household metadata
* @param recipientEmails - Optional map of address IDs to recipient email addresses
* @returns Promise resolving to AI-generated gift pairings with notification status
*/
async function generateGiftRecommendations(
addresses: Address[],
recipientEmails?: Map<string, string>
): Promise<GiftPairing[]> {
const prompt = `
You are Santa's AI gift coordinator. For each household below, recommend
an appropriate gift from the available catalog. Consider the household type,
whether children are present, and the estimated age demographics.
After selecting gifts, send a festive email notification to each recipient
using the AgentMail email tools. The email should have:
- Subject: "🎅 Santa's Delivering Your Present!"
- Body: A warm, festive message announcing that a special gift is on its way,
mentioning the estimated delivery window of December 23-24, 2025.
Sign it "With holiday cheer, Santa's Workshop"
Households to process:
${addresses
.map(
(a) => `
- Address ID: ${a.id}
- Address: ${a.streetAddress}, ${a.city}, ${a.state} ${a.postalCode}
- Household Type: ${a.metadata?.householdType ?? "unknown"}
- Has Children: ${a.metadata?.hasChildren ?? "unknown"}
- Estimated Age: ${a.metadata?.estimatedAge ?? "unknown"}
${recipientEmails?.get(a.id) ? `- Email: ${recipientEmails.get(a.id)}` : ""}
`
)
.join("\n")}
Use the searchProducts tool to find appropriate gifts. For each household,
provide your recommendation in the following JSON format:
{
"addressId": "...",
"asin": "...",
"reason": "Brief explanation of why this gift fits this household",
"emailSent": true/false
}
`.trim();
const result = await runner.run({
input: prompt,
model: "openai/gpt-4o-mini",
tools: [searchProducts],
mcpServers: ["vroom08/agentmail-mcp"]
});
return parseRecommendations(result.finalOutput);
}The AgentMail MCP server (vroom08/agentmail-mcp) from the Dedalus Labs Marketplace provides email functionality:
- Email Sending: Send transactional emails with customizable subject, body, and recipient
- Rich Content: Support for HTML-formatted email bodies with festive styling
- Delivery Tracking: Confirmation of email dispatch status
- Managed Infrastructure: Fully hosted by Dedalus Labs—no SMTP configuration required
The system sends festive delivery notifications with the following structure:
/**
* Email notification template for gift delivery announcements.
* Sent via the AgentMail MCP server when gifts are paired.
*/
type DeliveryNotificationEmail = {
/** Recipient email address */
to: string;
/** Festive subject line with emoji */
subject: "🎅 Santa's Delivering Your Present!";
/** HTML-formatted email body */
body: string;
};
/**
* Generates the HTML body for a delivery notification email.
*
* @param recipientName - Name of the gift recipient
* @param address - Delivery address
* @param estimatedDelivery - Expected delivery date range
* @returns Formatted HTML email body
*/
function generateNotificationEmailBody(recipientName: string, address: string, estimatedDelivery: string): string {
return `
<div style="font-family: 'Georgia', serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #c41e3a; text-align: center;">🎄 Special Delivery Incoming! 🎄</h1>
<p>Dear ${recipientName},</p>
<p>Ho ho ho! Great news from the North Pole!</p>
<p>Santa's elves have prepared something special just for you, and it's being
loaded onto the sleigh as we speak. Your gift will be delivered to:</p>
<p style="background: #f5f5f5; padding: 15px; border-radius: 8px; text-align: center;">
<strong>${address}</strong>
</p>
<p>Expected arrival: <strong>${estimatedDelivery}</strong></p>
<p>Keep an eye on the sky and listen for sleigh bells!</p>
<p style="margin-top: 30px;">With holiday cheer,<br/>
<em>Santa's Workshop</em> 🎅</p>
</div>
`;
}If the MCP server is unavailable, the system falls back to rule-based gift assignment without email notifications:
/**
* Fallback gift pairing strategy when MCP server is unavailable.
* Uses simple rule-based logic based on household metadata.
* Email notifications are skipped in fallback mode.
*
* @param address - Address with household metadata
* @returns Product selected using rule-based logic
*/
function fallbackGiftPairing(address: Address): Product {
const { metadata } = address;
if (metadata?.hasChildren) {
return mockCatalog.find((p) => p.category === "toys" && p.ageRange === "children")!;
}
if (metadata?.estimatedAge === "senior") {
return mockCatalog.find((p) => p.category === "home" && p.ageRange === "all")!;
}
// Default: books are universally appreciated
return mockCatalog.find((p) => p.category === "books")!;
}"Imagine you're running Santa's logistics operations. Every December, you need to coordinate gift delivery to millions of households worldwide. Today, I'm showing you ICBG—a platform that lets you select any neighborhood on the globe, automatically identify every household, assign appropriate gifts using AI, and generate a ready-to-fulfill delivery batch in one click."
-
Globe Interaction (30 sec): "Here's our interactive globe. I can select any location worldwide—let me zoom into San Francisco and draw a selection around the Castro neighborhood." [Draw polygon on map]
-
Address Identification (20 sec): "Watch this—ICBG queries OpenStreetMap's Overpass API in real-time and identifies 47 actual households within our selection. These are real addresses from the world's largest open geographic database, not mock data." [Show address list with OSM attribution]
-
AI Gift Pairing + Email Notifications (40 sec): "Now the magic happens. Using the AgentMail MCP server from the Dedalus Labs Marketplace, our AI analyzes each household—inferring demographics from building types—and recommends appropriate gifts. When gifts are assigned, recipients automatically receive festive email notifications saying 'Santa's delivering your present!' Watch as families get matched with toys while seniors receive cozy home goods." [Trigger pairing, show progress, display email notification preview]
-
Order Generation + Persistence (30 sec): "With one click, I generate 47 orders totaling $1,847—all persisted to our Convex database in real-time. Each order has a unique ID, real shipping address, and estimated delivery window. Watch the order history dashboard update instantly without refreshing. I can export this as a delivery manifest for our fulfillment team, or review past batches anytime." [Click confirm, show summary updating in real-time, show order history, download CSV]
"What you've seen is fully functional with real data. The area selection works globally with live OpenStreetMap data—over 10 billion geographic features worldwide. Product pairing and email notifications are powered by the AgentMail MCP server from the Dedalus Labs Marketplace—recipients actually receive festive delivery announcements. All orders are persisted to a Convex database with real-time synchronization—refresh the page and your order history remains intact. The Amazon integration is mocked for demo speed, but the interface mirrors the actual fulfillment flow. ICBG—making Santa's operations as smooth as a sleigh on fresh snow."
What judges should believe after the demo:
- Real Data: Address identification uses live OpenStreetMap data—not mock data—demonstrating real-world applicability
- Global Scalability: Area selection works at any zoom level, any location on the globe with OSM coverage
- Automation: Address ingestion happens automatically through Overpass API geospatial queries
- MCP Integration: Email notifications via the AgentMail MCP server from the Dedalus Labs Marketplace demonstrate real, production-ready MCP integration
- User Engagement: Recipients receive actual festive "Santa's delivering your present!" email notifications
- Persistent Storage: Order history is stored in Convex with real-time sync—data survives page refreshes and is queryable
- Production-Path: Amazon ordering is pluggable—the mock system mirrors real API patterns
Explicit disclaimers to include:
- "Address data comes from OpenStreetMap—the world's largest open geographic database with over 10 billion features."
- "Email notifications are sent via the AgentMail MCP server hosted on the Dedalus Labs Marketplace—recipients receive real emails."
- "Order history is persisted to Convex, a real-time backend database—refresh the page and all orders remain."
- "The Amazon integration is mocked for hackathon speed; the interface mirrors the real fulfillment flow and could be connected to actual APIs."
icbg/
├── app/
│ ├── layout.tsx # Root layout with providers
│ ├── page.tsx # Main application page
│ ├── globals.css # Global styles including Tailwind
│ ├── ConvexClientProvider.tsx # Convex provider for client components
│ ├── orders/
│ │ ├── page.tsx # Order history page (Server Component)
│ │ └── OrderHistoryClient.tsx # Order history (Client Component)
│ └── api/
│ ├── addresses/
│ │ └── identify/
│ │ └── route.ts # Address identification endpoint
│ ├── gifts/
│ │ └── pair/
│ │ └── route.ts # Gift pairing endpoint (MCP)
│ ├── notifications/
│ │ └── send/
│ │ └── route.ts # Email notification endpoint (AgentMail MCP)
│ ├── orders/
│ │ └── export/
│ │ └── route.ts # CSV export endpoint (reads from Convex)
│ └── catalog/
│ └── route.ts # Product catalog endpoint
│
├── convex/
│ ├── _generated/ # Auto-generated Convex types and API
│ │ ├── api.d.ts # Type-safe API references
│ │ ├── api.js # API runtime
│ │ ├── dataModel.d.ts # Database type definitions
│ │ └── server.d.ts # Server function types
│ ├── schema.ts # Database schema definition
│ ├── orderBatches.ts # Batch queries and mutations
│ ├── orders.ts # Order queries and mutations
│ └── addressSelections.ts # Selection queries and mutations
│
├── components/
│ ├── GlobeMap/
│ │ ├── index.tsx # Main map component
│ │ ├── DrawControl.tsx # Mapbox Draw wrapper
│ │ ├── DeliveryLines.tsx # Animated route lines
│ │ └── AddressMarkers.tsx # Household markers
│ │
│ ├── ControlPanel/
│ │ ├── index.tsx # Control panel container
│ │ ├── AreaSelection.tsx # Draw controls
│ │ ├── AddressList.tsx # Address list display
│ │ ├── GiftPairing.tsx # Pairing controls
│ │ ├── NotificationStatus.tsx # Email notification status
│ │ └── OrderSummary.tsx # Order summary display
│ │
│ ├── OrderHistory/
│ │ ├── index.tsx # Order history dashboard
│ │ ├── BatchCard.tsx # Individual batch display
│ │ └── OrderDetails.tsx # Order detail view
│ │
│ └── ui/
│ ├── Button.tsx # Styled button component
│ ├── Card.tsx # Card container
│ ├── Badge.tsx # Status badges
│ └── Progress.tsx # Progress indicator
│
├── lib/
│ ├── dedalus.ts # Dedalus SDK initialization
│ ├── agentmail.ts # AgentMail MCP email notification service
│ ├── mapbox.ts # Mapbox configuration
│ ├── overpass.ts # Overpass API client and query builder
│ ├── types.ts # TypeScript type definitions
│ └── utils.ts # Utility functions
│
├── data/
│ └── catalog.json # Mock product catalog
│
├── .env.local # Environment variables (git-ignored)
├── .env.example # Environment template
├── next.config.ts # Next.js configuration
├── convex.json # Convex project configuration
├── tailwind.config.ts # Tailwind configuration
├── tsconfig.json # TypeScript configuration
├── package.json # Dependencies and scripts
└── README.md # Project documentation
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Overpass API timeout | Medium | High | Limit polygon size, implement 30s timeout, show user-friendly error |
| Overpass API rate limiting | Low | Medium | Use appropriate query limits, cache results, consider secondary endpoint |
| Sparse OSM data in selected area | Medium | Medium | Display helpful message, suggest alternative areas with better coverage |
| Mapbox rate limiting | Low | Medium | Use cached tiles, limit zoom interactions |
| Dedalus API unavailable | Low | High | Implement fallback rule-based pairing (without email) |
| AgentMail MCP unavailable | Low | Medium | Skip email notifications, proceed with gift pairing only |
| Email delivery failures | Low | Low | Log failures, display partial success in UI, allow retry |
| Browser WebGL issues | Low | Medium | Test in Chrome, have Firefox backup |
| State management complexity | Medium | Medium | Keep state flat, use Convex for persistence and React Query for API state |
| Convex connection issues | Low | Medium | Convex has built-in retry logic; show connection status indicator in UI |
| Convex schema migration | Low | Low | Schema is defined upfront; use npx convex dev to sync changes automatically |
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Scope creep | High | High | Strict scope lock in Hour 1 |
| Integration delays | Medium | High | Build components independently, integrate last |
| Demo environment issues | Low | High | Test on demo machine early |
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Network issues | Medium | High | Cache all API responses, work offline |
| Live demo failure | Low | High | Record backup video of working demo |
- ✓ Globe displays with polygon selection capability
- ✓ Selecting area returns list of real addresses (OpenStreetMap Overpass API)
- ✓ Gift pairing uses Dedalus MCP integration (hackathon requirement)
- ✓ Email notifications sent via AgentMail MCP server from Dedalus Marketplace
- ✓ Orders generate with unique IDs and persist to Convex database
- ✓ Order history dashboard displays real-time data from Convex
- ✓ Export functionality produces valid CSV from persisted data
- ✓ OSM attribution displayed per license requirements
- ○ Animated delivery route lines
- ○ Real-time order counter animation
- ○ Festive UI theme with snow effects
- ○ Multiple area selection support
- ○ Order status state machine with Convex mutations
- ○ Email delivery status tracking in UI (synced to Convex)
- ○ Server-rendered order history page with SSR preloading
- ✓ Complete flow in under 3 minutes
- ✓ No console errors during demo
- ✓ Clear explanation of MCP integration
- ✓ Professional presentation delivery
Address data is sourced in real-time from the OpenStreetMap Overpass API. Below is a sample of the transformed output:
{
"id": "osm_way_123456789",
"streetAddress": "456 Castro Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94114",
"lat": 37.7612,
"lng": -122.4348,
"metadata": {
"householdType": "family",
"hasChildren": true,
"estimatedAge": "middle"
},
"osmRef": {
"type": "way",
"id": 123456789
}
}Raw Overpass API Response (abbreviated):
{
"version": 0.6,
"generator": "Overpass API 0.7.62.4",
"osm3s": {
"timestamp_osm_base": "2025-12-14T20:00:00Z",
"copyright": "The data included in this document is from www.openstreetmap.org."
},
"elements": [
{
"type": "way",
"id": 123456789,
"tags": {
"building": "house",
"addr:housenumber": "456",
"addr:street": "Castro Street",
"addr:city": "San Francisco",
"addr:state": "CA",
"addr:postcode": "94114",
"building:levels": "2"
},
"geometry": [
{ "lat": 37.7611, "lon": -122.4349 },
{ "lat": 37.7611, "lon": -122.4347 },
{ "lat": 37.7613, "lon": -122.4347 },
{ "lat": 37.7613, "lon": -122.4349 },
{ "lat": 37.7611, "lon": -122.4349 }
]
}
]
}{
"asin": "B0X001",
"name": "LEGO Holiday Train Set",
"description": "Build and display this festive train with 4 decorated cars, engine, and holiday-themed accessories. Perfect for ages 8+.",
"price": 49.99,
"category": "toys",
"ageRange": "children"
}{
"orderId": "HOHOHO-A7B2C9D4E1",
"createdAt": "2025-12-14T22:30:00Z",
"status": "ORDER_CREATED",
"shippingAddress": {
"id": "addr_001",
"streetAddress": "123 Castro Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94114",
"lat": 37.7609,
"lng": -122.435
},
"product": {
"asin": "B0X001",
"name": "LEGO Holiday Train Set",
"price": 49.99
},
"estimatedDelivery": {
"earliest": "2025-12-23",
"latest": "2025-12-24"
},
"cost": {
"productPrice": 49.99,
"shipping": 0.0,
"total": 49.99
}
}Convex is a real-time backend platform that combines a document database with serverless functions. For ICBG, Convex provides:
- Persistent Order Storage: All orders and batches are durably stored and queryable
- Real-time Reactivity: UI updates automatically when data changes—no polling required
- Type Safety: Schema validators generate TypeScript types for compile-time safety
- Zero Configuration: No database setup, connection strings, or infrastructure management
npm install convex
npx convex dev # Creates project and starts syncConvex uses the v validator library for defining schemas:
import { v } from "convex/values";
// Primitive validators
v.string(); // String values
v.number(); // Numeric values (integers and floats)
v.boolean(); // true or false
v.null(); // Literal null
// Complex validators
v.array(v.string()); // Array of strings
v.object({ key: v.string() }); // Object with typed properties
v.optional(v.string()); // String or undefined
// Union validators (enum-like)
v.union(v.literal("PENDING"), v.literal("CONFIRMED"), v.literal("FULFILLED"));
// ID references
v.id("orders"); // Reference to document in "orders" tableQueries are read-only functions that can be subscribed to for real-time updates:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getBatch = query({
args: { batchId: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("orderBatches")
.withIndex("by_batchId", (q) => q.eq("batchId", args.batchId))
.unique();
}
});Mutations modify data and trigger reactive updates to subscribed queries:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const updateStatus = mutation({
args: { batchId: v.string(), status: v.string() },
handler: async (ctx, args) => {
const batch = await ctx.db
.query("orderBatches")
.withIndex("by_batchId", (q) => q.eq("batchId", args.batchId))
.unique();
if (batch) {
await ctx.db.patch(batch._id, { status: args.status });
}
}
});import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
// Subscribe to real-time data
const batches = useQuery(api.orderBatches.listBatches);
// Call mutations
const createBatch = useMutation(api.orderBatches.createBatch);
await createBatch({ batchId: "BATCH-123", totalCost: 499.99, ... });For SSR with App Router, use preloadQuery in Server Components:
// Server Component
import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
export default async function Page() {
const preloaded = await preloadQuery(api.orderBatches.listBatches);
return <ClientComponent preloaded={preloaded} />;
}// Client Component
"use client";
import { usePreloadedQuery } from "convex/react";
export function ClientComponent({ preloaded }) {
const batches = usePreloadedQuery(preloaded);
// Data is hydrated and remains reactive
}View and manage data at dashboard.convex.dev:
- Browse tables and documents
- Run ad-hoc queries
- View function logs
- Monitor deployment health
- Convex Next.js Quickstart
- App Router Integration
- Server Rendering
- Schema Definition
- Queries and Mutations
npm install dedalus-labsimport Dedalus from "dedalus-labs";
import { DedalusRunner } from "dedalus-labs";
// Initialize client
const client = new Dedalus({
apiKey: process.env.DEDALUS_API_KEY
});
// Create runner
const runner = new DedalusRunner(client);
// Execute with MCP servers from Dedalus Marketplace and local tools
const result = await runner.run({
input: "Your prompt here",
model: "openai/gpt-4o-mini",
tools: [localFunction1, localFunction2],
mcpServers: ["vroom08/agentmail-mcp"] // Marketplace server reference format
});
console.log(result.finalOutput);MCP servers from the Dedalus Labs Marketplace are referenced using the format username/server-name:
// Examples of marketplace MCP server references:
mcpServers: [
"vroom08/agentmail-mcp", // Email sending capabilities
"simon-liang/brave-search-mcp", // Web search
"windsor/ticketmaster-mcp" // Event/ticket lookup
];The AgentMail MCP server (vroom08/agentmail-mcp) provides email functionality:
- Marketplace URL: dedaluslabs.ai/marketplace/vroom08/agentmail-mcp
- Capabilities: Send transactional emails, HTML content support, delivery confirmation
- Use Case: Sending festive delivery notification emails to gift recipients
// Example: Sending email via AgentMail MCP
const result = await runner.run({
input: `Send an email to recipient@example.com with subject "🎅 Santa's Delivering Your Present!"
and a festive HTML body announcing their gift delivery.`,
model: "openai/gpt-4o-mini",
mcpServers: ["vroom08/agentmail-mcp"]
});The Dedalus platform supports multiple AI providers through a unified interface:
- OpenAI:
openai/gpt-4o-mini,openai/gpt-4.1,openai/gpt-5 - Anthropic:
anthropic/claude-opus-4.1,anthropic/claude-sonnet-4.5 - Google:
google/gemini-2.5-flash - Others: See docs.dedaluslabs.ai/sdk/guides/providers
For this hackathon, openai/gpt-4o-mini provides the best balance of speed and capability for gift pairing and email notification workflows.
The Overpass API is a read-only API that serves custom selected parts of OpenStreetMap data. It is optimized for data consumers that need elements selected by search criteria like location, type of objects, tag properties, and combinations thereof.
Key Characteristics:
- Free to use, no authentication required
- Supports complex geospatial queries via Overpass QL
- Returns data in JSON or XML format
- Queries execute server-side for efficiency
- Public endpoint:
https://overpass-api.de/api/interpreter
[out:json] // Output format
[timeout:30] // Query timeout in seconds
[bbox:south,west,north,east]; // Bounding box filter
(
// Query statements
way["building"]["addr:housenumber"](poly:"lat lon lat lon ...");
node["addr:housenumber"](poly:"lat lon lat lon ...");
);
out body geom; // Output with geometry
| Tag | Description | Example |
|---|---|---|
addr:housenumber |
House/building number | "123" |
addr:street |
Street name | "Main Street" |
addr:city |
City name | "San Francisco" |
addr:state |
State/province | "CA" |
addr:postcode |
Postal/ZIP code | "94102" |
addr:full |
Complete address string | "123 Main St, SF, CA" |
building |
Building type | "house", "apartments", "residential" |
building:levels |
Number of floors | "2" |
| Value | Typical Household | Gift Strategy |
|---|---|---|
house |
Single family | Family-oriented gifts |
detached |
Single family | Family-oriented gifts |
apartments |
Mixed demographics | Varied gifts |
residential |
General residential | Default strategy |
terrace |
Row houses, families | Family-oriented gifts |
- Query Timeout: Set
[timeout:30]to prevent long-running queries - Bounding Box: Always include
[bbox:...]to limit query scope - Result Limits: Process results client-side; Overpass doesn't support LIMIT
- Polygon Size: Keep selection polygons reasonably small (< 1 km²)
- Attribution: Display "© OpenStreetMap contributors" per license requirements
If the primary endpoint is unavailable:
https://overpass.kumi.systems/api/interpreterhttps://overpass.openstreetmap.ru/api/interpreter
[out:json][timeout:30];
(
way["building"~"^(house|residential|apartments|detached|terrace)$"]
(37.76,-122.44,37.77,-122.43);
way["building"]["addr:housenumber"]
(37.76,-122.44,37.77,-122.43);
);
out body geom;
This query retrieves all residential-type buildings and buildings with house numbers within a bounding box covering part of San Francisco.
Document prepared for Santa's Ho's HackNight @ Dedalus Labs Break In
Theme: Intercontinental Ballistic Gifts
December 14, 2025