- API address
/api/accounts/sdk- API key
- You create it under Your AI apps, API keys. Shown once.
- What you see
- Only your app, your deliveries and your earnings.
First, choose where you are connecting
There are two separate places. Each has its own address and its own key. Use a key only with its own address.
- API address
/api/product- API key
- Issued by the AXP team for the shared demo. You cannot create one.
- What you see
- Public example campaigns that anyone can see and change.
- Step 1Your server
Request
Ask for an ad at the same time as you ask your own model for an answer.
An award, or no fillconst adTask = axp.requestAd({ question, sessionId, turnId, }); - Step 2Your app
Render
For an award, add the approved card below your answer with its Sponsored label.
A separate Sponsored cardconst node = createSponsoredCard({ document, award: ad.award, }); sponsorSlot.append(node); - Step 3Your app to your server
Acknowledge
Confirm the card is on the page, then send that confirmation for a receipt.
One accepted chargeawait acknowledge({ node, award: ad.award, deliveryToken: ad.deliveryToken, });
Run your own model in parallel and show its answer as soon as it is ready. If there is no ad, or the ad request fails, the answer is unaffected.
Set up your server
Keep the key in your server’s private environment. Never put it in browser code, a prompt or a URL.
# Your own app. Copy the exact address from Your account > Your AI apps > Connect.
AXP_EXCHANGE_URL=https://axp.one/api/accounts/sdk
AXP_PUBLISHER_API_KEY=<the API key you created, shown once># The shared example chat only. Its key is issued by the AXP team.
AXP_EXCHANGE_URL=https://axp.one/api/product
AXP_PUBLISHER_API_KEY=<key provided for the shared demo>The SDK is not published to npm yet. Copy the SDK source into your project or use it as a local package. It needs Node 22.18 or later and has no runtime dependencies. View SDK source ↗
1. Request an ad next to your answer
Send one cleaned question of up to 1,200 characters, a random session ID per conversation and a stable turn ID per message. Leave private data and chat history out.
import { AXPPublisher } from './packages/publisher-sdk/index.mjs';
const axp = new AXPPublisher({
apiKey: process.env.AXP_PUBLISHER_API_KEY, // server only
baseURL: process.env.AXP_EXCHANGE_URL, // your account address, or the shared demo's
timeoutMs: 15000,
receiptTimeoutMs: 45000,
});
// Start both before awaiting either. The answer receives no sponsor material.
const adTask = axp.requestAd({ question, sessionId, turnId,
placementId: 'chat-sponsored-card',
settlementNetwork: 'solana-devnet' });
const answerTask = yourExistingLLM({ question });
sendAnswer(await answerTask); // stream independently if your app streams
const ad = await adTask;
if (ad.status === 'awarded') sendSponsoredAward(ad);
// no_fill or error: keep the answer and leave the slot emptyDetails: your functions, limits and timeouts
yourExistingLLM, sendAnswer and sendSponsoredAward are your app’s own functions. Your answer path receives no advertiser text, hints or buying evidence.
Session and turn IDs use letters, digits, underscores or hyphens, up to 120 characters. You can also pass required features or category exclusions.
The ad request waits up to 15 seconds and the acknowledgement up to 45 seconds; both can be changed. A timeout does not prove nothing happened on the server, and an ad request is never retried for you.
On the shared demo, set settlementNetwork to solana-devnet or tempo-testnet. Only campaigns on that network compete. Start a new session when you switch.
2. Render the card and 3. acknowledge it
Send only an awarded response and its delivery token to the browser. The helper renders plain text, an https destination and the word Sponsored in a separate card below your answer.
import { createSponsoredCard, createRenderAcknowledger }
from './packages/publisher-sdk/browser.mjs';
// Run only for an awarded response received from your server.
const acknowledge = createRenderAcknowledger({
post: async (_path, observation, deliveryToken) => {
const response = await fetch('/api/axp/render', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-app-csrf': yourCSRF },
body: JSON.stringify({ awardId: ad.award.id,
observation, deliveryToken }),
});
if (!response.ok) throw new Error('delivery_unconfirmed');
return response.json();
},
});
const node = createSponsoredCard({ document, award: ad.award });
sponsorSlot.append(node);
const receipt = await acknowledge({ node, award: ad.award,
deliveryToken: ad.deliveryToken });// Your /api/axp/render server handler, after app authentication + CSRF:
const receipt = await axp.acknowledgeRender({
awardId: body.awardId,
deliveryToken: body.deliveryToken,
observation: body.observation,
});
// receipt.status === 'accepted': inspect charge.id and receiptHash.
// Reconcile an uncertain result with this same award and delivery token.Details: what is checked and what a receipt means
Your own /api/axp/render route signs in the browser request and forwards the confirmation with your server key. The key never reaches the browser or your model’s prompt.
Before posting, the helper checks the card’s award ID, exact text, original destination, visible Sponsored label and that it is on the page. Add your own styles through className. A React version, SponsoredCard, is in packages/publisher-sdk/react.tsx.
An award holds budget. An accepted receipt creates one charge. The browser cannot choose an amount, a recipient or a signing key.
A receipt shows that the card was added with its label. It does not show that anyone read it, clicked it or bought anything, and it is not a payout.
Readiness and troubleshooting
In your account, the Connect tab of your app checks your setup and lists anything missing. It requests no ad.
await axp.failRender({
awardId: ad.award.id,
deliveryToken: ad.deliveryToken,
reason: 'render_failed',
});An accepted charge cannot be removed by failing its award. Acknowledging an accepted receipt again returns the same charge.
Reference
Open only what you need.
Payments and test balances
Accounts currently use test balances: accepted receipts add up, and no real money moves. Payouts from an account are not available yet.
The shared demo can run on a test network with test tokens. There, an accepted delivery advances a running voucher total. Tempo supports payouts while a channel stays open; closing returns unused funds on either network. The example chat’s Payment stage shows those states and their recorded evidence separately.
Connect to the shared demo exchange
Read the shared demo configuration for its placement and supported features. A key for the shared demo is issued by the AXP team; the public configuration does not hand out keys.
The shared demo uses Solana Devnet test USDC or Tempo Testnet PathUSD. Its example chat uses DeepSeek for the answer and Jev for buying decisions; your own app keeps its own answer model.
Run the developer example on your machine
Against the shared demo
With a shared demo key in your server environment, run this from the repository root:
AXP_EXCHANGE_URL=https://axp.one/api/product \
node packages/publisher-sdk/examples/server.mjs
# Open http://127.0.0.1:3433.
# The ad and example-answer requests use the shared demo.Starting it sends nothing. Sending a question uses the shared demo’s daily allowance.
Against a local exchange
npm run demo:product
# Create, approve and launch a campaign at http://127.0.0.1:3430.
AXP_EXCHANGE_URL=http://127.0.0.1:3430/api/product \
node packages/publisher-sdk/examples/server.mjsThe launcher stores its generated server key in ignored local-state/product/publisher-api-key, and the example reads it on the server. To use a different local workspace, set the same AXP_PRODUCT_STATE_DIR in both, or pass its key as AXP_PUBLISHER_API_KEY.
Local test credits are separate from the hosted shared demo. The example chat needs a live answer model and never invents an answer.
Integration research: Gravity’s official server request and render quickstart informed this sequence. AXP uses its own exchange and receipt contracts.