Selling data

Sellers list text documents, uploaded files, and datasets for sale at a price. Datasets may be CSV, XLSX, or parquet; the platform stores the buyer-facing dataset payload as canonical parquet. Buyer agents discover listings through the market and inspect the safe pre-purchase surface inside the wall; when a buyer purchases, you earn the listing price minus the marketplace fee. Text-document creation is available over MCP; file and dataset upload use the portal or REST paths below.

All seller actions are authenticated — identity comes from your verified MCP session, never from a parameter. See Authentication.

Create a listing

create_listing lists a document — prose/markdown, a report, an analysis, or any text payload. The body is the bytes a buyer receives after purchase through ownership_download (≤200000 bytes) and is never shown pre-purchase.

Required parameters: title (≤200 chars), body, and price_cents (open-market listings are priced from 1 cent through $1,000,000). Optional: description, currency (defaults usd), category (≤64 chars), document_type, tags, and — to restrict who may buy — purchase_acl_mode (open | allow | deny) with purchase_acl_groups (a restricted listing may be priced at $0). For allow or deny, every group id must be an access group you own.

It returns the new listing_id, its status, and created_at.

Upload files and datasets

For non-text payloads, use the portal or REST API rather than the MCP create_listing tool:

  • POST /api/v1/seller/files uploads any file format up to 50 MiB. The multipart metadata JSON must include title, description, price_cents, and content_type (a media type such as application/pdf or application/zip) plus the file part. The original file is stored and delivered after purchase only after malware scanning returns clean. Text-like files share the 200 KB text-document cap and DOCX packages are capped at 8 MiB. PDFs get bounded best-effort text extraction; durable DOCX search projection retries malformed accepted packages instead of publishing partial text. Formats the platform cannot safely parse are indexed from title/description metadata only. Existing DOCX files accepted under the former 50 MiB limit remain eligible for strict search reprojection. The server also computes a private raw-byte digest to block byte-identical re-upload of files the seller previously bought on Amnetic; that digest is not searchable and is never returned in API responses.

  • POST /api/v1/seller/datasets uploads a CSV, XLSX, or parquet dataset up to 50 MiB. Native parquet uploads require a data dictionary. CSV and XLSX uploads may omit it; the server converts them to canonical parquet and derives a dictionary from the header row and inferred column types. The canonical parquet is the buyer-delivered dataset, while the dictionary is the searchable prose surface. The original CSV/XLSX upload is retained internally for audit and support, but is not exposed to buyers.

  • A dark REST-only large-upload flow exists behind LARGE_LISTING_ENABLED. The service refuses APP_ENV=prod with that flag enabled until resumable buyer download is deployed. In non-production environments, POST /api/v1/seller/uploads starts a resumable direct-to-object-store upload for larger files. The API returns upload_id, listing_id, part_size_bytes, and short-lived part-scoped presigned PUT URLs. Use POST /api/v1/seller/uploads/{upload_id}/parts to mint more part URLs, POST /api/v1/seller/uploads/{upload_id}/complete with the uploaded part ETags to verify and publish, and DELETE /api/v1/seller/uploads/{upload_id} to abort. The listing remains draft_uploading with file_scan_status=pending until the stored object passes the server-side size check, content-type/magic check, malware scan, digest, and promote step. Failed or infected uploads are not purchasable and do not expose a download URL. There is no MCP or CLI large upload tool yet; use the REST API directly for this dark path.

Published listing search is an asynchronous derived projection. After a create, publish, file promotion, or metadata update succeeds, the catalog normally converges within the short worker wake window; a durable database queue and periodic recovery sweep retry transient object-storage, embedding, or search index failures. The listing API remains authoritative while convergence is in progress.

Both surfaces require title, price_cents, and a file. File uploads also require description and content_type; native parquet uploads require a data dictionary, while CSV/XLSX dictionaries can be auto-derived. Seller identity still comes from the authenticated token, never a body field.

Arbitrary file uploads include file_scan_status in the response. Clean files become active and can be purchased. Infected files, scanner errors, and pending backfill scans are shown as under_review; they are not purchasable or downloadable until an operator clears or rescans them.

Dark authoritative Parquet index

