# Partner API terminal integration

The server-to-server surface is `https://api.lp.xyz/v1`. Your consumer needs
access switched on by the team, an `lpx_live_…` key and the required scopes.
Keep the key in your backend secret manager. Requesting access (Settings → API)
and the team's approval are separate from this data API.

## Run the terminal smoke

Use Node 20–22. Inject `LPX_API_KEY` from your secret manager; avoid shell tracing,
printing the environment, or committing credentials. The script sends the key only
in an HTTP header, refuses credential redirects, and reports request IDs.

Download [smoke-public-api.mjs](smoke-public-api.mjs) and
[prepare-signing.mjs](prepare-signing.mjs) into your working directory.

```sh
export LPX_API_URL=https://api.lp.xyz
node smoke-public-api.mjs

# With LPX_API_KEY already injected, optionally exercise an unsigned Base build:
export LPX_FROM=0x1111111111111111111111111111111111111111
export LPX_TO=0x2222222222222222222222222222222222222222
node smoke-public-api.mjs --build
```

The default checks metadata, then an authenticated price and Solana quote when a
key is supplied. `--build` also builds a Base transfer, repeats it with the same
idempotency key and saves `public-api-build.json` with private file permissions.
Use your own public wallet addresses for the deployed canary. The script never
signs or submits a transaction. Every call, including the identical builder
retry, is recorded against your key.

## Read → quote → build

These examples require curl and jq. `$LPX_API_KEY` must already be set. Review the
quote's route, slippage, fees and safety information before requesting a build.
Amounts on swap quotes/builds are **integer base-unit strings**. Transfer/bridge
and LP deposit amounts use **human token units**. Human amounts currently enter
numeric builders; strings that change during numeric conversion are rejected.
Use supported numeric precision and inspect the returned exact wire amounts.

```sh
curl --fail-with-body --silent --show-error "$LPX_API_URL/v1/chains" | jq .
curl --fail-with-body --silent --show-error "$LPX_API_URL/v1/openapi.json" > openapi.json

curl --fail-with-body --silent --show-error --get \
  "$LPX_API_URL/v1/quotes/swap" \
  -H "X-API-Key: $LPX_API_KEY" \
  --data-urlencode chain=solana \
  --data-urlencode inputMint=So11111111111111111111111111111111111111112 \
  --data-urlencode outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
  --data-urlencode amount=1000000 \
  --data-urlencode slippageBps=50 > quote.json

# SOLANA_WALLET is your signer's public base58 address. Preserve the complete raw quote.
jq --arg user "$SOLANA_WALLET" \
  '{chain:"solana",user:$user,quoteResponse:.raw}' quote.json > request.json
IDEMPOTENCY_KEY=$(node -e 'console.log(crypto.randomUUID())')
curl --fail-with-body --silent --show-error \
  "$LPX_API_URL/v1/build/swap" \
  -H "X-API-Key: $LPX_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @request.json > build.json
jq '{signing,metadata,quote,fees,transactions}' build.json
```

An EVM swap build supplies `chain,user,inputMint,outputMint,amount,slippageBps`.
It is requoted on the server. A reviewed `maxValueLossPpm` or
`v4SwapCostApproval` may be required; a `409` means review fresh conditions.
Never automatically acknowledge safety or value-loss fields.

LP requests supply `chain,venue,version,user` plus operation fields. For example:

```sh
jq -n --arg user "$LPX_FROM" --arg pool "$POOL_ADDRESS" \
  '{chain:"base",venue:"Uniswap",version:"CPMM v2",user:$user,pool:$pool,bps:2500}' \
  > remove.json
IDEMPOTENCY_KEY=$(node -e 'console.log(crypto.randomUUID())')
curl --fail-with-body --silent --show-error "$LPX_API_URL/v1/build/lp/remove" \
  -H "X-API-Key: $LPX_API_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' --data-binary @remove.json > build.json
```

