From 1b433f830105dde95d7077e9c9b366cf6d90896a Mon Sep 17 00:00:00 2001 From: Vladimir Rogojin Date: Thu, 11 Jun 2026 10:16:12 +0200 Subject: [PATCH] fix(market): drop structured price field from trader's market posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The market-api at market-api.unicity.network is a semantic-search database — counterparties discover us by description matching. The trader was sending a redundant structured `price` field at the top of the request body, and the deployed server hit an unhandled exception on it (HTTP 500 with HTML body, swallowed by MarketModule's parseResponse to an opaque "unexpected response (not JSON)"). The trader's own counterparty-discovery already parses the description (parseDescription extracts rate_min/rate_max/volume_min/volume_max fields per TIP-0), so emitting a separate single-point price was useless even when the server accepted it. Successful posts in the live feed (@alphasentinel0X, @gulungtikar990, @w025ixd) all omit the field. Changes: - intent-engine.ts:904: drop the `price: midpointRate.toString()` argument; the description carries the full rate band. - intent-engine.ts:896: drop the now-unused `midpointRate` calc. - types.ts MarketPostRequest.price: required → optional, with a doc block explaining when (not) to use it. Tests: - e2e/trader-intent-lifecycle.e2e.test.ts T1.1: assertion changed from `postCall.price === '475'` to `postCall.price === undefined`. Description assertions unchanged — the rate band is still asserted to round-trip through encodeDescription. - 698/698 test suite passes. Surfaced by: unicity-sphere/sphere-sdk#475 (trader-roundtrip soak) §6. Final root cause from a chain of three: (1) Number() precision loss fixed in PR #25, (2) types out of step with SDK's bigint-string convention fixed via sphere-sdk PR #483 + this PR's earlier commits, and (3) the actual server-side schema mismatch — the field shouldn't have been there at all. This commit ships the (3) fix. Related: sphere-sdk PR #483 (SDK side type changes — already merged). --- src/trader/intent-engine.ts | 28 +++++++++++--------- src/trader/types.ts | 16 +++++------ test/e2e/trader-intent-lifecycle.e2e.test.ts | 10 ++++--- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/trader/intent-engine.ts b/src/trader/intent-engine.ts index c362cef..1bfc8fd 100644 --- a/src/trader/intent-engine.ts +++ b/src/trader/intent-engine.ts @@ -891,18 +891,23 @@ export function createIntentEngine(deps: IntentEngineDeps): IntentEngine { intents.set(intentId, draftRecord); // Transition DRAFT → ACTIVE (validates transition) - // Build description for market posting + // Build description for market posting. + // + // The market-api is a semantic-search database: counterparties + // discover us by matching the description. We do NOT send a + // structured `price` field — the description embeds all the + // structured rate/volume info already (encodeDescription is the + // canonical TIP-0 form, parseDescription extracts the fields on + // the receiving side). Sending price separately is redundant AND + // hits an unhandled exception in the deployed market-api server + // (HTTP 500 with HTML body) — sphere-sdk#475 §6 surfaced this. + // + // category + currency are still sent because they're useful + // semantic tags for the search index (and the server accepts + // strings there without issue). The `price` field stays + // OPTIONAL in PostIntentRequest for callers with a separate + // numeric pricing flow — the trader doesn't. const description = encodeDescription(tradingIntent); - // Bigint midpoint of the rate band, serialized to a decimal - // string for the wire. NEVER cast to Number — for a typical - // 18-decimal quote asset, the midpoint of a 0.08–0.12 band is - // 1e17 which is past Number.MAX_SAFE_INTEGER (≈9e15). Coercing - // loses precision, the market-api server rejects with HTTP 500, - // and the trader logs only an opaque status code. The SDK's - // PostIntentRequest.price field is `string` for exactly this - // reason (see sphere-sdk PR #483); pass `.toString()` through - // and the JSON serialization is correct end-to-end. - const midpointRate = (rateMin + rateMax) / 2n; const expiresInDays = Math.max( 1, Math.ceil((expiryMs - nowMs()) / 86_400_000), @@ -914,7 +919,6 @@ export function createIntentEngine(deps: IntentEngineDeps): IntentEngine { description, intentType: params.direction, category: `${params.base_asset}/${params.quote_asset}`, - price: midpointRate.toString(), currency: params.quote_asset, // Prefer @nametag for contactHandle — it resolves faster than DIRECT:// // which requires a binding event lookup that may not have propagated yet. diff --git a/src/trader/types.ts b/src/trader/types.ts index 008548e..bd878ea 100644 --- a/src/trader/types.ts +++ b/src/trader/types.ts @@ -269,15 +269,15 @@ export interface MarketPostRequest { readonly intentType: 'buy' | 'sell'; readonly category: string; /** - * Price as a decimal-string bigint in the quote currency's smallest - * units. Same convention as token amounts in sphere-sdk (TXF amount - * fields, transfer payloads): internally bigint, over the wire - * decimal-string. Avoids JavaScript's `Number` precision loss for - * values above 2^53 (an 18-decimal coin hits that ceiling around - * 0.09 in human units). See sphere-sdk PR #483 for the SDK-side - * type that aligns with this. + * Optional structured price hint (decimal-string bigint in the + * quote currency's smallest units). The trader does NOT set this — + * the market-api is a semantic-search database, so we let the + * description (which encodes the full rate band per TIP-0) be the + * source of truth. `price` stays in the shape for callers that have + * a single-point pricing use case, but emitting it from the trader + * is both redundant and trips the deployed market-api server. */ - readonly price: string; + readonly price?: string; readonly currency: string; readonly contactHandle: string; readonly expiresInDays: number; diff --git a/test/e2e/trader-intent-lifecycle.e2e.test.ts b/test/e2e/trader-intent-lifecycle.e2e.test.ts index a15f29c..3078291 100644 --- a/test/e2e/trader-intent-lifecycle.e2e.test.ts +++ b/test/e2e/trader-intent-lifecycle.e2e.test.ts @@ -249,10 +249,12 @@ describe('E2E: Trader Intent Lifecycle (T1 + T10)', () => { expect(postCall.category).toBe('ALPHA/USDC'); expect(postCall.currency).toBe('USDC'); expect(postCall.contactHandle).toBe(AGENT_ADDRESS); - // Midpoint of 450-500 = 475, serialized as decimal-string bigint - // per sphere-sdk PostIntentRequest.price convention (no Number - // cast — see trader/intent-engine.ts comment block). - expect(postCall.price).toBe('475'); + // No structured `price` field — the trader leans on the + // semantic-search engine in the deployed market-api. The + // description (asserted below) already carries the rate band + // in TIP-0 form, so a structured price hint would be redundant + // (and trips the server). See intent-engine.ts comment block. + expect(postCall.price).toBeUndefined(); // Description must match canonical format expect(postCall.description).toContain('Selling 100-1000 ALPHA for USDC'); expect(postCall.description).toContain('Rate: 450-500 USDC per ALPHA');