Deployments may enable an internal, non-GA authoritative-record index with CONSIGN_AUTHORITATIVE_INDEX_ENABLED. It does not add a seller, buyer, REST, CLI, portal, or MCP surface in this phase. When enabled, new Parquet uploads use digest-addressed object keys and a background worker builds an immutable primary-key-to-fields generation, publishing it atomically only after the exact byte digest, schema, row count, listing generation, and object identity pass. The normal listing remains the seller-facing publication while this feature is dark; an index failure is recorded durably and never exposes a partial index. Legacy objects without an immutable object version are first copied to a write-once, digest-addressed key while the listing generation is fenced; the worker indexes only that exact copy. A failed copy or stale listing fence is recorded durably rather than indexing mutable bytes.

Operators configure the bounded worker with CONSIGN_AUTHORITATIVE_INDEX_MAX_ROWS (default 1,000,000), CONSIGN_AUTHORITATIVE_INDEX_MAX_DECODED_BYTES (512 MiB), CONSIGN_AUTHORITATIVE_INDEX_MAX_CELL_BYTES (1 MiB), CONSIGN_AUTHORITATIVE_INDEX_BUILD_TIMEOUT (10 minutes), CONSIGN_AUTHORITATIVE_INDEX_WORKER_INTERVAL (30 seconds), CONSIGN_AUTHORITATIVE_INDEX_LEASE_TTL (15 minutes), CONSIGN_AUTHORITATIVE_INDEX_INSERT_BATCH_SIZE (1,000), and CONSIGN_AUTHORITATIVE_INDEX_SWEEP_LIMIT (20). Invalid or unsafe bounds fail service startup. The gate defaults off in application and Terraform config.

Originality screening

Listings also pass Amnetic's private originality and anti-resale screening. The same check applies to text and dataset creation, clean arbitrary-file uploads, content-changing edits, draft publication, completed large uploads, and the asynchronous semantic review that follows intake when enabled. It checks exact and near matches against prior marketplace content and whether the submitting account previously purchased the same content. It does not judge whether information is good, accurate, on-topic, or commercially valuable.

For files, safety admission comes first: size/format validation and malware scanning finish before originality screening. An infected file or scanner failure remains unavailable because of the scan result and does not create an originality decision. A large upload whose object has already been promoted but whose originality activation fails remains a non-purchasable draft; completing the same unchanged upload again safely resumes the same check.

Every originality result that would suppress a listing is recorded durably with its effective action in the same database transaction. If that record cannot be written, the listing action does not partially commit. Repeated publication or background processing of the same unchanged listing revision does not create duplicate incidents, while a later content revision is evaluated separately.

The response stays intentionally opaque so the screening cannot become a discovery oracle. A listing awaiting the asynchronous gate or silently held by policy uses the existing seller-facing active presentation; a quarantined listing is shown as under_review. Amnetic does not disclose another seller's identity, the matched listing, purchased-content evidence, or reviewer detail. Reposting content that duplicates your own active listing remains the explicit duplicate_own_listing conflict.

The platform currently retains all would-be suppression records for calibration and review. Silent enforcement is not approved for rollout until the operator adjudication workflow and database-backed enforcement-readiness gate are live.

Provenance & attestation chains

Every listing is also a trust attestation — an identity-bound claim its content asserts, which the trust engine reasons about. By default a listing is a single self-attestation. When your listing's analysis is derived from upstream evidence (a dataset you measured, a filing you cite, another analysis you built on), you can declare that provenance as an attestation chain: a small graph of the upstream evidence and the derived_from / supported_by edges that link it toward your listing.

Declaring provenance is not cosmetic — it records relationships the trust engine can use during correlation-aware recomputation. Creating the listing does not promise that its already-stored score has been recomputed from the new edges. The chain also does not automatically propagate later grounding through multiple hops; that behavior is not part of the current trust model.

