The Business API docs sit behind a magic-link gate. Open the ValueMatch app, hit Settings → "Open Business API docs" — you'll be redirected back here with a valid token.
Requirements: KYC verified as "business" via Stripe Connect. Token valid for 10 minutes.
Business sellers can post, update, and cancel listings via signed HTTP requests. There is no separate API — the same event log the app writes to is publicly reachable. You sign events locally with your recovery key (BIP39 mnemonic) and POST them.
Base URL: https://api.valuematch.me
To publish a listing or any other action, sign a
SignedEvent envelope and POST it to:
POST /events
Content-Type: application/json
X-API-Key: <app api key>
New listings from non-trusted sellers return
HTTP 202 Accepted with a pendingId — the
listing goes through admin review before hitting the chain. Trusted
sellers (allow-listed by an admin) get a normal
200 OK with the ledger entry.
Every request body has this shape:
{
"type": "LISTING_CREATED",
"payload": { ... },
"actorPubkey": "<hex ed25519 public key, 64 chars>",
"actorSignature": "<hex ed25519 signature, 128 chars>",
"createdAt": 1748880000000,
"nonce": "<hex, 32 chars>"
}
The signature covers the SHA-256 of the canonical JSON of
{ actorPubkey, createdAt, nonce, payload, type } —
keys sorted alphabetically, no whitespace, undefined
values omitted. See the Node example below for a reference
implementation.
| Field | Type | Required | Notes |
|---|---|---|---|
auctionId | string | yes | your-chosen unique id |
title | string | yes | headline |
description | string | no | up to 280 chars |
tags | string[] | yes | at least one |
startingPrice | number | yes | EUR, integer |
reservePrice | number | yes | EUR, below starting |
decayAmount | number | yes | EUR per decayUnit |
decayUnit | string | yes | minute / hour / day |
quantity | number | yes | currently must be 1 |
imageUrl | string | yes | uploaded via POST /uploads |
shippingCost | number | no | EUR; required if not pickup-only |
localPickup | boolean | no | defaults to false |
pickupLocation | string | no | shown when localPickup is on |
postalCode | string | no | buyer-visible item location |
purchasedAt | number | no | UTC ms timestamp; renders as item age |
conditionKey | string | no | one of new, like_new, very_good, good, acceptable, for_parts |
sellerName | string | no | display name buyers see |
sellerType | string | yes | set to "business" |
paymentIntentId | string | yes | Stripe PaymentIntent holding 10% of starting price |
Uses @noble/ed25519, @noble/hashes, and
@scure/bip39. Install:
npm i @noble/ed25519 @noble/hashes @scure/bip39
import * as ed from '@noble/ed25519';
import { sha256 } from '@noble/hashes/sha256';
import { mnemonicToSeedSync } from '@scure/bip39';
const BASE = 'https://api.valuematch.me';
const API_KEY = process.env.VALUEMATCH_API_KEY;
const MNEMONIC = process.env.VALUEMATCH_MNEMONIC; // 12-word recovery phrase
const toHex = (b) => Buffer.from(b).toString('hex');
const fromHex = (s) => Buffer.from(s, 'hex');
// Derive the same Ed25519 keypair the app uses
const seed = mnemonicToSeedSync(MNEMONIC).slice(0, 32);
const publicKey = await ed.getPublicKey(seed);
const PUBKEY_HEX = toHex(publicKey);
// Canonical JSON: keys sorted, no whitespace, undefined dropped
const canonical = (v) => {
if (v === null || typeof v !== 'object') return JSON.stringify(v);
if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']';
const keys = Object.keys(v).filter((k) => v[k] !== undefined).sort();
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
};
async function signEvent(type, payload) {
const createdAt = Date.now();
const nonce = toHex(crypto.getRandomValues(new Uint8Array(16)));
const digest = sha256(new TextEncoder().encode(
canonical({ actorPubkey: PUBKEY_HEX, createdAt, nonce, payload, type })
));
const signature = await ed.sign(digest, seed);
return {
type, payload,
actorPubkey: PUBKEY_HEX,
actorSignature: toHex(signature),
createdAt, nonce,
};
}
async function createListing(payload) {
const envelope = await signEvent('LISTING_CREATED', payload);
const res = await fetch(BASE + '/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
body: JSON.stringify(envelope),
});
return { status: res.status, body: await res.json() };
}
The imageUrl field must point at a URL the backend
accepts. Upload via:
POST /uploads
Content-Type: multipart/form-data
X-API-Key: <app api key>
(file=<your-image>)
→ { "url": "...", "sha256": "...", "size": 12345, "contentType": "image/jpeg" }
The same envelope structure applies to:
LISTING_UPDATED — change a listing before it is engaged (no bids, no standing limits).LISTING_CANCELLED — pull a listing. Triggers seller-commitment refund or capture depending on engagement.SHIPMENT_MARKED — mark a sold item as shipped (payload.trackingCode).To list your pending (in-review) submissions, sign a GET request:
GET /listings/mine/pending
X-API-Key: <app api key>
X-Identity-Pubkey: <hex pubkey>
X-Identity-Timestamp: <unix ms>
X-Identity-Signature: <hex sig of sha256(canonical({ method, path, timestamp }))>
POST /events — 60 requests/minute.400 — payload validation failed.403 — signature didn’t match the actor pubkey.202 — listing queued for admin review.By default every new listing goes through manual review. If you’re publishing in volume and want listings to go live immediately, contact up@pries.me from the email address on your business profile and we’ll add your pubkey to the allow-list.