Skip to content

Commit 59900e8

Browse files
authored
feat: add API endpoints for partners (cowprotocol#809)
1 parent 93d805f commit 59900e8

6 files changed

Lines changed: 177 additions & 35 deletions

File tree

packages/config/src/types/configs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ export type ApiBaseUrls = Record<SupportedChainId, string>
6666
* @property {SupportedChainId} chainId The `chainId`` corresponding to this CoW Protocol API instance.
6767
* @property {CowEnv} env The environment that this context corresponds to.
6868
* @property {ApiBaseUrls} [baseUrls] URls that may be used to connect to this context.
69+
* @property {string} [apiKey] API key for Partner API (partners.cow.fi). When set, requests use the partner gateway and include X-API-Key header.
6970
*/
7071
export interface ApiContext {
7172
chainId: SupportedChainId
7273
env: CowEnv
7374
baseUrls?: ApiBaseUrls
75+
apiKey?: string
7476
limiterOpts?: RateLimiterOpts
7577
backoffOpts?: BackoffOptions
7678
}

packages/contracts-ts/src/api.ts

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,27 @@ export enum Environment {
1010

1111
export const LIMIT_CONCURRENT_REQUESTS = 5
1212

13-
export function apiUrl(environment: Environment, network: string): string {
13+
const PARTNER_PROD_BASE_URL = 'https://partners.cow.fi'
14+
const PARTNER_DEV_BASE_URL = 'https://partners.barn.cow.fi'
15+
16+
export function apiUrl(environment: Environment, network: string, apiKey?: string): string {
17+
let baseUrl: string
1418
switch (environment) {
1519
case Environment.Dev:
16-
return `https://barn.api.cow.fi/${network}`
20+
baseUrl = apiKey ? PARTNER_DEV_BASE_URL : 'https://barn.api.cow.fi'
21+
break
1722
case Environment.Prod:
18-
return `https://api.cow.fi/${network}`
23+
baseUrl = apiKey ? PARTNER_PROD_BASE_URL : 'https://api.cow.fi'
24+
break
1925
default:
2026
throw new Error('Invalid environment')
2127
}
28+
return `${baseUrl}/${network}`
2229
}
2330

2431
export interface ApiCall {
2532
baseUrl: string
33+
apiKey?: string
2634
}
2735

2836
export interface EstimateTradeAmountQuery {
@@ -126,9 +134,18 @@ function apiSigningScheme(scheme: SigningScheme): string {
126134
}
127135
}
128136