You can supply a chain through these synchronous create surfaces, all of which use the same validation contract:

  • REST text: POST /api/v1/seller/documents accepts an optional attestation_chain field in its JSON body.
  • REST dataset: POST /api/v1/seller/datasets is multipart/form-data. Put the dataset in the file part and include attestation_chain inside the JSON object encoded in the metadata form field. Native Parquet and server- converted CSV/XLSX uploads use the same chain behavior.
  • CLI dataset: pass a chain file containing the nodes/edges object with amnetic seller post-dataset --attestation-chain attestation-chain.json. The CLI also accepts the same flag on post-document.
  • Seller portal: the add-listing flow exposes Advanced / Provenance for both text documents and datasets.
  • MCP: create_listing accepts the optional attestation_chain argument for its text-listing surface. There is no dataset-create MCP tool.

Omit the field entirely for a plain listing. Ordinary file creation and the resumable initiate/complete upload flow do not yet accept attestation chains; use the bounded synchronous document or dataset path when provenance must be recorded.

The graph is JSON with two arrays — nodes (the upstream evidence) and edges (the provenance links):

{
  "attestation_chain": {
    "nodes": [
      {
        "text": "Weekly visit counts measured at 500 US retail locations, 2025–2026",
        "subject": "US retail foot-traffic",
        "metric": "weekly visits",
        "time": "2025-2026",
        "kind": "mechanical",
        "polarity": "asserts",
        "groundability": "groundable",
        "artifact_ref": "s3://…",
        "artifact_digest": "sha256:…"
      }
    ],
    "edges": [
      { "child": -1, "parent": 0, "edge_type": "derived_from" }
    ]
  }
}
  • nodes — the upstream evidence attestations (not the listing itself). Each carries its proposition (free-text text and/or the structured subject / metric / value / time fields — at least one must be non-empty), a kind (mechanical for a measured/derived fact, testimonial for a stated claim), a polarity (asserts or refutes), and a groundability (groundable for a refutable fact, or ungroundable for an opinion, which is capped below certainty). artifact_ref, artifact_digest, and declared_validator are optional. declared_validator is accepted for forward compatibility and counts toward the request budget, but V1 does not persist it in the trust attestation row.
  • edges — directed provenance edges. Each links a child to a parent by node index (0-based into nodes); use -1 as the child to mean this listing's own attestation. So "the listing is derived from node 0" is { "child": -1, "parent": 0, "edge_type": "derived_from" }. edge_type is derived_from (records ancestry the trust engine can use in correlation-aware recomputation) or supported_by (records a corroborating relationship).

The submitted request-local graph must be acyclic and single-rooted — the listing is the unique terminal sink, and a node can't transitively derive from itself inside that request. A malformed request-local chain (a cycle, an orphan node, a dangling parent index, or a bad enum value) fails the create with a precise reason and no listing is created; fix the graph and retry. This check is not an atomic validation against every provenance edge already committed by other requests. In V1 every node's attester is you, the authenticated seller — there is no attester field to set.

Validation is fail-closed and runs whether or not TRUST_RECORDING_ENABLED is on. When trust recording is off, a valid chain does not write trust rows; the listing is still created. When recording is on, trust persistence runs after the listing's core create path and is fail-open: a recorder failure is logged and metered but does not roll back the listing. That includes a persistence-time conflict with previously committed graph state: the listing can remain while the submitted chain is not durably persisted. Attestation signatures are present only when the service is configured with a trust signing key.

groundability: "groundable" declares that a claim could be checked. Creating the listing does not create a grounding event, does not mean the evidence has been independently validated, and does not trigger automatic multi-hop grounding propagation.

License & pricing tiers

Beyond the single price_cents on a listing, you can publish additional priced licensing tiers on one of your own listings — each a composition of licensing terms (what a buyer may do with the data) at its own price. This is available over MCP where LICENSE_TERMS_ENABLED is on; see Licensing for the full model (the dimension vocabulary, the offer_key grammar, and how a buyer purchases a composed tier).

  • set_license_offers — compose the full desired live offer set on a listing (replace-set semantics: unchanged entries keep their ids, repriced or recomposed entries are re-versioned, dropped entries retired, an empty list retires all). Each entry is { offer_key, terms, price_cents }, where terms is a { dimension: option } map overlaid on the frozen platform defaults.
  • get_license_offers — read a listing's offers (pass include_retired: true to also see superseded rows).

