{"slug": "the-api-endpoint-explosion-what-if-ai-agents-could-use-sql-on-user-data-directly", "title": "The API Endpoint Explosion: What If AI Agents Could use SQL on User Data Directly?", "summary": "A developer argues that traditional REST APIs are becoming a maintenance nightmare for AI agents, proposing that agents should be able to query user data directly via SQL. The post illustrates the complexity of maintaining hundreds of hyper-specific endpoints and suggests a shift toward more flexible data access for AI systems.", "body_md": "Imagine you're architecting an MCP server for a financial platform.\n\nA customer connects their AI agent and asks:\n\n\"What's my current account balance?\"\n\nSimple.\n\nYou expose an API:\n\n```\nGET /accounts/{accountId}/balance\n```\n\nThen the customer asks:\n\n\"Show me my latest 20 transactions.\"\n\nAnother endpoint:\n\n```\nGET /accounts/{accountId}/transactions\n```\n\nThen:\n\n\"Which credit cards do I have?\"\n\nAnother endpoint:\n\n```\nGET /customers/{customerId}/cards\n```\n\nThen:\n\n\"Show me every document generated for my accounts during the last six months.\"\n\nYet another endpoint.\n\nThen:\n\n\"Show me transactions over $500, made using cards expiring this year, together with the account they belong to and any related documents.\"\n\nNow the design starts falling apart under real-world complexity.\n\nYou end up designing bespoke endpoints, forcing the client to stitch together response payloads, or attempting to predict every relational query upfront.\n\nTomorrow, the query requirement shifts again.\n\nThis is how traditional enterprise APIs scale into maintenance nightmares.\n\nYou start with a clean core of REST operations.\n\nBefore long, you're maintaining hundreds—sometimes thousands—of hyper-specific endpoints.\n\nSomething like:\n\n```\nGET /accounts\nGET /accounts/{id}\nGET /accounts/{id}/balance\nGET /accounts/{id}/transactions\nGET /accounts/{id}/documents\n\nGET /cards\nGET /cards/{id}\nGET /cards/{id}/transactions\n\nGET /payments\nGET /payments/{id}\n\nGET /transfers\nGET /statements\n...\n```\n\nBut an endpoint isn't just one line.\n\nHere is what that looks like in a typical OpenAPI specification for a single endpoint:\n\n```\n/accounts/{accountId}/transactions:\n  get:\n    operationId: getAccountTransactions\n    summary: Get transactions for an account\n\n    parameters:\n      - name: accountId\n        in: path\n        required: true\n        schema:\n          type: string\n\n      - name: from\n        in: query\n        schema:\n          type: string\n          format: date-time\n\n      - name: to\n        in: query\n        schema:\n          type: string\n          format: date-time\n\n      - name: limit\n        in: query\n        schema:\n          type: integer\n          default: 50\n\n    responses:\n      \"200\":\n        description: \"List of transactions\"\n        content:\n          application/json:\n            schema:\n              type: array\n              items:\n                $ref: \"#/components/schemas/Transaction\"\n\n      \"400\":\n        description: \"Invalid request\"\n\n      \"401\":\n        description: \"Authentication required\"\n\n      \"403\":\n        description: \"Account access denied\"\n\n      \"404\":\n        description: \"Account not found\"\n\n      \"500\":\n        description: \"Internal server error\"\n/accounts/{accountId}/transactions:\n  get:\n    operationId: getAccountTransactions\n    summary: Get transactions for an account\n\n    parameters:\n      - name: accountId\n        in: path\n        required: true\n        schema:\n          type: string\n\n      - name: from\n        in: query\n        schema:\n          type: string\n          format: date-time\n\n      - name: to\n        in: query\n        schema:\n          type: string\n          format: date-time\n\n      - name: limit\n        in: query\n        schema:\n          type: integer\n          default: 50\n\n    responses:\n      \"200\":\n        description: \"List of transactions\"\n        content:\n          application/json:\n            schema:\n              type: array\n              items:\n                $ref: \"#/components/schemas/Transaction\"\n\n      \"400\":\n        description: \"Invalid request\"\n\n      \"401\":\n        description: \"Authentication required\"\n\n      \"403\":\n        description: \"Account access denied\"\n\n      \"404\":\n        description: \"Account not found\"\n\n      \"500\":\n        description: \"Internal server error\"\n```\n\nAnd somewhere else:\n\n```\nTransaction:\n  type: object\n  properties:\n    id:\n      type: string\n\n    accountId:\n      type: string\n\n    amount:\n      type: number\n      format: decimal\n\n    currency:\n      type: string\n\n    merchant:\n      type: string\n\n    category:\n      type: string\n\n    createdAt:\n      type: string\n      format: date-time\nTransaction:\n  type: object\n  properties:\n    id:\n      type: string\n\n    accountId:\n      type: string\n\n    amount:\n      type: number\n      format: decimal\n\n    currency:\n      type: string\n\n    merchant:\n      type: string\n\n    category:\n      type: string\n\n    createdAt:\n      type: string\n      format: date-time\n```\n\nMultiply this footprint by hundreds of operations. Every single endpoint carries boilerplate overhead: parameter parsing, validation, authentication checks, schema definitions, custom pagination, unit tests, and SDK updates.\n\n```\n200 endpoints\n500 endpoints\n1,000 endpoints\n```\n\nWhen AI agents consume these APIs, we end up attempting to pass down an entire static domain model wrapped inside API specifications.\n\nWe are effectively describing every action that developers predicted the user might want.\n\nInstead of exposing hundreds of endpoints, what if your interface collapses into two primary capabilities?\n\n```\nget_schema()\nquery(sql)\n```\n\n`get_schema()`\n\ndoesn't return only table and column names.\n\n`get_schema()`\n\ngoes beyond column names. It provides structural metadata, keys, indexes, and descriptions so the LLM can reason about relationships directly:\n\n```\n{\n  \"table\": \"transactions\",\n  \"description\": \"Financial transactions belonging to the authenticated user.\",\n\n  \"columns\": [\n    {\n      \"name\": \"id\",\n      \"type\": \"UUID\",\n      \"nullable\": false,\n      \"description\": \"Unique transaction identifier.\"\n    },\n    {\n      \"name\": \"account_id\",\n      \"type\": \"UUID\",\n      \"nullable\": false,\n      \"description\": \"Account on which the transaction occurred.\"\n    },\n    {\n      \"name\": \"card_id\",\n      \"type\": \"UUID\",\n      \"nullable\": true,\n      \"description\": \"Card used for the transaction, when applicable.\"\n    },\n    {\n      \"name\": \"amount\",\n      \"type\": \"DECIMAL(18,2)\",\n      \"nullable\": false,\n      \"description\": \"Transaction amount in the transaction currency.\"\n    },\n    {\n      \"name\": \"merchant\",\n      \"type\": \"VARCHAR\",\n      \"nullable\": true,\n      \"description\": \"Merchant display name.\"\n    },\n    {\n      \"name\": \"category\",\n      \"type\": \"ENUM('food', 'transport', 'utilities', 'entertainment', 'shopping')\",\n      \"nullable\": true,\n      \"description\": \"Normalized transaction category.\"\n    },\n    {\n      \"name\": \"created_at\",\n      \"type\": \"TIMESTAMP\",\n      \"nullable\": false,\n      \"description\": \"Time at which the transaction was recorded.\"\n    }\n  ],\n\n  \"primary_key\": [\n    \"id\"\n  ],\n\n  \"foreign_keys\": [\n    {\n      \"column\": \"account_id\",\n      \"references\": \"accounts.id\"\n    },\n    {\n      \"column\": \"card_id\",\n      \"references\": \"cards.id\"\n    }\n  ],\n\n  \"indexes\": [\n    {\n      \"name\": \"idx_transactions_account_created\",\n      \"columns\": [\n        \"account_id\",\n        \"created_at\"\n      ]\n    },\n    {\n      \"name\": \"idx_transactions_card\",\n      \"columns\": [\n        \"card_id\"\n      ]\n    }\n  ]\n}\n```\n\nWith structured metadata across entities (`accounts`\n\n, `transactions`\n\n, `cards`\n\n, `documents`\n\n), the LLM gets exact context on types, foreign keys, and indexes.\n\nThis gives the model all the context it needs to construct accurate, performant queries.\n\nNow the customer asks: \"Show me my five largest restaurant transactions this month.\"\n\nThe agent generates:\n\n```\nSELECT\n    merchant,\n    amount,\n    created_at\nFROM transactions\nWHERE category = 'restaurant'\n  AND created_at >= DATE_TRUNC('month', CURRENT_DATE)\nORDER BY amount DESC\nLIMIT 5;\n```\n\nNobody had to create:\n\n```\nGET /transactions/largest-restaurants-this-month\n```\n\nThe user invented the question.\n\nThe agent translated it.\n\nThe database answered it.\n\nSimple filtering isn't the most interesting example.\n\nJoins are.\n\nImagine the customer asks:\n\n\"Show me every transaction above $500 made with a card expiring this year. Include the account name, card type, merchant, amount and any document generated for that transaction.\"\n\nWith SQL:\n\n```\nSELECT\n    a.name AS account_name,\n    c.card_type,\n    c.expires_at,\n    t.merchant,\n    t.amount,\n    t.created_at,\n    d.filename,\n    d.document_type\nFROM transactions t\n\nJOIN accounts a\n    ON a.id = t.account_id\n\nJOIN cards c\n    ON c.id = t.card_id\n\nLEFT JOIN documents d\n    ON d.transaction_id = t.id\n\nWHERE t.amount > 500\n  AND EXTRACT(YEAR FROM c.expires_at)\n      = EXTRACT(YEAR FROM CURRENT_DATE)\n\nORDER BY t.created_at DESC;\n```\n\nTo do this with traditional REST services, an agent would have to issue N+1 requests: fetch transactions, query corresponding accounts, look up individual card details, and pull related documents.\n\n```\nGET /transactions?minAmount=500\nGET /accounts/{accountId}\nGET /cards/{cardId}\nGET /transactions/{transactionId}/documents\n```\n\nInstead of re-inventing query engine mechanics through custom HTTP parameters, we leverage SQL directly.\n\nHuman UI workflows are fixed. Agent workflows are dynamic.\n\nUsers ask questions that span multiple dimensions:\n\n\"Compare monthly spending between my personal and business accounts for the last two years, grouped by category, but exclude transactions that were later refunded.\"\n\nor:\n\n\"Find documents linked to transactions made using cards that have since been cancelled.\"\n\nor:\n\n\"Show merchants where my average transaction value increased by more than 30% compared with last year.\"\n\nor:\n\n\"Find accounts with incoming transfers that were followed by an outgoing payment within 24 hours.\"\n\nThe combinatorial explosion of possible queries makes static endpoint design unviable. Data access and write operations require distinct architectural boundaries.\n\nReplacing hundreds of specialized endpoints with a clean data interface:\n\n```\n// DATA INTERFACE\nget_schema()\nquery(sql)\ngetAccounts()\ngetAccount()\ngetBalance()\ngetTransactions()\ngetTransactionsByDate()\ngetTransactionsByCategory()\ngetCards()\ngetDocuments()\ngetDocumentsByTransaction()\ngetMonthlySpending()\ngetSpendingByCategory()\n...\n```\n\nThe separation of concerns becomes clear:\n\n• **Schema & Metadata:** Defines entity semantics.\n\n• **SQL:** Expresses arbitrary query intent.\n\n• **Database Storage Layer (e.g., KalamDB):** Enforces tenant-isolation, security bounds, and execution constraints.\n\nExposing production database credentials directly to an LLM is a major security risk. That is not the design here.\n\n**The agent isn't given unrestricted access to the application's database.**\n\nIt is given controlled access to **the authenticated user's data boundary**.\n\nConceptually:\n\n```\nUser\n  │\n  ▼\nAI Agent\n  │\n  ▼\nMCP Server\n  │ authenticated user context\n  ▼\nKalamDB (Tenant Isolation Layer)\n  └── Tenant Scope: user_123\n          ├── accounts\n          ├── transactions\n          ├── cards\n          └── documents\n```\n\nWhen:\n\nWhen `user_123`\n\nexecutes a query, execution is locked inside that tenant's boundaries.\n\n```\nSELECT *\nFROM transactions;\n```\n\nWe do not rely on the LLM to append `WHERE user_id = 'user_123'`\n\n. Tenancy security must be enforced deterministically below the model layer.\n\nStart with a strict read-only model:\n\n```\nget_schema()\nquery()\n```\n\nNo `INSERT`\n\n, `UPDATE`\n\n, or `DELETE`\n\npermissions.\n\nState mutation requires strict business rules, idempotency checks, fraud controls, and transactional guarantees. Read-only query execution provides immense value while minimizing operational risk.\n\nOnce an agent can safely query the user's data, your application's UI stops being the only interface to that data.\n\nThe customer could say:\n\n\"Create a pie chart showing where I spent my money this year.\"\n\nThe agent queries:\n\n```\nSELECT\n    category,\n    SUM(amount) AS total\nFROM transactions\nWHERE created_at >= DATE_TRUNC('year', CURRENT_DATE)\nGROUP BY category\nORDER BY total DESC;\n```\n\nThen the agent renders the visualization.\n\nOr export custom analytics directly:\n\n\"Create an Excel workbook containing all my 2025 transactions, with one sheet per account.\"\n\nThe agent fetches data directly and builds the spreadsheet without requiring custom backend export features.\n\nToday many applications technically allow customers to export their data.\n\nStandard data export features often force users through clunky batch workflows:\n\n```\nSettings → Privacy → Request Export → Wait → Download ZIP\nSettings → Privacy → Request Export → Wait → Download ZIP\n```\n\nAn agent backed by a schema-aware query engine renders custom exports trivial. Users can request CSVs, PDFs, or formatted reports on demand.\n\nKalamDB makes another interesting capability possible.\n\nSQL queries can also become subscriptions.\n\nFor example:\n\n```\nSELECT\n    id,\n    account_id,\n    merchant,\n    amount,\n    created_at\nFROM transactions\nWHERE amount > 1000;\n```\n\nAn application could subscribe to matching changes.\n\nConceptually:\n\n```\nKalamDB\n   │\n   │ transaction committed\n   ▼\nSQL Subscription\n   │\n   ▼\nApplication / Agent\n```\n\nRather than polling endpoints continuously, the agent subscribes directly to live data events matching query constraints.\n\nREST, RPC, and GraphQL remain essential for state changes. We draw a clear line between **Data Access** and **Transactional Actions**.\n\nReading balance data is a query task. Money transfers, card freezes, and account applications belong in strict RPC/REST tools.\n\nAn MCP interface could therefore look like:\n\n```\n// DATA\nget_schema()\nquery(sql)\n\n// ACTIONS\ntransfer_money(...)\nfreeze_card(...)\nrequest_new_card(...)\n```\n\nThis is the model I find particularly interesting.\n\n**SQL handles the long tail of questions.**\n\n**Explicit APIs handle consequential actions.**\n\nThis architectural pattern simplifies the data layer into three components:\n\n• **Schema & Metadata:** Tells the agent what entities exist, what fields mean, data types, relationships, and index efficiency.\n\n• **Permissions:** Enforces what data the authenticated user is permitted to query.\n\n• **SQL:** Expresses how to assemble answers to arbitrary user questions.\n\nThat's a very different abstraction from describing hundreds of predetermined endpoints.\n\nTraditional application stack:\n\n```\nDatabase → Backend → REST / GraphQL API → Application → User\n```\n\nData-first agent architecture:\n\n```\n                     ┌── Product UI\n                     ├── AI Agent\nUser Data Layer ─────┼── User scripts\n                     ├── Analytics\n                     └── User-built applications\n```\n\nThe product UI becomes **one interface to the user's data**.\n\nNot necessarily the only interface.\n\nIn KalamDB, we anchor data around tenant-isolated boundaries. When AI agents interact with data, we provide them with structured schema metadata, relationships, tenant permissions, SQL capabilities, and real-time streaming subscriptions.\n\nImagine six months after launching your application a customer asks:\n\n\"Can you add an API that returns monthly spending grouped by merchant, excluding refunded transactions, together with the card and account used for each payment?\"\n\nTraditionally:\n\n```\nFeature Request → API Design → Implementation → Auth → Tests → Docs → Deployment\nFeature Request → API Design → Implementation → Auth → Tests → Docs → SDK Update → Deployment\n```\n\nWith a safe SQL query layer over tenant data, the capability already exists. You don't need to build or deploy new endpoints for unexpected queries.\n\nInstead of building APIs around fixed assumptions, we build systems that allow users and AI agents to safely query data in ways we didn't predict.\n\nCheck out more at [kalamdb.org](https://kalamdb.org).", "url": "https://wpnews.pro/news/the-api-endpoint-explosion-what-if-ai-agents-could-use-sql-on-user-data-directly", "canonical_source": "https://dev.to/slipxnot/the-api-endpoint-explosion-what-if-ai-agents-could-query-user-data-directly-56dp", "published_at": "2026-08-14 13:25:26+00:00", "updated_at": "2026-08-14 13:35:37.046889+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "artificial-intelligence"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/the-api-endpoint-explosion-what-if-ai-agents-could-use-sql-on-user-data-directly", "markdown": "https://wpnews.pro/news/the-api-endpoint-explosion-what-if-ai-agents-could-use-sql-on-user-data-directly.md", "text": "https://wpnews.pro/news/the-api-endpoint-explosion-what-if-ai-agents-could-use-sql-on-user-data-directly.txt", "jsonld": "https://wpnews.pro/news/the-api-endpoint-explosion-what-if-ai-agents-could-use-sql-on-user-data-directly.jsonld"}}