HTTP APIs, webhooks, and indexing for SmartDrop. This repository contains Node.js services that talk to Horizon, Soroban RPC, and external APIs.
| Repository | Role |
|---|---|
| smart-frontend | Next.js static app |
| smartdrop-contracts | Soroban Rust contracts |
| SmartDrop | Original monorepo (reference) |
Multi-source price oracle that fetches and caches USD prices for Stellar assets.
Data Sources:
- Stellar DEX (orderbook prices)
- CoinGecko API
- CoinMarketCap API
Features:
- Median price aggregation from multiple sources
- Redis caching with configurable TTL (default: 60s)
- Background job refreshes prices every 30 seconds
- Stale price detection (>5 minutes)
- Price anomaly logging (>20% changes)
- Fallback chain: DEX → CoinGecko → CoinMarketCap → cached
Registers subscriber endpoints for SmartDrop lifecycle events and delivers signed JSON payloads with retry tracking.
Events:
airdrop.createdairdrop.executingairdrop.completedairdrop.failedrecipient.claimed
Features:
- Webhook endpoint CRUD with secrets kept out of list responses
- Timestamped HMAC-SHA256 request signatures
- At-least-once delivery attempts with exponential backoff
- Delivery logs with response code, error, duration, and attempt count
- Dead-letter storage after retry exhaustion
You can spin up the entire local development stack—including the API, PostgreSQL database, and Redis instance—using a single command.
- Ensure you have Docker and Docker Compose installed.
- Clone and Navigate to the project root directory.
- Set up Environment Variables:
cp .env.example .env
3. **Launch the Infrastructure**:
```bash
docker compose up --build
The API will stand up on http://localhost:4000.
- Hot Reloading: Any changes made to files within the
./srcdirectory will instantly trigger an application restart inside the container. - Database & Cache: Health checks prevent the API from booting until Postgres and Redis are fully operational.
- Teardown: To stop the containers and maintain volume data, run
docker compose down. To wipe database volumes completely during stop, usedocker compose down -v.
The application reads configurations from the .env file at the root.
Environment Variables:
| Variable | Description | Default | Required |
|---|---|---|---|
PORT |
Server port | 4000 | No |
REDIS_HOST |
Redis server host | redis | No |
REDIS_PORT |
Redis server port | 6379 | No |
REDIS_PASSWORD |
Redis password | undefined | No |
REDIS_URL |
Redis connection string | redis://redis:6379 | No |
DATABASE_URL |
PostgreSQL connection string | postgres://smartdrop:smartdrop@postgres:5432/smartdrop | No |
STELLAR_HORIZON_URL |
Horizon API URL | https://horizon.stellar.org | No |
USDC_ISSUER |
USDC issuer address | GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335AX2OBFLDTQLNUEHRGPTM6RIA | No |
COINGECKO_API_KEY |
CoinGecko API key | undefined | No |
COINMARKETCAP_API_KEY |
CoinMarketCap API key | undefined | No |
PRICE_CACHE_TTL |
Cache TTL in seconds | 60 | No |
PRICE_REFRESH_INTERVAL |
Refresh interval in seconds | 30 | No |
PRICE_STALE_THRESHOLD |
Stale threshold in minutes | 5 | No |
PRICE_ANOMALY_THRESHOLD |
Anomaly detection threshold % | 10 | No |
ADMIN_API_KEY |
Bootstrap admin bearer token for API key management | undefined | Yes, for protected endpoints |
LOG_LEVEL |
Logging level | info | No |
CORS_ALLOWED_ORIGINS |
Allowed origins split by commas | http://localhost:4000,http://localhost:3001 | No |
| ---------- | ------------- | --------- | ---------- |
NODE_ENV |
Runtime environment: development, test, or production |
development | No |
PORT |
Server port | 3000 | No |
REDIS_URL |
Redis connection URL | redis://localhost:6379 in development/test | Yes in production |
DATABASE_URL |
Database connection URL reserved for persistence-backed features | postgres://localhost/smartdrop in development, postgres://localhost/smartdrop_test in test | Yes in production |
STELLAR_HORIZON_URL |
Horizon API URL | https://horizon.stellar.org | No |
USDC_ISSUER |
USDC issuer address | GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335AX2OBFLDTQLNUEHRGPTM6RIA | No |
COINGECKO_API_KEY |
CoinGecko API key | empty | No |
COINMARKETCAP_API_KEY |
CoinMarketCap API key | empty | No |
PRICE_CACHE_TTL_SECONDS |
Cache TTL in seconds | 60 | No |
PRICE_REFRESH_INTERVAL_SECONDS |
Refresh interval in seconds | 30 | No |
PRICE_STALE_THRESHOLD_MINUTES |
Stale threshold in minutes | 5 | No |
PRICE_ANOMALY_THRESHOLD_PCT |
Anomaly detection threshold % | 20 | No |
ADMIN_API_KEY |
Bootstrap admin bearer token for API key management | empty | Yes, for protected endpoints |
LOG_LEVEL |
Logging level: debug, info, warn, or error |
info | No |
GET /api/v1/prices/:asset_code?issuer=<issuer_address>
Response:
{
"asset_code": "XLM",
"issuer": null,
"price_usd": 0.1234,
"source": "stellar_dex",
"fetched_at": "2024-01-15T10:30:00.000Z",
"is_stale": false,
"stale_warning": null,
"sources_attempted": ["stellar_dex", "coingecko"]
}
GET /api/v1/prices/:asset_code/refresh?issuer=<issuer_address>
Requires Authorization: Bearer <api_key>.
Protected endpoints use Authorization: Bearer <api_key>. Set ADMIN_API_KEY to a 32-byte hex token for bootstrap access, then create scoped API keys with the key-management endpoints.
GET /api/v1/keys
POST /api/v1/keys
DELETE /api/v1/keys/:id
POST /api/v1/keys returns the raw api_key only once. Stored keys are hashed with SHA-256 and listed with metadata only (label, created_at, last_used_at, scopes, and key_prefix).
POST /api/v1/webhooks
GET /api/v1/webhooks
DELETE /api/v1/webhooks/:id
POST /api/v1/webhooks/:id/test
GET /api/v1/webhooks/:id/deliveries
GET /health
Response:
{
"status": "ok",
"timestamp": "2024-01-15T10:30:00.000Z"
}
curl http://localhost:4000/api/v1/prices/XLM
curl "http://localhost:4000/api/v1/prices/USDC?issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335AX2OBFLDTQLNUEHRGPTM6RIA"
curl http://localhost:4000/api/v1/prices/XLM/refresh \
-H "Authorization: Bearer $API_KEY"
curl -X POST http://localhost:4000/api/v1/keys \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"alerts worker","scopes":["alerts"]}'
curl http://localhost:4000/health
The API returns appropriate HTTP status codes:
200- Success400- Invalid request parameters404- Price not available500- Internal server error
Error Response Format:
{
"error": "Error type",
"message": "Detailed error message"
}
src/
├── index.js # Express server entry point
├── config.js # Configuration management
├── logger.js # Winston logger setup
├── routes/
│ └── prices.js # Price API endpoints
├── services/
│ ├── cache.js # Redis cache wrapper
│ ├── priceOracle.js # Core oracle aggregation logic
│ └── sources/
│ ├── stellarDex.js # Stellar DEX price source
│ ├── coingecko.js # CoinGecko API source
│ └── coinmarketcap.js # CoinMarketCap API source
└── jobs/
└── priceRefresh.js # Background price refresh job
To add a new price source:
- Create a new file in
src/services/sources/ - Implement a
fetchPrice(assetCode, issuer)function that returns a price ornull - Add the source to the
SOURCESarray insrc/services/priceOracle.js
Example:
// src/services/sources/customSource.js
const axios = require('axios');
const logger = require('../../logger');
async function fetchPrice(assetCode, issuer) {
try {
const response = await axios.get('[https://api.example.com/price](https://api.example.com/price)', {
params: { asset: assetCode }
});
return response.data.price;
} catch (err) {
logger.warn('Custom source fetch failed', { assetCode, error: err.message });
return null;
}
}
module.exports = { fetchPrice };If you see "Redis connection error" in logs:
- Verify containers are running:
docker compose ps - Check Redis logs:
docker compose logs redis - Ensure environmental parameters (
REDIS_HOST=redis) reference the compose network alias rather thanlocalhost.
- Verify Redis is running:
redis-cli ping - Check
REDIS_URLin.env - If Redis requires a password, include it in the connection URL
If prices return null:
- Check that at least one price source is configured
- Verify API keys for CoinGecko/CoinMarketCap if using those sources
- Check logs for specific source errors
- Stellar DEX may have no liquidity for the asset
External APIs may rate limit requests:
- CoinGecko: Free tier has rate limits
- CoinMarketCap: Requires API key for production use
- The service handles rate limits gracefully and falls back to other sources
The service logs important events:
- Price fetches from each source
- Price anomalies (>10% changes)
- Stale price warnings
- Cache refresh cycles
- API errors
- Price fetches from each source
- Price anomalies (>20% changes)
- Stale price warnings
- Cache refresh cycles
- API errors
Monitor logs for:
- Frequent source failures
- Price anomalies (may indicate market volatility or data issues)
- Stale prices (may indicate cache or source issues)
MIT