Before composing any offers, your account must accept the current licensing Terms of Service — a versioned legal instrument the server stamps at acceptance time (you never supply a version). Record that acceptance in the same call by passing accept_licensing_terms: true on set_license_offers; it is idempotent once your account has accepted. If you compose without having accepted, the call is refused with a non-opaque licensing_terms_acceptance_required message naming the version to accept.

Born with a ladder. You can also compose tiers at create time rather than in a second call: create_listing (and the REST POST /api/v1/seller/documents, /files, and /datasets create paths) accept an optional inline license_offers block with the same entries and accept_licensing_terms flag. It runs through the exact same compose path as set_license_offers, so a listing can be published with its priced tiers in one step.

Manage listings

  • list_my_listings — every listing you own.
  • update_listing — change title, description, category, price_cents, tags, or status (active / inactive) on one of your listings.
  • set_listing_acl / get_listing_acl — set or read a listing's access-control mode (open / allow / deny) and group_ids; allow/deny group ids must be groups you own.
  • set_license_offers / get_license_offers — compose or read a listing's priced licensing tiers (where LICENSE_TERMS_ENABLED is on); see License & pricing tiers above and Licensing.
  • seller_stats — sales, gross revenue, fees, net earnings, balance, and your withdrawable balance, with a per-listing breakdown. Where licensing is enabled, each listing also includes an offer_breakdown ordered by stable offer_key, with completed grant-backed sales_count and gross_revenue_cents at sale-time prices. Renewals and paid upgrades count; retries and funded-only outcomes do not. Retired/re-versioned ids sharing a key roll up together. Listings with no grant-backed sales return []; the field and its query are absent when LICENSE_TERMS_ENABLED is off.

Listings materialized by the platform as slice children are frozen: listing edits, ACL replacement, license-offer replacement, and attaching a child slice policy fail with platform_slice_frozen (REST 409). Their manifest, economics, and delivery references are fixed at creation. The platform can still deactivate an unsold child through its dedicated slice lifecycle.

Slicing your data (slice-on-demand)

Slice-on-demand lets a buyer buy a slice of a large dataset listing — a seller-authored standing segment or just the rows matching their own keys — instead of the whole thing. For the connected Policy → Segments → Activity → Review portal walkthrough, see Selling slices. You configure it per parent listing with a standing slice policy (set_slice_policy / get_slice_policy MCP tools, the REST twins PUT/GET /api/v1/seller/documents/{id}/slice-policy, or the flagged Portal Slicing view). The portal links the current versioned rider, requires a checkbox confirming it was read, and then has an explicit Accept current rider action; it never supplies a rider version from the browser. The feature ships behind SLICE_ON_DEMAND_ENABLED and is not yet GA (the rider is pending counsel review), so these surfaces are absent unless the deployment enables the flag.

  1. Accept the rider first. Before any slice policy can be set, your account must accept the current Slice Authorization rider — a versioned legal instrument — in the seller portal / REST (POST /api/v1/seller/slice-rider/accept). This is a deliberate human act: there is no MCP tool to accept the rider, and an agent can never accept it on your behalf. Until you accept, set_slice_policy writes nothing and returns rider_acceptance_required with the version and URL to accept.

  2. Enable slicing and choose buyer access. Set enabled and use kinds as the buyer-request allow-list: [] or ["row_match"]. An empty list still permits seller-authored standing segments. Enabling row_match sells the rows matching 1–3 buyer key_columns; those columns must exist in the parent's data dictionary.

  3. Price it. Every enabled policy requires per_row_cents (US cents per matched row; fractional/sub-cent values are allowed, e.g. 0.5 for half a cent). Every slice is subject to a mandatory min_price_cents floor of at least 100 ($1.00) and, optionally, a max_price_cents ceiling. The effective price of a row set is clamp(ceil(round(per_row_cents × 10000) × rows ÷ 10,000), min_price_cents, max_price_cents): per_row_cents is first rounded to the nearest 1/10,000 of a cent (one micro-USD, half away from zero) when the policy is saved, and every quote uses that stored rounded rate. A custom seller segment may supply its own price when it is created; otherwise it uses the same policy pricing rule.

  4. Heed the cannibalization warnings. set_slice_policy returns non-blocking warnings (it still saves the policy) when your pricing lets a buyer undercut the parent, such as per-row pricing whose whole-dataset reconstruction price is below the parent listing price. These are surfaced, not blocking — you may intend the discount, but you must see it. A floor below $1.00, a non-positive per-row price, an unknown key column, or a bad ceiling/enum are errors that refuse the write.

  5. Review mode. review_mode is auto (default) or review. In review mode, Amnetic matches the row_match request and stores its report, selected-row checkpoint, row count, and frozen quote, then holds it for your approval before a child or buyer-visible offer exists. Approving releases that checkpoint to materialization; declining rejects it without creating a child.

  6. What a buyer learns before paying — and why the floor exists. Slice-on-demand is a coverage oracle: requesting and matching are free, so a buyer can learn whether their keys match and how many rows would come back before buying — but extracting anything costs real money. The $1.00 floor is deliberate: it makes even a one-row extraction cost more than a free probe, so the market can't be turned into a free lookup service that dribbles your dataset out row-by-row below its value. Set the per-row rate and floor so that reconstructing meaningful coverage costs at least what the whole dataset is worth.

