Skip to main content
Glama
agilesix
by agilesix
usaspending.ts13.8 kB
/** * USASpending.gov API Client * * API Documentation: https://api.usaspending.gov/docs/endpoints * Base endpoint: https://api.usaspending.gov/api/v2 */ import type { Award, AwardSearchParams, AwardsSearchResponse, AwardDetailsResponse, ApiError, } from '../types/award'; import type { RecipientSearchParams, RecipientSearchResponse, RecipientDetailsResponse, RecipientAwardsParams, } from '../types/recipient'; /** * USASpending API Client configuration */ export interface USASpendingClientConfig { baseUrl?: string; timeout?: number; // milliseconds maxRetries?: number; retryDelay?: number; requestDelay?: number; } /** * USASpending API Client */ export class USASpendingClient { private baseUrl: string; private timeout: number; private maxRetries: number; private retryDelay: number; private requestDelay: number; private lastRequestTime: number = 0; constructor(config: USASpendingClientConfig = {}) { this.baseUrl = config.baseUrl || 'https://api.usaspending.gov/api/v2'; this.timeout = config.timeout || 90000; // 90 second default (API can be slow for complex queries) this.maxRetries = config.maxRetries ?? 2; // Reduce retries to 2 to avoid compounding timeouts this.retryDelay = config.retryDelay || 1000; // Default 1 second base delay this.requestDelay = config.requestDelay || 100; // Reduce to 100ms between requests } /** * Sleep for a specified number of milliseconds */ private async sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Throttle requests to avoid hitting rate limits */ private async throttleRequest(): Promise<void> { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.requestDelay) { await this.sleep(this.requestDelay - timeSinceLastRequest); } this.lastRequestTime = Date.now(); } /** * Fetch with retry logic for rate limiting */ private async fetchWithRetry(url: string, options: RequestInit): Promise<Response> { let lastError: Error | null = null; for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { // Throttle requests await this.throttleRequest(); console.log(`[USASpending] Attempting fetch to ${url} (attempt ${attempt + 1}/${this.maxRetries + 1})`); console.log(`[USASpending] Request options:`, JSON.stringify({ method: options.method, headers: options.headers, hasBody: !!options.body, bodyLength: options.body ? String(options.body).length : 0 })); const response = await fetch(url, options); console.log(`[USASpending] Response status: ${response.status} ${response.statusText}`); console.log(`[USASpending] Response headers:`, JSON.stringify(Object.fromEntries(response.headers.entries()))); // If we get a 429 (rate limit) error, retry with exponential backoff if (response.status === 429) { if (attempt < this.maxRetries) { const delay = this.retryDelay * Math.pow(2, attempt); console.log(`Rate limited (429). Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})...`); await this.sleep(delay); continue; } throw new Error(`USASpending API rate limit exceeded after ${this.maxRetries} retries. Please try again later.`); } // Return successful or non-429 error responses return response; } catch (error) { lastError = error as Error; // Log detailed error information console.error(`[USASpending] Fetch error on attempt ${attempt + 1}:`, { name: error instanceof Error ? error.name : 'Unknown', message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, url, attempt: attempt + 1, maxRetries: this.maxRetries }); // Don't retry on network errors or timeouts if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('timeout'))) { console.error(`[USASpending] Abort/timeout error, not retrying`); throw error; } // Retry on other errors if (attempt < this.maxRetries) { const delay = this.retryDelay * Math.pow(2, attempt); console.log(`Request failed. Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})...`); await this.sleep(delay); continue; } } } throw lastError || new Error('Request failed after all retry attempts'); } /** * Search for awards with the given parameters */ async searchAwards(params: Partial<AwardSearchParams>): Promise<AwardsSearchResponse> { const url = `${this.baseUrl}/search/spending_by_award/`; try { // NOTE: Removed AbortController to avoid potential SSL handshake interference // Cloudflare Workers have built-in timeouts that will handle this const response = await this.fetchWithRetry(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } console.error(`[USASpending] searchAwards API returned error status ${response.status}:`, { status: response.status, statusText: response.statusText, errorText: errorText.substring(0, 500), errorData: errorData, url: url, requestBody: JSON.stringify(params).substring(0, 500) }); throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json() as AwardsSearchResponse; console.log(`[USASpending] searchAwards successful, got ${data.results?.length || 0} results`); return data; } catch (error) { console.error('[USASpending] searchAwards caught error:', error); if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching awards'); } } /** * Get detailed information about a specific award */ async getAwardDetails(awardId: string): Promise<AwardDetailsResponse | null> { const url = `${this.baseUrl}/awards/${awardId}/`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await this.fetchWithRetry(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, signal: controller.signal, }); clearTimeout(timeoutId); if (response.status === 404) { return null; } if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json(); return data as AwardDetailsResponse; } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error('USASpending API request timed out'); } throw error; } throw new Error('Unknown error occurred while fetching award details'); } } /** * Search for recipients (autocomplete) */ async searchRecipients(params: RecipientSearchParams): Promise<RecipientSearchResponse> { const url = `${this.baseUrl}/autocomplete/recipient/`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await this.fetchWithRetry(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(params), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json(); return data as RecipientSearchResponse; } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error('USASpending API request timed out'); } throw error; } throw new Error('Unknown error occurred while searching recipients'); } } /** * Get detailed information about a specific recipient */ async getRecipientDetails(recipientHash: string): Promise<RecipientDetailsResponse | null> { const url = `${this.baseUrl}/recipient/${recipientHash}/`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await this.fetchWithRetry(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, signal: controller.signal, }); clearTimeout(timeoutId); if (response.status === 404) { return null; } if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json(); return data as RecipientDetailsResponse; } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error('USASpending API request timed out'); } throw error; } throw new Error('Unknown error occurred while fetching recipient details'); } } /** * Search for transactions by action date * Endpoint: /api/v2/search/spending_by_transaction/ */ async searchTransactions(params: Partial<AwardSearchParams>): Promise<AwardsSearchResponse> { const url = `${this.baseUrl}/search/spending_by_transaction/`; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } console.error(`[USASpending] searchTransactions API returned error status ${response.status}:`, { status: response.status, statusText: response.statusText, errorText: errorText.substring(0, 500), errorData: errorData, url: url, requestBody: JSON.stringify(params).substring(0, 500) }); throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json() as AwardsSearchResponse; console.log(`[USASpending] searchTransactions successful, got ${data.results?.length || 0} results`); return data; } catch (error) { console.error('[USASpending] searchTransactions caught error:', error); if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching transactions'); } } /** * Get awards over time for trend analysis * Endpoint: /api/v2/search/spending_over_time/ */ async getSpendingOverTime(params: any): Promise<any> { const url = `${this.baseUrl}/search/spending_over_time/`; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json(); return data; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching spending over time'); } } /** * Get IDV activity (child awards and task orders) * Endpoint: /api/v2/idvs/activity/ */ async getIdvActivity(params: any): Promise<any> { const url = `${this.baseUrl}/idvs/activity/`; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); if (!response.ok) { const errorText = await response.text(); let errorData: ApiError; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText || response.statusText, }; } throw new Error( `USASpending API error (${response.status}): ${errorData.detail}` ); } const data = await response.json(); return data; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching IDV activity'); } } } /** * Helper function to create a USASpendingClient instance */ export function createUSASpendingClient(config: USASpendingClientConfig = {}): USASpendingClient { return new USASpendingClient(config); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/agilesix/usaspending-mcp-nextjs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server