Perps DEX Aggregator

AI suggestions, pay-as-you-go

Several AI providers answer one request, the engine merges them into a single trade idea, and you pay only per call.

The app can ask AI for a trade idea. What makes this different from a single chatbot call is that it asks several providers at once, merges their answers into one result, and charges you only for the calls it actually makes. This page covers the idea, the merge logic, and the payment model that keeps it honest.

What it means for you

You open the suggestion sheet, type a symbol, and optionally add some shape to the ask: a style, a strategy, how much margin you have in mind, and a leverage. You do not pick long or short. Direction is something the engine tells you, not something you ask for.

When you submit, the engine asks every AI provider it knows about, in parallel, and blends their answers. You get back one of three results:

  • A directional idea: long or short, with an entry, a stop-loss, a take-profit, a confidence score, and the reasoning behind it. You can push it straight into the order ticket, where you review and confirm it. Nothing is ever placed automatically.
  • A delta-neutral result: the providers disagreed on direction strongly enough that no side won. Acting on that as a market-neutral position is a planned feature, so for now it shows as "coming soon".
  • A no-trade result: the providers agree there is no setup worth taking.

You pay per call, from your Agent Wallet, and only when the engine actually has to call a paid provider. If you ask the same question twice in a short window, the second answer comes from a cache and costs nothing.

Asking several providers at once

Each AI provider plugs in behind a small port, the same way each exchange plugs in behind the Adapter. A provider returns a neutral suggestion shape, and the only required fields are deliberately tiny, so a provider that emits nothing but a direction and a confidence can still take part:

// apps/server/src/suggestions/domain/suggestion.types.ts
export type ProviderSuggestion = {
  readonly providerId: string;
  readonly side: 'long' | 'short' | 'neutral';
  readonly confidence: number; // 0–100
  readonly entryPrice?: number;
  readonly stopLossPrice?: number;
  readonly takeProfitPrice?: number;
  readonly reasons?: readonly string[];
  readonly risks?: readonly string[];
};

The request a provider is asked to price is just as small. The symbol is the only required input. Critically, there is no side field, because side is an output:

export type SuggestionInput = {
  readonly actor: AuthenticatedUser;
  readonly symbol: string;
  readonly style?: string;
  readonly strategy?: string;
  readonly marginUsd?: number;
  readonly leverage?: number;
};

There are two providers today. A fee-free mock provider always takes part, which means the merge logic, the agreement maths, and the delta-neutral path can be exercised even when only one paid provider exists. The minara provider is the first real one, and it charges per call.

How the answers get merged

The engine does not just pick the most confident provider. It ensembles them, field by field, over the side that wins a confidence-weighted vote:

  • Side is decided by a confidence-weighted vote across the providers that returned a real direction. If a clear winner emerges, the outcome is directional. If the split is too even, the outcome is delta-neutral. If nobody offered a direction, it is no-trade.
  • Entry price is the confidence-weighted mean of the winning-side providers that supplied one.
  • Stop-loss and take-profit take the conservative value: the tightest stop and the nearest target across the winning side. Caution wins ties.
  • Confidence is the mean confidence of the winning side, scaled down by how much agreement there actually was. A narrow majority produces a lower headline confidence than a unanimous one.
  • Reasons and risks are unioned and attributed, so the breakdown shows which provider said what.

The result the client receives is one outcome plus a full per-provider breakdown:

// apps/server/src/suggestions/domain/engine-outcome.types.ts
export type EngineOutcome =
  | { kind: 'directional'; side: 'long' | 'short'; suggestion: EnsembledSuggestion }
  | { kind: 'delta-neutral'; comingSoon: true }
  | { kind: 'no-trade' };

export type EngineResult = {
  readonly outcome: EngineOutcome;
  readonly breakdown: readonly ProviderBreakdownEntry[];
};

The breakdown is always present, even for delta-neutral and no-trade, so the UI can always show who voted what. A provider that fails appears in the breakdown as failed rather than sinking the whole request. One slow or broken provider does not deny you an answer from the rest.

Three kinds of neutral, kept apart

A single provider can return its own neutral vote. The engine can return delta-neutral when providers conflict on direction. It can return no-trade when providers agree there is nothing there. These are three different things, and only a directional outcome ever prefills an order.

Pay-as-you-go, and the gate that protects you

Paid AI calls are settled from your Agent Wallet in USDC, using the x402 payment standard. The wallet holds funds across Base and Polygon, and a call is routed to whichever chain has the funds for it. The balance you spend here is the Agent Balance, and it is completely separate from your trading collateral.

Before the engine pays for anything, it validates the request. This validate-before-pay gate is the rule that means a bad request never costs you money:

  1. The symbol must be listed on the target venue.
  2. The leverage must be at or under the cap.
  3. Your Agent Balance must be enough to cover the call.

If any check fails, the server returns 422 with a SUGGESTION_INPUT_INVALID code and per-field reasons: which symbol, which leverage cap, the exact balance against the call price. No provider is called, so nothing is charged.

The other half of the cost story is the cache. The engine keeps a suggestion_history table that does double duty as a freshness cache and a history log. When a request comes in, the engine hashes its parameters and looks for a fresh row:

  • A cache hit within the freshness window returns the stored result and skips the provider fan-out entirely. No call, no charge.
  • A cache miss fans out, merges, and then stores the result with the exact USD cost it paid and the freshness window. That same row later answers a repeat question for free and shows up in your history.

Because the cache and the history are one table, the cost you were charged and the answer you got can never drift apart. Every paid call leaves exactly one auditable row.

What happens to a directional answer

A directional result can flow into the order ticket, but only with your hand on it. When you choose "use this suggestion", the app prefills the side, the limit entry, and the take-profit and stop-loss through an order-intent bus, and applies the chosen leverage through the venue's leverage controller. Then it stops and waits for you to confirm. The suggestion carries no venue, no size, and no order type of its own; the trading layer derives those against whichever venue is live. Delta-neutral and no-trade prefill nothing.

Under the hood

  • The engine and providers live in apps/server/src/suggestions/. It owns the SuggestionProvider port, the provider registry, the ensemble merge, the validate-before-pay gate, and the suggestion_history table.
  • The payment adapter lives in apps/server/src/minara-client/. Its quotePrice reads the live price from the provider's 402 response, and payAndCall drives the x402 request, sign, retry loop using the delegated signer described in self-custody.
  • The wallet that pays is the Agent Wallet in apps/server/src/agent-treasury/, which also exposes the balance read the gate uses.
  • The client surface is the perp-suggestion sheet in apps/client/src/modules/trading/, plus the agent-balance module that shows your balance and runs deposit, withdraw, and delegation consent.

Routes

Method and pathWhat it does
POST /api/suggestionsValidate, check the cache, fan out and pay on a miss, return one merged result plus the breakdown.
GET /api/suggestions/historyList your stored suggestions, newest first, from the same table the cache uses.

The request

// apps/server/src/suggestions/suggestions.dto.ts
export const getSuggestionSchema = z.object({
  symbol: z.string().min(1),
  style: z.string().min(1).optional(),
  strategy: z.string().min(1).optional(),
  marginUsd: z.number().positive().optional(),
  leverage: z.number().positive().optional(),
});

The acting user comes from the verified JWT, never from the body, so one user can never request or read another user's suggestions.

Design records

The provider-agnostic suggestion engine and its ensemble rules (ADR-0045), x402 payments through a self-custodial delegated wallet (ADR-0044), and the cross-chain rebalancing of the Agent Balance over CCTP (ADR-0046).