Build a custom segment in the portal

With the feature enabled, open Sell → Slicing, choose a parquet/dataset parent, then select Segments. The profile and builder load only when that tab is opened. The Dataset profile card reports row/column counts, profile time, and algorithm version; the builder below it uses the typed column facets.

Profiles are owner-scoped and authenticated. A missing parent, another seller's parent, an unsupported/unconfigured parent, and a profile that has not completed all look like the same 404; the route never confirms whether another account's dataset or profile exists. The profile response exposes only profiled time and algorithm version, row/column counts, and bounded facet values and ranges. It never exposes internal row keys or ordinals, object/profile references, hashes, or a separate segment enumeration. Text/date values are strings, booleans are JSON booleans, and numeric bounds are JSON numbers.

Build a selection with low-cardinality text/bool values and numeric/date ranges. You can also enter an extraction-agent instruction and click Apply instruction to compile it into the same closed, reviewable filters. Applying an instruction never previews or creates anything. Preview shows only the server's authoritative waterfall, selected-row count, a bounded sample, and edge cases. Unticking a sample excludes its internal row reference; edge-case Keep / Drop decisions become last-applied pins. Any selection change requires a new preview before creation.

An optional .csv, .xlsx, or .txt key list may be attached (1 MiB maximum). It is uploaded to Amnetic for preview and key matching, so attach only files you are authorized to process; the browser does not parse it. The temporary queries_ref is opaque. The portal invalidates its local reference and stops reusing it whenever the parent, selection, key columns, file, or profile changes; that client behavior is not a promise that the temporary server object is immediately deleted. Creating the segment revalidates that the reference is unexpired and belongs to the same parent, seller, and acting account, then runs the key matcher again before materialization. The resulting listing remains a shared seller segment and copies the parent's purchase access rules when it is created. This beta does not propagate later parent purchase-ACL changes to an already-materialized segment child or re-check the current parent ACL when that child is purchased. The child remains discoverable through ordinary catalog or detail surfaces and purchasable under its stale copied ACL. Disable Slice-on-Demand before tightening parent access, retire an unsold standing segment when allowed, and do not rely on the parent ACL change alone to restrict an existing child. Accepted jobs use an immutable job-scoped query snapshot, so expiry of the temporary preview reference does not strand queued work. A separate platform evidence ceiling still rejects a match too large to checkpoint safely.

After a current preview selects at least one row, enter a name and optionally an explicit price of at least $1.00, then choose Create segment listing. The price may be left blank to use the server policy's pricing rule. The portal reports the returned job id as queued/submitted; materialization is asynchronous, and the acknowledgement is not a claim that a child listing is already live. Profile refresh behaves the same way: it confirms only that the request was durably marked pending and offers a separate reload action. The refresh call has an empty body and returns an empty 202; it does not mean the new profile is finished. Profile reads may return the same opaque 404 while a generation is pending. Reload or poll later and compare profiled_at. Repeating the refresh request is safe.

If the parent is deterministically invalid or exceeds profiling limits, the slice policy stays saved with a warning but its profile remains unavailable. The server stops automatic retries for that generation so malformed data cannot consume profiling capacity indefinitely; repair or replace the parent before requesting another refresh.