129-
async function call<T>(route: string, baseUrl: string, init?: RequestInit): Promise<T> {
137+
async function call<T>(
138+
route: string,
139+
baseUrl: string,
140+
init?: RequestInit,
141+
apiKey?: string,
142+
): Promise<T> {
130143
const url = `${baseUrl}/api/v1/${route}`
131-
const response = await fetch(url, init)
144+
const headers: Record<string, string> = {
145+
...((init?.headers as Record<string, string>) ?? {}),
146+
...(apiKey ? { 'X-API-Key': apiKey } : {}),
147+
}
148+
const response = await fetch(url, { ...init, headers })
132149
const body = await response.text()
133150
if (!response.ok) {
134151
const error: CallError = new Error(
@@ -150,6 +167,7 @@ async function estimateTradeAmount({
150167
kind,
151168
amount,
152169
baseUrl,
170+
apiKey,
153171
}: EstimateTradeAmountQuery & ApiCall): Promise<BigIntish> {
154172
const side: BuyAmountAfterFee | SellAmountAfterFee =
155173
kind == OrderKind.SELL
@@ -162,7 +180,7 @@ async function estimateTradeAmount({
162180
buyAmountAfterFee: amount,
163181
}
164182
const { quote } = await getQuote(
165-
{ baseUrl },
183+
{ baseUrl, apiKey },
166184
{
167185
from: '0x0000000000000000000000000000000000000000',
168186
sellToken,
@@ -182,10 +200,19 @@ async function estimateTradeAmount({
182200
return getGlobalAdapter().utils.toBigIntish(estimatedAmount)
183201
}
184202

185-
async function placeOrder({ order, signature, baseUrl, from }: PlaceOrderQuery & ApiCall): Promise<string> {
203+
async function placeOrder({
204+
order,
205+
signature,
206+
baseUrl,
207+
from,
208+
apiKey,
209+
}: PlaceOrderQuery & ApiCall): Promise<string> {
186210
const normalizedOrder = normalizeOrder(order)
187211
const adapter = getGlobalAdapter()
188-
return await call('orders', baseUrl, {
212+
return await call(
213+
'orders',
214+
baseUrl,
215+
{
189216
method: 'post',
190217
body: JSON.stringify({
191218
sellToken: normalizedOrder.sellToken,
@@ -203,15 +230,24 @@ async function placeOrder({ order, signature, baseUrl, from }: PlaceOrderQuery &
203230
from,
204231
}),
205232
headers: { 'Content-Type': 'application/json' },
206-
})
233+
},
234+
apiKey,
235+
)
207236
}
208237

209-
async function getExecutedSellAmount({ uid, baseUrl }: GetExecutedSellAmountQuery & ApiCall): Promise<BigIntish> {
210-
const response: OrderDetailResponse = await call(`orders/${uid}`, baseUrl)
238+
async function getExecutedSellAmount({
239+
uid,
240+
baseUrl,
241+
apiKey,
242+
}: GetExecutedSellAmountQuery & ApiCall): Promise<BigIntish> {
243+
const response: OrderDetailResponse = await call(`orders/${uid}`, baseUrl, undefined, apiKey)
211244
return getGlobalAdapter().utils.toBigIntish(response.executedSellAmount)
212245
}
213246

214-
async function getQuote({ baseUrl }: ApiCall, quote: QuoteQuery): Promise<GetQuoteResponse> {
247+
async function getQuote(
248+
{ baseUrl, apiKey }: ApiCall,
249+
quote: QuoteQuery,
250+
): Promise<GetQuoteResponse> {
215251
// Convert BigNumber into JSON strings (native serialisation is a hex object)
216252
if ((<SellAmountBeforeFee>quote).sellAmountBeforeFee) {
217253
;(<SellAmountBeforeFee>quote).sellAmountBeforeFee = String((<SellAmountBeforeFee>quote).sellAmountBeforeFee)
@@ -222,30 +258,35 @@ async function getQuote({ baseUrl }: ApiCall, quote: QuoteQuery): Promise<GetQuo
222258
if ((<BuyAmountAfterFee>quote).buyAmountAfterFee) {
223259
;(<BuyAmountAfterFee>quote).buyAmountAfterFee = String((<BuyAmountAfterFee>quote).buyAmountAfterFee)
224260
}
225-
return call('quote', baseUrl, {
226-
method: 'post',
227-
headers: { 'Content-Type': 'application/json' },
228-
body: JSON.stringify(quote),
229-
})
261+
return call(
262+
'quote',
263+
baseUrl,
264+
{
265+
method: 'post',
266+
headers: { 'Content-Type': 'application/json' },
267+
body: JSON.stringify(quote),
268+
},
269+
apiKey,
270+
)
230271
}
231272

232273
export class Api {
233274
network: string
234275
baseUrl: string
276+
apiKey?: string
235277

236-
constructor(network: string, baseUrlOrEnv: string | Environment) {
278+
constructor(network: string, baseUrlOrEnv: string | Environment, apiKey?: string) {
237279
this.network = network
238-
let baseUrl
280+
this.apiKey = apiKey
239281
if (typeof baseUrlOrEnv === 'string') {
240-
baseUrl = baseUrlOrEnv
282+
this.baseUrl = baseUrlOrEnv
241283
} else {
242-
baseUrl = apiUrl(baseUrlOrEnv, network)
284+
this.baseUrl = apiUrl(baseUrlOrEnv, network, apiKey)
243285
}
244-
this.baseUrl = baseUrl
245286
}
246287

247288
private apiCallParams() {
248-
return { network: this.network, baseUrl: this.baseUrl }
289+
return { network: this.network, baseUrl: this.baseUrl, apiKey: this.apiKey }
249290
}
250291

251292
async estimateTradeAmount(query: EstimateTradeAmountQuery): Promise<BigIntish> {

packages/order-book/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,39 @@ const orderBookApi = new OrderBookApi({
121121
})
122122
```
123123

124+
### Partner API (Authenticated Access)
125+
126+
Partners can use the Partner API for authenticated, rate-limited access with higher request quotas. Provide your API key and the SDK will automatically route requests through the partner gateway and attach the `X-API-Key` header.
127+
128+
| Environment | Partner API Base URL |
129+
|-------------|---------------------|
130+
| Production | `https://partners.cow.fi` |
131+
| Staging | `https://partners.barn.cow.fi` |
132+
133+
```typescript
134+
const orderBookApi = new OrderBookApi({
135+
chainId: SupportedChainId.MAINNET,
136+
apiKey: 'your-partner-api-key',
137+
})
138+
```
139+
140+
To use the Partner API with the Trading SDK, pass a configured `OrderBookApi` instance:
141+
142+
```typescript
143+
import { TradingSdk } from '@cowprotocol/sdk-trading'
144+
145+
const orderBookApi = new OrderBookApi({
146+
chainId: SupportedChainId.MAINNET,
147+
apiKey: 'your-partner-api-key',
148+
})
149+
150+
const sdk = new TradingSdk(
151+
{ chainId: SupportedChainId.MAINNET, appCode: 'YOUR_APP_CODE' },
152+
{ orderBookApi },
153+
adapter,
154+
)
155+
```
156+
124157
### Rate Limiting Configuration
125158

126159
```typescript

packages/order-book/src/api.ts

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { RateLimiter } from 'limiter'
33
import {
44
ApiBaseUrls,
55
ApiContext,
6-
CowEnv,
76
DEFAULT_COW_API_CONTEXT,
87
ENVS_LIST,
98
PartialApiContext,
@@ -33,6 +32,8 @@ import { EnrichedOrder } from './types'
3332

3433
const PROD_BASE_URL = 'https://api.cow.fi'
3534
const STAGING_BASE_URL = 'https://barn.api.cow.fi'
35+
const PARTNER_PROD_BASE_URL = 'https://partners.cow.fi'
36+
const PARTNER_STAGING_BASE_URL = 'https://partners.barn.cow.fi'
3637

3738
/**
3839
* An object containing *production* environment base URLs for each supported `chainId`.
@@ -71,6 +72,46 @@ export const ORDER_BOOK_STAGING_CONFIG: ApiBaseUrls = {
7172
[SupportedChainId.INK]: `${STAGING_BASE_URL}/ink`,
7273
}
7374

75+
/**
76+
* An object containing *partner production* environment base URLs for each supported `chainId`.
77+
* Used when apiKey is set; requests include X-API-Key header.
78+
* @see {@link https://partners.cow.fi}
79+
*/
80+
export const ORDER_BOOK_PARTNER_PROD_CONFIG: ApiBaseUrls = {
81+
[SupportedChainId.MAINNET]: `${PARTNER_PROD_BASE_URL}/mainnet`,
82+
[SupportedChainId.GNOSIS_CHAIN]: `${PARTNER_PROD_BASE_URL}/xdai`,
83+
[SupportedChainId.ARBITRUM_ONE]: `${PARTNER_PROD_BASE_URL}/arbitrum_one`,
84+
[SupportedChainId.BASE]: `${PARTNER_PROD_BASE_URL}/base`,
85+
[SupportedChainId.SEPOLIA]: `${PARTNER_PROD_BASE_URL}/sepolia`,
86+
[SupportedChainId.POLYGON]: `${PARTNER_PROD_BASE_URL}/polygon`,
87+
[SupportedChainId.AVALANCHE]: `${PARTNER_PROD_BASE_URL}/avalanche`,
88+
[SupportedChainId.LENS]: `${PARTNER_PROD_BASE_URL}/lens`,
89+
[SupportedChainId.BNB]: `${PARTNER_PROD_BASE_URL}/bnb`,
90+
[SupportedChainId.LINEA]: `${PARTNER_PROD_BASE_URL}/linea`,
91+
[SupportedChainId.PLASMA]: `${PARTNER_PROD_BASE_URL}/plasma`,
92+
[SupportedChainId.INK]: `${PARTNER_PROD_BASE_URL}/ink`,
93+
}
94+
95+
/**
96+
* An object containing *partner staging* environment base URLs for each supported `chainId`.
97+
* Used when apiKey is set and env is staging; requests include X-API-Key header.
98+
* @see {@link https://partners.barn.cow.fi}
99+
*/
100+
export const ORDER_BOOK_PARTNER_STAGING_CONFIG: ApiBaseUrls = {
101+
[SupportedChainId.MAINNET]: `${PARTNER_STAGING_BASE_URL}/mainnet`,
102+
[SupportedChainId.GNOSIS_CHAIN]: `${PARTNER_STAGING_BASE_URL}/xdai`,
103+
[SupportedChainId.ARBITRUM_ONE]: `${PARTNER_STAGING_BASE_URL}/arbitrum_one`,
104+
[SupportedChainId.BASE]: `${PARTNER_STAGING_BASE_URL}/base`,
105+
[SupportedChainId.SEPOLIA]: `${PARTNER_STAGING_BASE_URL}/sepolia`,
106+
[SupportedChainId.POLYGON]: `${PARTNER_STAGING_BASE_URL}/polygon`,
107+
[SupportedChainId.AVALANCHE]: `${PARTNER_STAGING_BASE_URL}/avalanche`,
108+
[SupportedChainId.LENS]: `${PARTNER_STAGING_BASE_URL}/lens`,
109+
[SupportedChainId.BNB]: `${PARTNER_STAGING_BASE_URL}/bnb`,
110+
[SupportedChainId.LINEA]: `${PARTNER_STAGING_BASE_URL}/linea`,
111+
[SupportedChainId.PLASMA]: `${PARTNER_STAGING_BASE_URL}/plasma`,
112+
[SupportedChainId.INK]: `${PARTNER_STAGING_BASE_URL}/ink`,
113+
}
114+
74115
function cleanObjectFromUndefinedValues(obj: Record<string, string>): typeof obj {
75116
return Object.keys(obj).reduce(
76117
(acc, key) => {
@@ -419,8 +460,8 @@ export class OrderBookApi {
419460
* @returns The API endpoint to get the order.
420461
*/
421462
getOrderLink(orderUid: UID, contextOverride?: PartialApiContext): string {
422-
const { chainId, env } = this.getContextWithOverride(contextOverride)
423-
return this.getApiBaseUrls(env)[chainId] + `/api/v1/orders/${orderUid}`
463+
const context = this.getContextWithOverride(contextOverride ?? {})
464+
return this.getApiBaseUrls(context)[context.chainId] + `/api/v1/orders/${orderUid}`
424465
}
425466

426467
/**
@@ -433,14 +474,17 @@ export class OrderBookApi {
433474
}
434475

435476
/**
436-
* Get the base URLs for the API endpoints given the environment.
437-
* @param env The environment to get the base URLs for.
477+
* Get the base URLs for the API endpoints given the context.
478+
* Uses partner URLs when apiKey is set (and no custom baseUrls override).
479+
* @param context The merged API context for the request.
438480
* @returns The base URLs for the API endpoints.
439481
*/
440-
private getApiBaseUrls(env: CowEnv): ApiBaseUrls {
441-
if (this.context.baseUrls) return this.context.baseUrls
442-
443-
return env === 'prod' ? ORDER_BOOK_PROD_CONFIG : ORDER_BOOK_STAGING_CONFIG
482+
private getApiBaseUrls(context: ApiContext): ApiBaseUrls {
483+
if (context.baseUrls) return context.baseUrls
484+
if (context.apiKey) {
485+
return context.env === 'prod' ? ORDER_BOOK_PARTNER_PROD_CONFIG : ORDER_BOOK_PARTNER_STAGING_CONFIG
486+
}
487+
return context.env === 'prod' ? ORDER_BOOK_PROD_CONFIG : ORDER_BOOK_STAGING_CONFIG
444488
}
445489

446490
/**
@@ -450,13 +494,15 @@ export class OrderBookApi {
450494
* @returns The response from the API.
451495
*/
452496
private fetch<T>(params: FetchParams, contextOverride: PartialApiContext = {}): Promise<T> {
453-
const { chainId, env, backoffOpts: _backoffOpts } = this.getContextWithOverride(contextOverride)
454-
const baseUrl = this.getApiBaseUrls(env)[chainId]
497+
const context = this.getContextWithOverride(contextOverride)
498+
const { chainId, backoffOpts: _backoffOpts, apiKey } = context
499+
const baseUrl = this.getApiBaseUrls(context)[chainId]
455500
const backoffOpts = _backoffOpts || DEFAULT_BACKOFF_OPTIONS
456501
const rateLimiter = contextOverride.limiterOpts ? new RateLimiter(contextOverride.limiterOpts) : this.rateLimiter
502+
const additionalHeaders = apiKey ? { 'X-API-Key': apiKey } : undefined
457503

458504
log(`Fetching OrderBook API: ${baseUrl}${params.path}. Params: ${JSON.stringify(params, jsonWithBigintReplacer)}`)
459505

460-
return request(baseUrl, params, rateLimiter, backoffOpts)
506+
return request(baseUrl, params, rateLimiter, backoffOpts, additionalHeaders)
461507
}
462508
}

packages/order-book/src/request.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const getResponseBody = async (response: Response): Promise<unknown> => {
101101
* @param body The body of the request.
102102
* @param rateLimiter The rate limiter to use.
103103
* @param backoffOpts The backoff options to use.
104+
* @param additionalHeaders Optional additional headers (e.g. X-API-Key for Partner API).
104105
* @returns The response of the request.
105106
* @throws If the API returns an error or if the request fails.
106107
*/
@@ -109,11 +110,13 @@ export async function request<T>(
109110
{ path, query, method, body }: FetchParams,
110111
rateLimiter: RateLimiter,
111112
backoffOpts: BackoffOptions,
113+
additionalHeaders?: Record<string, string>,
112114
): Promise<T> {
113115
const queryString = query ? '?' + query : ''
114116
const headers = {
115117
Accept: 'application/json',
116118
'Content-Type': 'application/json',
119+
...additionalHeaders,
117120
}
118121
const url = `${baseUrl}${path}${queryString}`
119122
const bodyContent = (() => {

packages/sdk/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,23 @@ await sdk.getQuote(parameters, {
278278
> **Tip:** Use `utmContent` for graffiti without affecting your `appCode`. The `appCode` parameter tracks your integration on [CoW Protocol's Dune dashboards](https://dune.com/cowprotocol/cowswap), while `utmContent` is available for custom identifiers or experimentation - all while attributing your volume to SDK integrators' collective impact.
279279
280280

281+
## Partner API
282+
283+
Partners can use authenticated API access with higher rate limits via the Partner API gateway. Pass your API key when creating an `OrderBookApi` instance:
284+
285+
```typescript
286+
import { OrderBookApi, SupportedChainId } from '@cowprotocol/cow-sdk'
287+
288+
const orderBookApi = new OrderBookApi({
289+
chainId: SupportedChainId.MAINNET,
290+
apiKey: 'your-partner-api-key',
291+
})
292+
```
293+
294+
By default, the SDK routes requests through `partners.cow.fi` (prod) or `partners.barn.cow.fi` (staging) and includes the `X-API-Key` header. If you provide custom `baseUrls`, those take precedence over the default partner hosts, but the `X-API-Key` header is still sent with every request.
295+
296+
See the [OrderBookApi documentation](https://github.com/cowprotocol/cow-sdk/tree/main/packages/order-book/README.md#partner-api-authenticated-access) for more details, including usage with the Trading SDK.
297+
281298
## Adapters
282299

283300
The CoW SDK supports multiple blockchain adapters to work with different Web3 libraries. You need to install and configure one of the following adapters:

0 commit comments

Comments
 (0)