`bps:2500` withdraws 25%; `10000` is full withdrawal. NFT operations use
`positionIds` instead of a fungible pool address. Use [the OpenAPI contract](https://api.lp.xyz/v1/openapi.json)
for operation fields and [the liquidity guide](https://lp.xyz/docs/liquidity)
for position flows; partner paths use
`/v1/build/lp/…`. Some pool creates are empty (Meteora DLMM, Orca, Raydium CLMM):
wait for the create receipt, take `metadata.poolId`, then separately build/sign
`/v1/build/lp/add` to seed liquidity. Review the returned `seeded` metadata where
available; pool creation alone does not promise a funded position.

## Signing handoff and confirmation

The key authorizes building. Your wallet controls spending and keeps its private
keys. Builders perform ownership checks where the protocol requires them and use
third-party fee policy; an API key does not attest ownership of arbitrary wallets.

The SDK's `signAndSend` method executes sequential builds. It verifies
the build's `signing.family`, wallet address and EVM chain, then receipt-gates
every prerequisite, waits for `payload.expect`, executes the fee-bearing router
action, records optional failures as `null`, and confirms ordered receipt
evidence with the API.

For viem local accounts and JSON-RPC/browser wallet clients:

```ts
import { createPublicClient, createWalletClient, http } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { createLpxyzClient } from "@lpxyz/sdk";
import { createViemSigner } from "@lpxyz/sdk/viem";

const lpxyz = createLpxyzClient({ apiKey: process.env.LPX_API_KEY });
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: base, transport: http(process.env.BASE_RPC_URL) });
const walletClient = createWalletClient({ account, chain: base, transport: http(process.env.BASE_RPC_URL) });

const build = await lpxyz.build.lp.add({
  chain: "base", venue: "Uniswap", version: "CPMM v2",
  user: account.address, pool: process.env.POOL_ADDRESS!, baseAmount: 0.1,
});

// Apply your transaction-policy review to build.data.transactions here.
const execution = await lpxyz.signAndSend(
  build.data,
  createViemSigner({ walletClient, publicClient, account }),
  { onProgress: ({ phase, transaction }) => console.log(phase, transaction.purpose) },
);
console.log(execution.signatures, execution.confirmation?.data);
```

Use `createEthersSigner({ signer })` from `@lpxyz/sdk/ethers` for ethers v6.
Both EVM adapters refresh the pending nonce and estimate gas immediately before
each send; set `nonce: "wallet"` when a smart account or provider owns nonce
selection.
Both adapters verify the connected wallet chain immediately before submission.

For Solana Keypairs or wallet-adapter compatible wallets:

```ts
import { Connection, Keypair } from "@solana/web3.js";
import { createSolanaKeypairSigner } from "@lpxyz/sdk/solana";

const connection = new Connection(process.env.SOLANA_RPC_URL!, "confirmed");
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.SOLANA_SECRET_KEY!)));
const execution = await lpxyz.signAndSend(
  build.data,
  createSolanaKeypairSigner({ connection, keypair }),
);
```

`createSolanaWalletSigner({ connection, wallet })` accepts the structural
`publicKey` + `signTransaction` interface used by browser wallet adapters. It
preserves existing server/cosigner partial signatures, verifies the wallet kept
the message intact, submits raw bytes, and confirms against the build's
blockhash strategy. A stale transaction requires a fresh build because replacing
the blockhash invalidates existing signatures.

Persist `LpxyzExecutionError.signatures` and `submittedSignature` when a flow is
interrupted. They identify every broadcast already made and support safe receipt
reconciliation before a new build.
Cancellation stops pending confirmation reads. An in-flight submission stays
awaited so its eventual signature can be retained and reconciled.

### Bundled Solana builds

For `signing.mode:sender-bundles`, use `decodeSolanaBundles` to inspect the
transaction groups and a bundle-aware custody executor. `signAndSend` rejects
this mode before requesting a signature. Sign each transaction once, preserve
partial signatures, and submit each `signing.bundles` transaction-ID group as one
Helius Sender bundle. Keep the original signed bytes and signatures; confirm
every transaction before advancing to the next part. After a timeout or uncertain
response, reconcile those same signatures with historical lookup after blockhash
expiry. Permit a fresh build only after the original outcome is established, and
recover the remaining deposit slices when earlier slices landed. Preserve the
original signed blockhash.

### Low-level custody integrations

Custom custody systems can implement the SDK's small `EvmSigner` or
`SolanaSigner` interface. The distributable terminal helper remains useful for
offline policy review:

Generate reviewable EVM transaction intents locally:

```sh
# PENDING_NONCE accepts the hex result of eth_getTransactionCount(your address,"pending").
node prepare-signing.mjs build.json "$LPX_FROM" "$PENDING_NONCE" \
  > signing-intents.json
```

This tool produces review data from a nonce snapshot. The broadcaster refreshes
state, supplies gas and fee parameters, submits in dependency order, and
persists each transaction hash before retrying an uncertain send.

After all required transactions land, save their signatures in the same order
as `transactions` (use `null` for a failed optional step). When a
`confirmationToken` is present:

```sh
jq --slurpfile signatures signatures.json \
  '{confirmationToken,signatures:$signatures[0]}' build.json > confirmation.json
curl --fail-with-body --silent --show-error "$LPX_API_URL/v1/build/confirm" \
  -H "X-API-Key: $LPX_API_KEY" -H 'Content-Type: application/json' \
  --data-binary @confirmation.json | jq .
```

A confirmation token is bound to the consumer, chain, returned fee rows/intents,
and transaction order. It lasts 15 minutes. Tokens authorize receipt verification;
chain evidence determines whether the fee is confirmed. Inspect `confirmed`,
`updated`, `pendingIds`, `pendingReason`, and `createFees` where returned. A `200`
with `confirmed:false`/`updated:false` means verification is still incomplete.
Receipt indexing/finality may require a retry. The general transaction status
endpoint checks a submitted hash/signature; bridge destination fulfillment should
be tracked with the Relay `metadata.bridgeRequestId` through your provider
integration (the public API currently exposes origin transaction status only).

## Retries, cancellation and errors

Builders require a 1–128 character `Idempotency-Key`. Keep the same key, identical
body bytes, and API key when retrying an uncertain HTTP build within ten minutes.
Changing the body returns `409 idempotency_conflict`; an in-flight request returns
`409 idempotency_pending`. Read/confirmation requests always undergo fresh
admission. A replay retains the original build's body requestId while the response
header identifies the current request. Cached builds can outlive Solana blockhash
or EVM quote validity: validate freshness before signing, and use a new key for a
new unsigned plan after reconciling any prior broadcasts.

Authenticated responses use `private,no-store`. `X-Request-Id` is generated by the
server. Admitted calls include `X-RateLimit-Limit` and `X-RateLimit-Remaining`,
admission-time snapshots that concurrent calls can change. Failed handlers keep
their rate-limit cost. Metadata and early denials may omit these headers.

Errors have `{ok:false,error,code,requestId}`. HTTP status remains authoritative.
Actionable handler metadata is retained as `endpointCode`, `reasons`,
`verification`, `riskAcknowledgement`, `reason`, `valueLossPpm`,
`highFeeLastResort`, `hookAddress`, `retryAfterSec`, `startPrice`,
`marketPrice`, `ratio`, or `maxFactor`. Unexpected provider errors
are replaced with a generic message; use the request ID with support.

| Status | Caller action |
| --- | --- |
| 400 | Correct JSON, chain, address, units or operation selectors. |
| 401 | Check the key, its revocation, and whether access is switched on. |
| 403 | Check scope, endpoint availability and source allowlist. |
| 408 / 413 | Upload within 15 seconds and 128 KiB. |
| 409 | Resolve replay conflict/pending, or explicitly review updated safety/quote. |
| 422 | Inspect simulation/signing prerequisite failure; refresh safely. |
| 429 | Honor Retry-After with jitter; allowance is shared across consumer keys. |
| 499 | Caller cancelled; provider work may still finish, and an admitted call keeps its rate-limit cost. |
| 502 / 503 / 504 | Retry with bounded backoff; keep the same builder key/body. |

Redis admission/settlement failures return `503` and fail closed. A reservation
whose final settlement fails is reclaimed by maintenance; the response may be
uncertain, so retry idempotently. Cancelling a build never broadcasts. Some
coalesced provider work may finish after a disconnect.

## Response completeness and endpoint coverage

OpenAPI lists all endpoint paths, scopes, query parameters and body shapes.
Read responses preserve the owning handler's JSON, including additive fields.
Portfolio rows may contain per-source health and completeness metadata. A healthy
source with `complete:false` is partial; a missing field retains legacy complete
semantics. Inspect every source's `ok` and `complete`, plus per-owner failure
fields. `200` alone establishes HTTP success, not complete discovery or a zero
balance. The public gateway forces JSON even when a caller requests NDJSON.

OpenAPI object schemas deliberately
allow additive read fields and venue-specific LP fields; exact response types
remain in their owning server modules. Runtime validators are authoritative for
venue availability and conditional LP requirements.