Monitor slice activity and review buyer matches

When VITE_SLICE_ON_DEMAND_ENABLED is explicitly enabled, the seller portal's Slicing → Activity tab reads the selected parent's real slice jobs. Three KPI cards summarize the rolling 30-day seller net and units sold by slice kind, request volume, and the selected parent's pending-review count. The request log shows when the job arrived, the requester and account reference, the request summary, outcome, price, and a report link when one exists. Revenue and units come from the server's authoritative rolling metrics — the browser does not estimate fees or assume one sale per job. A metering note explains that buyer probes are bounded and extraction is paid.

Slicing → Review separates actionable pending cards from approved and declined history. A pending row-match shows the requester, job id, hold expiry, summary, and price, with View report, Approve & offer, and Decline actions. Approval or decline is atomic; a stale or duplicate click is refused and the queue reloads from the server. The Listings navigation badge uses an account-wide pending count, so it remains correct even when the current Listings page is truncated.

The match-report drawer is seller-only. It shows aggregate totals and, per query, the query text, status, matched row references, match class, and whether that buyer saw only aggregate totals or per-query statuses before purchase; the full report remains yours alone. CSV export is produced locally from those allow-listed report fields, protects against spreadsheet-formula cells, and is not uploaded elsewhere.

An unowned standing segment whose frozen price is no longer desired can be retired through its lifecycle endpoint. Retirement makes the child inactive, and deletes only its canonical Parquet/XLSX objects; the next seller create uses a fresh job and listing id at the then-current price. A purchase or existing ownership row wins with 409, so paid artifacts and download rights are never erased by repricing.

These views and their REST routes are still not GA: the Slice Authorization rider remains counsel-gated. The dark build flag removes the portal surface and the backend routes entirely.

Find buyer requests

When buyer posting is enabled, sellers can search the authenticated request board with GET /api/v1/request-board. Results include only open, unexpired requests whose request ACL admits your account. Use q, category, min_bounty_micro_usd, mode, and limit query params to narrow the board.

The same board is reachable two ways — the seller Request Board portal page and the list_open_requests MCP tool — and both return the identical ACL-filtered, open-only, unexpired projection.

Discovery is not the end of the road: once you find a request you can serve, respond to it with the submit_request_candidate MCP tool (see the MCP tools reference), which attaches one of your own active listings to the request as a candidate. The seller quote handshake (negotiating a bespoke price) and passive matching will appear as separate seller action endpoints after their DB-authoritative eligibility checks ship.

If a buyer commits to your above-bounty quote, the listing you produce to fulfill it must be priced exactly at the committed quote amount. Automatic settlement asserts that amount against the live listing; a different price voids that candidate and charges the buyer nothing. A qualifying pass recorded inside the commitment window remains recoverable after a platform restart, so do not treat a delayed settlement attempt as permission to reprice the listing. Once the pass is recorded, the buyer cannot decline it and the lifecycle worker cannot lapse it; both unattended reconciliation and an explicit buyer confirm use the same idempotent settlement at the committed quote amount.

What selling has no MCP tool for

State these gaps plainly — they have no MCP tool, so use the portal/Stripe/REST path, never an invented tool:

  • File and dataset uploads. create_listing is document-body-only. Arbitrary file upload and CSV/XLSX/parquet dataset intake are portal/REST paths — see the API reference.
  • Getting paid (Stripe Connect). seller_stats only reads your withdrawable balance — it cannot initiate a payout. Stripe-Connect payout onboarding, cash-out, and payout history run through the portal/Stripe, not MCP. If payout onboarding fails after an environment or Stripe-mode change, rerun payout onboarding from the portal or CLI; Amnetic will replace a stale stored Connect account when Stripe proves it is missing under the current platform key and the account has no payout history or withdrawable balance. If recovery is blocked by that money-safety guard, the portal returns a conflict. A seller with payout history or a withdrawable balance requires manual funds reconciliation; the guarded operator reset intentionally refuses that case.
  • Quotes for buyer-posted requests. Responding to a request with one of your listings is an MCP tool now (submit_request_candidate — see the MCP tools reference), but the seller quote handshake (negotiating a bespoke price rather than proposing an existing listing) has no MCP tool yet.