@agentoffernetwork/sdk
v0.1.1
Published
TypeScript SDK for Agent Offer Network — intent-driven offer matching, click/conversion tracking, and recommendation formatting for LLM agents.
Maintainers
Readme
@agentoffernetwork/sdk
TypeScript SDK for Agent Offer Network — connect your LLM agent to a marketplace of product and service offers.
⚠️ v0.1.1 Breaking:完全对齐 Protocol Offer Schema v0.1。从内部预览版 0.0.1 升级需改代码,见 Migration Guide。
🛑
0.1.0已废,请勿安装 — 发包时 CI 缓存命中旧 dist 导致 runtime import 失败。详见 CHANGELOG。安装请用@agentoffernetwork/sdk@^0.1.1。
Features
- Intent-driven search — describe what the user wants in natural language, get matched offers
- Multimodal intent — text + image support (OpenAI-compatible content parts format)
- Click & conversion tracking — full attribution pipeline for monetization
- Recommendation formatting — ready-to-display output with disclosure labels
- Strong typing —
Category.attributesis a discriminated union (33 sub_type interfaces); zerocastneeded thanks to TypeScript native narrowing - Zero dependencies — uses native
fetch(Node 20+) - Dual mode —
mockfor development,livefor production
Install
npm install @agentoffernetwork/sdkRequires Node 20+.
Fastest Start
Start in mock mode first. It returns built-in sample offers locally, so you
can verify your integration before requesting a live API key.
import { initialize } from '@agentoffernetwork/sdk';
const client = await initialize({
apiKey: 'test-key',
mode: 'mock',
});
const result = await client.queryOffers({
intent: {
content: [{ type: 'input_text', text: 'noise-cancelling headphones under $300' }],
},
context: {
userProfile: {},
},
});
const top = result.offers[0];
if (top) {
console.log(top.offerInfo.title);
}To switch to production later, keep the same code and only change:
const client = await initialize({
apiKey: process.env.AGENTOFFERNETWORK_API_KEY!,
mode: 'live',
appId: process.env.AON_APP_ID, // optional — identifies your app for attribution
});Quick Start
import { initialize, formatPrice } from '@agentoffernetwork/sdk';
const client = await initialize({
apiKey: 'test-key',
mode: 'mock',
});
// Search offers by intent
const result = await client.queryOffers({
intent: {
content: [{ type: 'input_text', text: 'noise-cancelling headphones under $300' }],
},
context: {
userProfile: {},
},
pagination: { limit: 5 },
});
// Access nested fields per Protocol v0.1
for (const offer of result.offers) {
const info = offer.offerInfo;
const price = info.commercial?.price;
console.log(`${info.title} — ${formatPrice(price) ?? 'N/A'}`);
console.log(client.formatRecommendation(offer, { style: 'markdown' }));
}API
initialize(config): Promise<AgentOfferClient>
Create an SDK client instance.
const client = await initialize({
apiKey: 'your-api-key', // Required
mode: 'mock', // 'mock' | 'live'
baseUrl: 'https://api.agentoffernetwork.com', // Optional, default shown
timeout: 5000, // Optional, ms
appId: 'my-app-id', // Optional — sent as `x-aon-app-id` header
});client.queryOffers(params): Promise<QueryOffersResponse>
Search offers by user intent.
const result = await client.queryOffers({
intent: {
content: [
{ type: 'input_text', text: 'project management tool for small teams' },
],
},
context: {
platform: {
name: 'chatgpt',
channel: 'action',
},
userProfile: {
language: 'en',
},
},
pagination: {
limit: 10,
offset: 0,
},
});
// result.offers — Offer[] (each has .uuid, .offerInfo, .entity, .action, .bid, ...)
// result.total — number
// result.hasMore — boolean
// result.queryId — stringclient.reportClick(event): Promise<ClickResult>
Track when a user clicks an offer. Note: trackingUrl is the AON tracking
endpoint (from your integration layer), NOT the advertiser destination.
The advertiser destination is offer.action.payload.target.
const click = await client.reportClick({
offerId: offer.uuid,
trackingUrl: aonTrackingEndpoint, // AON tracking endpoint
timestamp: new Date().toISOString(),
});
// click.trackingId — use for conversion attribution
// click.timestampclient.reportConversion(event): Promise<void>
Track when a user completes a purchase.
await client.reportConversion({
offerId: offer.uuid,
trackingId: click.trackingId,
conversionType: 'sale',
amount: '29.99',
currency: 'USD',
timestamp: new Date().toISOString(),
});client.formatRecommendation(offer, options?): string
Format an offer as display text. Uses offer.action.payload.target for the link.
// Styles: 'brief' | 'detailed' | 'markdown'
const text = client.formatRecommendation(offer, {
style: 'markdown',
includeDisclosure: true, // Default true
disclosureText: 'Sponsored', // Default 'Sponsored'
includePrice: true, // Default true
});formatPrice(price): string | undefined
Standalone helper that returns a human-friendly price string (e.g. "349.99 USD")
or undefined if no price.
import { formatPrice } from '@agentoffernetwork/sdk';
const text = formatPrice(offer.offerInfo.commercial?.price);
// "349.99 USD" or undefinedvalidateCategory(category) — runtime schema check
import { validateCategory, AonValidationError } from '@agentoffernetwork/sdk';
try {
validateCategory(offer.offerInfo.category);
} catch (e) {
if (e instanceof AonValidationError) {
console.error(e.message, e.details?.jsonPath);
// e.g. "offer.offerInfo.category.attributes.sub_type"
}
}Context
The optional QueryContext helps the backend improve matching. Fill in what's available:
| Field | Type | Example | Purpose |
|-------|------|---------|---------|
| platform.name | string | 'sdk', 'mcp-skill', 'chatgpt', 'coze', 'dify' | Platform identification |
| platform.channel | string | 'action', 'mcp' | Integration channel |
| userProfile.language | string | 'en', 'zh-CN' | Language matching |
| userProfile.interests | string[] | ['travel'] | Matching hints |
| sessionId | string | UUID | Dedup within session |
| userProfile.userPseudoId | string | — | Pseudonymous user key |
createContextForPlatform(target, options?): QueryContext
Use the explicit platform adapter when you already know the host platform.
import { createContextForPlatform } from '@agentoffernetwork/sdk';
const mcpContext = createContextForPlatform('mcp-skill', {
interests: ['Apple ecosystem user'],
});
const cozeContext = createContextForPlatform('coze', {
nativeUserId: 'user-123',
nativeSessionId: 'conv-1',
});Notes:
detectContext()remains available for legacy auto-detection.- Browser-like runtimes may still return
web/actionthroughdetectContext()for backward compatibility. - ChatGPT Action integrations should continue omitting
platformanduserPseudoIdin the request body; the F011 server path injects them automatically.
Error Handling
import {
AonAuthError,
AonRateLimitError,
AonNetworkError,
AonValidationError,
AonApiError,
AonProtocolWarning,
protocolWarnings,
} from '@agentoffernetwork/sdk';
try {
const result = await client.queryOffers({ /* ... */ });
} catch (err) {
if (err instanceof AonAuthError) {
// Invalid API key (401)
} else if (err instanceof AonRateLimitError) {
// Too many requests (429)
} else if (err instanceof AonNetworkError) {
// Network/timeout error
} else if (err instanceof AonValidationError) {
// Invalid parameters (has .details?.jsonPath for precise location)
} else if (err instanceof AonApiError) {
// Business logic error (err.code, err.message)
}
}
// Subscribe to upstream protocol deviations (e.g. envelope-level warnings)
const unsubscribe = protocolWarnings.on((warning: AonProtocolWarning) => {
console.warn('[AON protocol deviation]', warning.message, warning.details);
});Modes
| Mode | Use case | Backend |
|------|----------|---------|
| mock | Development & testing | Built-in mock data (12 offers, nested shape) |
| live | Production | https://api.agentoffernetwork.com |
Mock mode defaults to filtering offers with auditStatus === "reject" (matches production behavior; no opt-out).
Platform Integrations
This SDK is the foundation for agent platform integrations:
| Platform | Integration | Package |
|----------|-------------|---------|
| Claude (MCP) | MCP tool server | @agentoffernetwork/skill |
| ChatGPT | OpenAPI Action | Direct API (see sdk/openapi.json) |
| Coze / Dify | HTTP Plugin | Direct API (see sdk/openapi.json) |
| Custom Agent | Code integration | This SDK |
Migration Guide (0.0.1 → 0.1.0)
v0.1.0 is the first public release and is fully aligned with Protocol Offer Schema v0.1. The internal preview 0.0.1 used a flat structure that does not decode real API payloads. If you wrote code against 0.0.1, update per this guide before upgrading.
Field Mapping
| Old SDK (0.0.1) | New SDK (0.1.0) | Type change |
|---|---|---|
| offer.id | offer.uuid | string → string |
| offer.title | offer.offerInfo.title | string |
| offer.description | offer.offerInfo.description | string |
| offer.offerType | offer.offerInfo.offerType | string (Literal) |
| offer.category | offer.offerInfo.category | interface restructured (see §Category) |
| offer.status | offer.offerInfo.status | string |
| offer.expireAt | offer.offerInfo.expireAt | string |
| offer.price.amount | offer.offerInfo.commercial?.price.amount | number → string (decimal) |
| offer.price.currency | offer.offerInfo.commercial?.price.currency | string |
| offer.price.display | (removed — use formatPrice(price)) | — |
| offer.trackingUrl | offer.action.payload.target | string (⚠️ semantics differ — see below) |
| offer.tags | (removed — no replacement) | — |
| offer.conversionRule | (removed — moved to Postback protocol) | — |
| Category.subType | (removed — moved to category.attributes.subType) | — |
| Money interface | (removed — use Price) | — |
| ConversionRule interface | (removed) | — |
Type Imports
- import type { Offer, Money, ConversionRule } from '@agentoffernetwork/sdk';
+ import type { Offer, OfferInfo, CommercialInfo, Price, AuditStatus } from '@agentoffernetwork/sdk';Before / After — queryOffers + formatRecommendation
// ───── 0.0.1 (old flat structure) ──────────────────────────────────
const response = await client.queryOffers(params);
for (const offer of response.offers) {
console.log(offer.title, offer.price?.display ?? 'N/A');
console.log(`Click → ${offer.trackingUrl}`);
console.log(client.formatRecommendation(offer));
}
const click = await client.reportClick({
offerId: offer.id,
trackingUrl: offer.trackingUrl,
timestamp: new Date().toISOString(),
});
// ───── 0.1.0 (nested aligned with Protocol v0.1) ──────────────────
import { formatPrice } from '@agentoffernetwork/sdk';
const response = await client.queryOffers(params);
for (const offer of response.offers) {
const info = offer.offerInfo;
const price = info.commercial?.price;
console.log(info.title, formatPrice(price) ?? 'N/A');
// `action.payload.target` is the advertiser destination (e.g. https://notion.so/plus)
// — NOT the AON tracking endpoint.
console.log(`Destination → ${offer.action.payload.target}`);
console.log(client.formatRecommendation(offer));
}
// The click event still carries `trackingUrl` (the AON tracking endpoint),
// but you now get it from your own integration (backend / wrapper), not from the Offer.
const click = await client.reportClick({
offerId: offer.uuid, // was offer.id
trackingUrl: aonTrackingEndpoint, // provided by your integration
timestamp: new Date().toISOString(),
});TypeScript Narrowing Advantage
Category.attributes is a native TypeScript discriminated union keyed on
subType. Zero cast calls are needed — the compiler narrows attributes
automatically once you check category.type and attributes.subType.
import type { Category } from '@agentoffernetwork/sdk';
function render(cat: Category) {
if (cat.type === 'electronics') {
const attrs = cat.attributes;
if (attrs.subType === 'audio') {
// attrs is now AudioAttributes — `audioType` is typed
const kind: 'earbuds' | 'headphones' | 'speaker' | 'soundbar' = attrs.audioType;
console.log(kind);
}
}
}This is a direct improvement over the Python SDK (which requires cast(AudioAttributes, attrs)).
tracking_url Semantics
0.0.1 had Offer.trackingUrl pointing to an AON redirect URL (conflating two concerns).
In 0.1.0 there are two separate URLs:
| URL | Location | What it is |
|---|---|---|
| Advertiser destination | offer.action.payload.target | Where the user actually goes (e.g. https://notion.so/plus) |
| AON tracking endpoint | ClickEvent.trackingUrl (user-supplied) | URL for reporting click attribution; obtained from your backend / integration layer |
SDK no longer holds the AON tracking endpoint on Offer. When the user clicks a
recommendation, your app/agent code:
- Redirects the user to
offer.action.payload.target - Calls
client.reportClick({ offerId: offer.uuid, trackingUrl: <AON endpoint>, ... })
The formatRecommendation(offer) helper uses offer.action.payload.target for the displayed link.
Price Type Change
Money { amount: number, currency, display } → Price { amount: string (decimal), currency }.
amountis now a decimal string (e.g."349.99"), not a number — matches protocol and avoids floating-point rounding.displayis removed. Build a display string viaformatPrice(price)or inline:`${price.amount} ${price.currency}` // e.g. "349.99 USD"
Material.url
Material.url is now string | undefined (protocol allows omission in examples).
Always guard before use:
for (const m of offer.material ?? []) {
if (m.url) {
renderImage(m.url);
}
}Removed Fields with No Replacement
offer.tags— no replacement. If you used tags for filtering, useoffer.offerInfo.category+attributes.subTypeinstead.offer.conversionRule— moved out ofOfferinto the server-side Postback protocol. SDK consumers no longer need to handle it.offer.ext— no replacement. Extension points live on the protocol envelope, not per-offer.Category.subType(top-level onCategory) — now lives insideCategory.attributes.subType.
Node Version
Minimum Node is 20 (was 18 in 0.0.1). If you're on 18, bump your runtime before upgrading.
SDKConfig.appId (new)
Pass your app identifier so AON can attribute queries correctly:
const client = await initialize({
apiKey: process.env.AGENTOFFERNETWORK_API_KEY!,
mode: 'live',
appId: process.env.AON_APP_ID,
});Internally this sends x-aon-app-id: <appId> on protected endpoints. Skip it in
mock mode.
ESM / CJS Dual-Bundle Note
SDK is packaged as ESM (.mjs) + CJS (.cjs) dual format. The
protocolWarnings pub-sub uses a globalThis-keyed singleton, so
subscribers keep working even if consumers mix import and require.
Recommended: use the ESM entry (import) consistently.
