Skip to main content
Glama
AImart Parquet

AImart Parquet

by balogvn

AImart Parquet

Messy data in, clean Parquet out, paid per call by AI agents.

AImart Parquet is an MCP server and REST API that turns messy CSV/TSV, JSON/NDJSON, Excel, SPSS, Stata and SAS files, and tables in digital PDFs, into clean, typed, zstd-compressed Parquet. You can then inspect the result (footer only), preview rows, and query it with sandboxed, read-only DuckDB SQL, including joins across several files. Agents pay per call in USDC with x402 v2. There is no signup and no API key.

Your data: inputs are deleted when the call ends. Converted files become unreachable after 60 minutes and are normally deleted within minutes after that; with Cloud Storage the worst case is about a day (the bucket's 1-day lifecycle rule). File contents are never logged or used for training. Details: docs/privacy.md.

Why it exists: an agent that reads a 100,000-row CSV as text spends a large share of its context on raw rows. With this service it reads a compact schema report and then asks targeted SQL questions. Every conversion reports two rough numbers so you can judge the trade-off for your own data: estimated_tokens_raw (input text bytes / 4) and estimated_tokens_schema (a column digest of name, type and null count, / 4). The ratio depends on rows × columns, so there is no fixed saving:

input (measured with aimart-parquet convert)

estimated_tokens_raw

estimated_tokens_schema

3-row, 2-column CSV

9

18

100,000-row, 6-column synthetic CSV (4.2 MB → 0.6 MB Parquet)

1,044,271

59

For tiny files the report costs more than the file, so just read those directly. The schema estimate covers only the column digest. The full JSON response is larger: it adds per-column notes and, on the hosted service, a provenance block with a 3,309-byte ML-DSA-65 signature (4,412 base64 characters).

Status: v0.1.0. It is not yet deployed or published to PyPI. Every https://YOUR-SERVICE.run.app below is a placeholder for your own deployment's URL (hosting). Until the PyPI release, run the local server from a checkout with make mcp.

Contents

Related MCP server: Pipetable

Quickstart for agents

The typical flow is: convert_to_parquet returns an artifact_id and a schema report. Then inspect_parquet or preview_parquet lets you look, and query_parquet answers questions with SQL. Pass the artifact_id between calls instead of re-sending the file. Inputs are a public url (best for large files), content_base64 plus filename (under about 3 MB), or the artifact_id of a file converted earlier (convert takes one too, to convert it again with new options). Over MCP only convert_to_parquet takes content_base64; on REST, convert, inspect, preview and query take all three.

REST: 402 → decode → pay → retry

An unpaid call returns HTTP 402 with an empty body. The quote is in the base64 PAYMENT-REQUIRED header:

URL=https://YOUR-SERVICE.run.app   # placeholder

curl -s -D - -o /dev/null -X POST "$URL/v1/convert" \
  -H 'content-type: application/json' \
  -d '{"url": "https://example.com/sales.csv"}' \
  | awk 'tolower($1) == "payment-required:" {print $2}' | tr -d '\r' | base64 --decode | jq .
{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {"url": "https://YOUR-SERVICE.run.app/v1/convert", "mimeType": "application/json", "...": "..."},
  "accepts": [{
    "scheme": "exact",
    "network": "eip155:84532",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "amount": "20000",
    "payTo": "0x…",
    "maxTimeoutSeconds": 300,
    "extra": {"name": "USDC", "version": "2"}
  }]
}

amount is in USDC atomic units (6 decimals): 20000 is $0.02. curl cannot sign. An x402 client signs the quote with your wallet and retries with a PAYMENT-SIGNATURE header. The settlement comes back in PAYMENT-RESPONSE. With the x402 Python SDK (pip install "x402[evm,httpx]"):

import os

import anyio
import httpx
from eth_account import Account
from x402 import max_amount, x402Client
from x402.http.clients.httpx import x402AsyncTransport
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import ExactEvmClientScheme

URL = "https://YOUR-SERVICE.run.app"  # placeholder
TEST_KEY = os.environ["PAYER_PRIVATE_KEY"]  # a Base Sepolia test wallet; stays on your side

payer = x402Client()
payer.register("eip155:84532", ExactEvmClientScheme(EthAccountSigner(Account.from_key(TEST_KEY))))
payer.register_policy(max_amount(20_000))  # never pay more than $0.02 per call


async def main() -> None:
    transport = x402AsyncTransport(payer, httpx.AsyncHTTPTransport())
    async with httpx.AsyncClient(base_url=URL, transport=transport, timeout=180) as http:
        r = await http.post("/v1/convert", json={"url": "https://example.com/sales.csv"})
        artifact_id = r.json()["artifact"]["id"]
        r = await http.post(
            "/v1/query",
            json={
                "artifact_id": artifact_id,
                "sql": "SELECT region, sum(units) FROM data GROUP BY 1",
            },
        )
        print(r.json()["rows"])


anyio.run(main)

A complete, tested script for REST, MCP and provenance checks is in docs/examples/agent_pays.py. The step-by-step guide is in docs/agents-guide.md. If a paid retry still returns 402, the body is again {} and the reason is in the PAYMENT-REQUIRED header's error field.

MCP

The remote server speaks Streamable HTTP at /mcp. initialize and tools/list are free. Each paid tools/call is charged through x402's MCP transport: an unpaid call returns isError: true with the PaymentRequired object in structuredContent, and the client retries with the signed payload in params._meta["x402/payment"].

Claude Code:

claude mcp add --transport http aimart https://YOUR-SERVICE.run.app/mcp

Cursor, or any client that takes a remote URL (.cursor/mcp.json or equivalent):

{ "mcpServers": { "aimart": { "url": "https://YOUR-SERVICE.run.app/mcp" } } }

Free local server over stdio, for files on your machine. It needs no payments, no wallet and no network. After the PyPI release:

claude mcp add aimart-local -- uvx aimart-parquet mcp
# with the quantum tools:
claude mcp add aimart-local -e AIMART_ENABLE_QUANTUM=true -- uvx --from 'aimart-parquet[quantum]' aimart-parquet mcp
{ "mcpServers": { "aimart-local": { "command": "uvx", "args": ["aimart-parquet", "mcp"] } } }

Local mode takes path inputs and writes the output next to the input, or to output_path. If AIMART_HOSTED_URL is set, convert results include a one-line hint that points at the hosted service, which adds signed provenance.

Paying from an MCP host. Today most MCP hosts do not sign x402 payments by themselves. In those hosts, tools/list works and paid tools return the PaymentRequired error result. To pay over MCP you need an x402-capable MCP client, such as the Python client in docs/agents-guide.md. The local server never asks for payment.

Joins

One query reads up to 8 tables in total: the top-level input is table data, and tables adds up to 7 more by name, for one query price. REST takes an object per table (artifact_id, or url of a Parquet file); the MCP tool takes the artifact_id string itself (a local path in local mode). With sql = SELECT d.*, o.status FROM data d JOIN orders o USING (order_id):

REST  POST /v1/query  {"artifact_id": "<invoices_id>", "sql": "...", "tables": {"orders": {"artifact_id": "<orders_id>"}}}
MCP   query_parquet   {"artifact_id": "<invoices_id>", "sql": "...", "tables": {"orders": "<orders_id>"}}

Pricing

Prices are the Settings defaults, in USDC per successful call. A call that fails (HTTP ≥ 400, or an MCP isError result) is never settled, so you are not charged for errors. A deployment can override each price with AIMART_PRICE_*. Its live list is at GET /pricing.

operation

REST

MCP tool

price

convert

POST /v1/convert, POST /v1/convert/upload (multipart)

convert_to_parquet

$0.02

inspect (footer only)

POST /v1/inspect

inspect_parquet

$0.002

preview

POST /v1/preview

preview_parquet

$0.002

query (one read-only SELECT; up to 8 tables: data plus up to 7 more in tables)

POST /v1/query

query_parquet

$0.005

premium (off by default): quantum-kernel PCA

POST /v1/quantum/compress

quantum_compress_features

$0.50

premium (off by default): QAOA portfolio

POST /v1/quantum/portfolio

quantum_optimize_portfolio

$1.00

Free: GET /, /health, /pricing, /llms.txt, /openapi.json, /docs, /.well-known/x402, /.well-known/mcp/server-card.json, /.well-known/aimart-signing-key, POST /v1/verify and the verify_provenance tool, POST /v1/feedback and the request_capability tool (tell us what you needed and would pay for), signed artifact downloads (GET /v1/artifacts/{id}/{filename}?exp&sig), MCP initialize and tools/list, and the whole local stdio server.

The floor is $0.002 because facilitators reject payments below about $0.0011–0.0015, and some charge up to about $0.002 per settlement. Below that floor a call is either unsettleable or loss-making. See docs/economics.md.

Networks: testnet by default (Base Sepolia eip155:84532, facilitator https://x402.org/facilitator). Mainnet (Base eip155:8453 via Heurist, Solana via Dexter) must be switched on explicitly by the operator. See docs/payments.md.

Supported formats

family

extensions

notes

delimited text

.csv .tsv .tab .txt

sniffs encoding (UTF-8, UTF-16, cp1252…), delimiter and header; skips title rows; 1.234,56 and $1,234 numbers; IDs with leading zeros stay text

JSON

.json .jsonl .ndjson .geojson

arrays, NDJSON, nested records (json_records_path, flatten_nested); repairs broken JSON up to 5 MB

spreadsheets

.xlsx .xlsm .xlsb .xls .ods

pick a sheet by name or index; the report lists available_sheets

SPSS

.sav .zsav .por

variable and value labels kept (Parquet metadata, applied, or both)

Stata

.dta

labels kept

SAS

.sas7bdat .xpt

labels kept

Parquet

.parquet .pq

re-typed and re-compressed

PDF (digital)

.pdf

tables with or without borders; tables continuing across pages are merged; pick pages with pdf_pages ("1-3,7") and a table with pdf_table; the report lists every table found. Scanned PDFs (images, no text layer) are rejected: no OCR. Password-protected PDFs are rejected with an error that says so

Any of these can also arrive gzip-, bz2- or zip-compressed, once or twice (a zip of data.csv.gz is read as the CSV); xz, zstd, lz4, 7z and rar, and a third layer, are refused with 415 unsupported_format. Detection uses magic bytes before extensions. Every column in the report carries its type, null count, rejected values with examples, and notes on anything the service assumed: "assumed day-first", "ambiguous thousands/decimal separator; assumed a decimal comma, so '1.234' is 1234" (the column's own values decide, then the file's other number columns when they agree and one writes a thousands group with a decimal separator, else the delimiter), "values without a UTC offset were read as UTC (k of n)", "values with a UTC offset were rejected (k of n)" or "converted to UTC", and "zone 'BST' was not read as a zone (k of n)" for a zone abbreviation with several meanings (BST, IST, CST, ...; only offsets, UTC, IANA names and GMT, EST, MST, HST and JST are read). Report warnings say when nested JSON keys that differ only by case were renamed (a.K is now a.K_2), title lines above a PDF table (or atop each of its pages) were dropped, or a PDF table has no header row (its columns are then column_1, column_2, ...; header=true reads its first row as names). expected_schema with strict turns the conversion into a validation step. The full option list is in the ConvertOptions schema (/openapi.json, or the tool's input schema), and the notes are listed in the agents guide.

Limits and errors

A failed call is never charged: every refusal is HTTP ≥ 400 (or an MCP isError result) with a code, a message and a hint. A call that can only fail, decided from the request alone, is refused before any price is quoted: a body or arguments that fail the schema, not exactly one input, content_base64 that decodes to no file (or, for inspect, preview and query, to no Parquet file), an upload whose file part is empty, an MCP argument the tool does not take, a URL that can never be fetched, or SQL of up to 256 characters that is not one SELECT (400 invalid_request, 422 invalid_input, 422 url_not_allowed or 400 query_rejected, or an MCP error result; the full list is in the agents guide). The defaults, which an operator can change:

limit

default

setting

over it

input size (url, content_base64 or upload)

50 MB per call, all inputs of the call together; stored artifact_ids do not count

AIMART_MAX_INPUT_BYTES

413 input_too_large

decompressed size

256 MB for a gzip, bz2 or zip stream; 512 MB for the XML of all the sheets of an xlsx or ods

AIMART_MAX_DECOMPRESSED_BYTES, AIMART_MAX_WORKBOOK_XML_BYTES

413 input_too_large

memory per job

700 MB, counting its resident memory plus its temporary files

AIMART_JOB_MEMORY_LIMIT_MB

413 input_too_large ("The job exceeded its memory budget")

stored outputs

512 MiB for all outputs together (256 MiB on the Cloud Run deploy without a bucket)

AIMART_ARTIFACT_MAX_TOTAL_BYTES

413 for one output larger than the whole budget; 503 busy while the space is held by outputs that have not expired (kept until their expires_at) or outputs still settling; the hint says when to retry

preview and query results

500 preview rows, 1,000 query rows, cells cut to 2,000 characters, 64 MB in all

AIMART_MAX_PREVIEW_ROWS, AIMART_MAX_QUERY_ROWS, AIMART_MAX_CELL_CHARS

more rows: truncated: true; over 64 MB: 413

time

120 s to convert, 15 s to inspect, preview or query

AIMART_CONVERT_TIMEOUT_S, AIMART_QUERY_TIMEOUT_S

504 timeout

downloads

60 s of downloading for all of a call's url inputs together

AIMART_FETCH_TIMEOUT_S

422 fetch_failed

concurrency

2 jobs; a call waits for a free one as long as its payment window leaves it (60 s to convert, 165 s to inspect or preview, 160 s to query), with up to 4 calls per job waiting

AIMART_MAX_CONCURRENT_JOBS, AIMART_QUEUE_WAIT_S

503 busy

CSV memory no longer grows with the file. The text is staged as compressed Parquet instead of loaded into a table, so a conversion's process stays at about 150-330 MiB whatever the file's size (it took about 3.5 times the file, and CSVs over about 120 MB were refused). The job's files still grow with it, and they count toward the 700 MB a job may use because /tmp is RAM on Cloud Run: the upload, the staged text and the output (those two about 0.4-0.65 times the CSV), and no input outlives its read. A gzip of CSV or TSV is not unpacked: DuckDB reads it where it lies, once one pass has counted all of it against the 256 MB cap and checked that DuckDB reads it as Python does, and text that is not UTF-8 is then transcoded straight from it, its UTF-8 version on disk while it is read. A gzip DuckDB would read otherwise is unpacked, as before: one of more than one member (concatenated, appended to a row at a time, or bgzip's blocks: DuckDB can stop before the last member, with no error), or with a header field DuckDB refuses (FTEXT, FHCRC, FCOMMENT) or zero padding after it. What gzip, pigz, Python and pandas write is one member. A bz2 or zip is unpacked too, and one that is not UTF-8 also holds its UTF-8 version while it is transcoded, which then replaces the unpacked text. Measured with the defaults, the input caps bind first: a 255 MiB CSV sent as 37 MiB of gzip converted at 245 MiB in all, and 230 MiB of orders in cp1252 sent as 45 MiB of gzip at 533 MiB; the same orders sent as bz2 or zip peaked at 574-664 MiB, close to the cap, with the unpacked text and its UTF-8 version both on disk. With the input caps raised, a 446 MiB CSV sent as 159 MiB of gzip converts (506 MiB; it used to be stopped at the cap), and 450 MB sent uncompressed is stopped there (157 MiB in memory plus 544 MiB of files). sort_by sorts the whole file and spills to disk past 400 MiB; the spill counts too, and DuckDB may spill only the 300 MiB the cap leaves it: a 100 MB CSV sorts (468 MiB), and so does 198 MiB sent as gzip (654 MiB), while 200 MB sent uncompressed is refused. Over the cap the call fails with 413, unpaid: split such files, or convert them with the free local server, which applies the same defaults, with AIMART_MAX_INPUT_BYTES, AIMART_MAX_DECOMPRESSED_BYTES and AIMART_JOB_MEMORY_LIMIT_MB raised to suit your machine. deploy/README.md has the numbers.

Password-protected and scanned PDFs (422 invalid_input), a column preview or query cannot show, such as a time past year 9999 (422; cast it to text), and a storage failure (503 storage_unavailable) are refused the same way. Every code, and what to do about it, is in the agents guide.

Premium quantum tier

Off by default. A deployment serves these tools only with AIMART_ENABLE_QUANTUM=true (and the quantum extra installed). A skeptical buyer panel of AI agents valued them at $0 and said they distract from the core product, so the hosted service leads with conversion.

Honesty first. Both premium tools run on a quantum circuit simulator (PennyLane), not on quantum hardware. At these sizes a simulator gives no speed or accuracy advantage over classical methods. Every result therefore includes a classical baseline computed in the same call, with a comparable metric, and the first entry of notes always says so. We do not claim quantum advantage.

  • quantum_compress_features ($0.50) runs quantum-kernel PCA. Rows are embedded with an entangling IQPEmbedding feature map and compared by state fidelity, and the kernel is decomposed (Nyström, on sampled landmarks). The bandwidth is chosen automatically to avoid kernel concentration. It writes a Parquet artifact with qpc_1..k (quantum) and pc_1..k (classical PCA) side by side, plus a neighbor_preservation_* score for each, so you can use whichever scores higher.

  • quantum_optimize_portfolio ($1.00) picks budget of up to 14 assets using QAOA with an XY mixer started from the uniform feasible state. It returns the result together with the exact optimum from exhaustive classical search and the approximation ratio.

What we measured, and when to use which output: docs/quantum.md. The spec and honesty contract: docs/specs/quantum-premium.md.

Post-quantum provenance

Every artifact the hosted service produces carries a provenance block: a manifest (artifact_id, sha256, size_bytes, filename, operation, created_at, service, version), an ML-DSA-65 signature (NIST FIPS 204, a post-quantum signature scheme), and a public_key_url. Signing proves the file came unaltered from this service. It does not prove the data is correct.

To verify:

  • Free endpoint: POST /v1/verify with {"manifest": …, "signature": …, "artifact_id": …} (or the free verify_provenance MCP tool). With artifact_id, the server also re-checks the stored file's hash and size.

  • Offline: fetch GET /.well-known/aimart-signing-key once. It gives the base64 public key and the context string. Verify the signature over the canonical JSON of the manifest (sorted keys, separators (",", ":"), UTF-8), then compare the downloaded file's SHA-256 with manifest.sha256:

import base64, hashlib, json
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA65PublicKey  # cryptography >= 48

key = MLDSA65PublicKey.from_public_bytes(base64.b64decode(key_doc["public_key"]))
message = json.dumps(manifest, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
key.verify(base64.b64decode(signature), message, key_doc["context"].encode())  # raises if forged
assert hashlib.sha256(parquet_bytes).hexdigest() == manifest["sha256"]

The operator sets the key with AIMART_SIGNING_KEY_MLDSA (base64 of a 32-byte seed). If it is unset, the server uses an ephemeral key, logs a warning, and reports "ephemeral": true. Signatures from an ephemeral key stop verifying after a restart.

Security model

In short:

  • URL fetches are SSRF-guarded at connect time: every resolved address must be public, every redirect is re-checked, only web ports are allowed, and size and time are capped.

  • DuckDB is locked down: no external access, file access only to the job's own directory, configuration locked. Queries are exactly one SELECT, with a row cap and an interrupt at the deadline.

  • Every parse of a caller's file (convert, inspect, preview, query) runs in a killable child process with a deadline and a memory cap, so a parser crash or a memory bomb fails one unpaid call.

  • Decompression is streamed against hard caps.

  • Request bodies are size-limited and clients are rate-limited.

  • The server holds no private keys, only receive (payTo) addresses.

  • Downloads use HMAC-signed, expiring links.

The details and how to report a vulnerability are in SECURITY.md.

Hosting

The hosted service runs on Modal; Google Cloud Run is the alternative (set DEPLOY_HOST=cloudrun). Both deploys, the free-tier budgets and the spend caps are in deploy/README.md. To run the container yourself:

The container image (Dockerfile: multi-stage, digest-pinned bases, non-root) serves REST and MCP on $PORT. The minimum configuration is AIMART_PAY_TO_EVM and/or AIMART_PAY_TO_SVM (receive addresses), AIMART_PUBLIC_BASE_URL, and AIMART_SIGNING_KEY_MLDSA from Secret Manager. Deployment steps, the spend cap and the free-tier budget are covered in deploy/README.md, with the rationale in ADR 0004.

make docker
docker run --rm -p 8080:8080 -e AIMART_PAY_TO_EVM=0xYourReceiveAddress aimart-parquet:dev

The image will be published as ghcr.io/balogvn/aimart-parquet once release automation exists.

Development

Python 3.12+ (the repo pins 3.13), managed with uv.

command

does

make setup

install locked dependencies, every extra, and the dev group

make check

lint + type + test: the definition of green

make fmt / make lint / make type / make test

the parts of check

make serve

REST + MCP over HTTP on $PORT or 8000 (testnet unless configured)

make mcp

the free local stdio MCP server

make docker

build the image

make smoke URL=https://host

decode a live deployment's 402s and check prices, payTo and network

make mutate

mutation-test the money and security paths

The CLI converts single files too: aimart-parquet convert data.sav -o data.parquet and aimart-parquet inspect data.parquet. The contributor guide is CONTRIBUTING.md. AI coding agents should start with AGENTS.md.

Documentation

License

Apache-2.0. Copyright Kayode Balogun.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Turns a folder of CSV, Parquet, and JSON files into a single SQL-queryable source for AI agents, supporting JOINs across files with read-only sandboxed access.
      6
      20 PyPI
      2
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Query local CSV, Parquet, JSON and TSV files with real SQL via DuckDB. Gives your AI coding tool ground-truth data access instead of hallucinated answers.
      5
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables AI assistants and MCP clients to ask natural-language questions about DuckDB or CSV data and receive safe, read-only SQL-generated tabular insights with automatic schema discovery and multi-table joins.
      1
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI assistants to explore and analyse local CSV, Parquet or JSON files through validated read-only SELECT queries, with schema discovery, profiling and query-plan tools. Results are capped, timed, PII-masked and audit-logged inside a locked DuckDB sandbox that cannot write, reach the network or read other files.
      MIT