For agent developers
An agent developer is an integrator in Sway. You work in two places. The integrator console at https://swayoffers.com/agents takes a browser wallet, and that wallet is your integrator authority: it registers the integrator, names the payout wallet and registers the key of each agent device. On the device, your shopping agent runs @sway/mcp, an MCP server that runs beside the agent and talks to it over stdio. The shopper's preferences, the per-intent keys and the decrypted offers stay in that process. The Sway API receives ciphertext, public keys and hashes. In return, your integrator earns 5% of every brand rebate your users claim.
What you need
- A Solana browser wallet for the integrator console. Connect a wallet lists the ones that work.
- A payout wallet: an existing Solana wallet address, owned by the System Program and already holding SOL. Use the wallet address itself, not a USDC token account. Your fees arrive in its USDC account. The console uses the connected wallet unless you enter another.
- Node.js 22 on the agent's device. The package declares
node >=22 <23. - An operating-system keychain on that device: Keychain on macOS, Credential Manager on Windows, or a persistent Secret Service on Linux. The package stores its keys there, and a locked or missing keychain stops the command.
Set up
The steps run from the integrator console to the device and back: register, wait for approval, install the package, create the agent key, register it, then attach its IDs on the device.
1. Register the integrator
Open https://swayoffers.com/agents and connect the wallet. The Register as an integrator panel takes two fields.
| Field | What to enter |
|---|---|
| Agent or company name | Up to 120 characters |
| USDC payout wallet (defaults to this wallet) | The wallet address that receives your fees. Left empty, the connected wallet |
Select Register and sign the message your wallet shows. The console asks the API for a one-use integrator_registration challenge, your wallet signs it, and the console posts the name, the wallet's public key as the authority and the payout address to POST /v1/integrators. The API checks the payout wallet against finalized chain state. The panel then shows the integrator's name with Status, Integrator ID and Payout wallet.
The console remembers the integrator ID for this wallet in this browser. In another browser, connect the same wallet, enter the ID under "Already registered from another browser? Integrator ID" and select Use this ID.
2. Wait for approval
A new integrator starts as pending, and the console reads "Waiting for the operator's approval. This page reads the status again when you reload it." The Sway operator approves the integrator, and its status becomes active. Reload the page to read it: the Agent keys panel appears once the integrator is active.
Each read of the integrator is a signed SwayIntegrator request, so your wallet asks for a message signature when the page loads. Until approval, the API refuses agent-key registration with FORBIDDEN and "The integrator is pending operator approval."
3. Install the package
On the agent's device:
npm install -g https://swayoffers.com/downloads/sway-mcp-0.3.1.tgz
The console's Install the agent package panel links the same archive as Download @sway/mcp and shows this command with the next one. The install adds the sway-mcp command.
4. Create the agent key
sway-mcp init --api https://api.swayoffers.com
init creates the device's agent key, an Ed25519 key, in the OS keychain. It prints the key's publicKey, the scopes the package uses (intent:write, intent:read and purchase:report) and the next step. Running it again prints the same key. The device needs no authority key.
Pass the same --api value on every command, or set SWAY_MCP_API_URL once. The package names its keychain entries after that API URL, so a different URL reaches a different set of keys. The signed-request audience comes from the API itself: GET https://api.swayoffers.com/v1/encryption publishes it as authorizationAudience, and the package stores it on first use. --audience <origin> sets it explicitly.
5. Register the agent key
Back in the integrator console, fill the form in Agent keys.
| Field | What to enter |
|---|---|
| Agent public key (printed by sway-mcp init) | The publicKey that init printed |
| Label | Up to 120 characters, for example the device's name |
| Scopes | intent:write, intent:read and purchase:report are checked |
Select Register agent key. Your wallet signs an agent_registration challenge bound to your integrator ID, then a SwayIntegrator request, and the console sends both to POST /v1/integrator/agent-keys. The rate limit comes from the deployment's policy and appears as rateLimitPolicy on the key. The console reads "Registered. Attach it on the device with the command below." and prints the command for the next step. The key joins the list with its label, public key, scopes and status.
6. Attach the IDs on the device
Run the command the console printed:
sway-mcp configure --api https://api.swayoffers.com --integrator-id <integrator ID> --agent-key-id <agent-key ID>
sway-mcp status --api https://api.swayoffers.com
configure stores the integrator ID and the agent-key ID beside the agent key. status prints the public IDs, the public keys, the audience, the last intent ID and the key store. It never prints a private key. Then connect your MCP host, and the five tools sign as this agent key.
Revoking a key
Select Revoke next to a key in the console. Your wallet signs a SwayIntegrator request to DELETE /v1/integrator/agent-keys/{id}, and the key's status changes to revoked. A revoked key can no longer use the relayer.
Commands
| Command | What it does |
|---|---|
init | Creates the device's agent key in the keychain if it has none, and prints its public key and scopes |
configure --integrator-id <id> --agent-key-id <id> | Attaches the IDs the integrator console issued for this device's key |
status | Prints public IDs, public keys, the audience, the last intent ID and the key store. Never a private key |
serve | Starts the MCP server on stdio |
find_offers, get_voucher, report_purchase, claimable, claim | Runs one tool from the command line. Hyphen forms such as find-offers work too |
keygen-authority, import-authority, register-integrator, register-agent | The authority-on-a-machine path, below |
help | Lists the commands |
Connect your MCP host
Point the host at the serve command. For a host configured with mcpServers JSON:
{
"mcpServers": {
"sway": {
"command": "sway-mcp",
"args": ["serve", "--api", "https://api.swayoffers.com"]
}
}
}
On Windows, some hosts need sway-mcp.cmd as the command. The server writes only MCP JSON-RPC to stdout and registers five tools. claimable is marked read-only and claim destructive.
| Variable | Effect | Default |
|---|---|---|
SWAY_MCP_API_URL | API base URL when --api is absent | none |
SWAY_MCP_MAX_POLLS | Polls per find_offers call | 60 |
SWAY_MCP_POLL_INTERVAL_MS | Milliseconds between polls | 500 |
SWAY_MCP_CLAIM_TTL_SECONDS | Lifetime of a claim authorization, in chain seconds | 120 |
SWAY_MCP_KEYCHAIN_SERVICE | Keychain service name | sway-mcp- plus 24 hex characters derived from the API URL |
Alternative: an authority key on a machine
An integrator can hold its authority as a key on a machine instead of in a browser wallet. The package keeps that key in the machine's keychain and signs the same registrations with it.
sway-mcp keygen-authority --api https://api.swayoffers.com
sway-mcp register-integrator --api https://api.swayoffers.com --name "My shopping agent" --payout <PAYOUT_WALLET>
sway-mcp register-agent --api https://api.swayoffers.com --label "Shopper device"
| Command | What it does |
|---|---|
keygen-authority | Creates the integrator authority key in the keychain. Repeating it returns the same key; it never replaces one |
import-authority | Reads an existing authority key from stdin as a JSON byte array, either a 32-byte Ed25519 seed or a 64-byte Solana keypair. Refuses if the machine already has an authority |
register-integrator --name <name> --payout <wallet> | Signs an integrator_registration challenge with the authority and posts the name, authority public key and payout wallet to POST /v1/integrators. The integrator starts as pending |
register-agent --label <label> | After approval, creates this device's agent key if needed, registers it with the scopes intent:write, intent:read and purchase:report, and stores the issued IDs, so configure is not needed |
The authority can also register a key that init created on another device: post it to POST /v1/integrator/agent-keys with an agent_registration proof and a SwayIntegrator request, then run configure on that device with the IDs the API returns.
The five tools
Every tool returns the same envelope: { "data": …, "error": null, "meta": { "mode", "scenario", "requestId" } } on success, and data: null with an error of { code, message, retryable } on failure, with MCP isError set. Amounts are decimal strings of USDC base units: "15000000" is 15 USDC. The full JSON schemas are published at https://api.swayoffers.com/v1/mcp/tools.
On the deployed service, a brand ID is the brand's merchant wallet address. topOffers returns it as brandId, and brandBonusesUsdc and existingBrandIds take the same addresses.
find_offers
Encrypts the shopper's intent on the device, posts it, waits for matching and decrypts the sealed top three.
| Input | Type | Meaning |
|---|---|---|
intent.categoryShard | string | A category from allowedShards at https://api.swayoffers.com/v1/encryption |
intent.size | number | Size in whole or half steps |
intent.width | narrow, standard or wide | Width |
intent.region | US | Region |
intent.maxPriceUsdc | amount | Highest product price the shopper accepts |
intent.brandBonusesUsdc | object of brand ID to amount | Extra value the shopper places on a brand. It counts in the score and is never paid |
intent.existingBrandIds | array of brand IDs | Brands the shopper already buys from. Offers marked new customers only skip them |
intentId | string, optional | Resume an earlier call. Omitted, a random ID is generated |
cashbackOnly | boolean, optional | Register for cashback without brand matching. Answers CONFIGURATION_REQUIRED until the deployment connects an affiliate network |
Bonuses and existing brands together fit at most ten distinct nonzero entries, the budget of the eight encrypted elements an intent carries.
| Output | Meaning |
|---|---|
intentId | The intent's ID. Keep it: every later tool takes it |
status | pending, matched, expired, cashback_fallback or failed |
topOffers | Up to three of { slot, campaignId, brandId, priceUsdc, rebateUsdc, scoreUsdc }, best first |
fallbackReason | NO_ELIGIBLE_OFFERS, COMPUTATION_EXPIRED_TWICE, CASHBACK_ONLY or null |
The score is brand_bonus + rebate − price. For example, a 120 USDC product with a 15 USDC rebate and no bonus scores "-105000000".
Before sending anything, the tool creates a fresh claim key (Ed25519) and result key (x25519) for the intent and saves both in the keychain. It binds a purchase deadline, read from the chain clock and the deployment's purchase window, into the encrypted input. A matching computation has 180 Solana slots to finish; if it expires, the tool requests the one allowed retry by itself. When polling ends first, the tool returns pending or expired with the intent ID. Call it again with that intentId and the same intent to resume. Reusing an ID with different preferences returns IDEMPOTENCY_CONFLICT.
On the deployed service, cashback_fallback is the final state of an intent that no brand offer serves: matching found no eligible offer (NO_ELIGIBLE_OFFERS), or the retried computation expired as well (COMPUTATION_EXPIRED_TWICE). The tool returns it with an empty topOffers, and get_voucher refuses it.
On the command line, pass the input as a file or on stdin: find_offers --input intent.json or --input -.
get_voucher
| Input | Output |
|---|---|
intentId, slot (0–31, from topOffers) | intentId, intentAccountAddress, slot, attachAtCheckout |
attachAtCheckout is the string sway:<intentId>:<slot>. Pass the whole voucher to the merchant with the order: the receipt signer needs intentAccountAddress. A voucher is issued only for a matched intent, before its purchase deadline.
report_purchase
| Input | Meaning |
|---|---|
intentId | The intent |
slot | The slot bought from |
orderHash | SHA-256 of the order, 64 lowercase hex characters |
evidenceHash | SHA-256 of the checkout receipt the agent keeps |
encryptedEvidenceReference | Optional opaque handle of the form opaque:<id> |
It returns { id, intentId, status, evidenceHash, conversionId, createdAt }, with status one of awaiting_attestation, conversion_received or disputed. Only the hashes leave the device. Keep the checkout receipt itself: it is the evidence for any dispute.
claimable
| Input | Output |
|---|---|
intentId | A page of items, each { id, conversionId, intentId, claimPublicKey, amount, proofTier, unlockAt, status, destination, transactionId, conversionAccountAddress, programId } |
The tool signs a fresh claim_read challenge with the intent's claim key; balances are never readable without it. status is one of pending, locked, claimable, claiming, claimed, refunded, disputed or failed. proofTier is T2, a merchant receipt, for a brand offer.
claim
| Input | Output |
|---|---|
intentId, claimableId (an item id), destination (a Solana wallet address the shopper chose) | { id, claimableIds, destination, totalAmount, status, transactionId, failure } |
The claim key signs a 128-byte claim message that binds the program, the conversion, the destination, an expiry and a nonce, then a fresh HTTP challenge. The package stores that authorization and its idempotency key before submitting, so a retry repeats the same request and never pays twice. Sway's relayer pays the transaction fee and creates the destination's USDC account if it has none.
status moves from queued to submitted to confirmed. A queued claim is not yet paid; check with claimable until the item reads claimed. To send a claim to a different address, wait until the earlier authorization expires, 120 seconds by default.
A typical purchase
- Call
find_offerswith the shopper's intent. PresenttopOffersto the shopper. - When the shopper picks one, call
get_voucherwith the intent ID and that offer'sslot. - Buy through the merchant's normal checkout at exactly
priceUsdc, with the voucher attached. - Call
report_purchasewith the order and receipt hashes. Keep the receipt on the device. - Call
claimablefrom time to time. The item showslockedwith itsunlockAt, thenclaimable. - Ask the shopper for a destination address and call
claim. Checkclaimableuntil the item isclaimed.
Cashback
Cashback from affiliate commissions, with the cashback:link scope and POST /v1/cashback/tracking-links, is built into the program and the API and turns on when the deployment connects an affiliate network; until then those calls answer CONFIGURATION_REQUIRED.
Keys
| Key | Held in | Signs |
|---|---|---|
| Integrator authority, Ed25519 | Your browser wallet, in the integrator console. On the machine path, the keychain, from keygen-authority or import-authority | Integrator registration, agent-key registration, SwayIntegrator requests |
| Agent key, Ed25519 | The device keychain, from init or register-agent | SwayAgent requests: intents, vouchers, purchase reports |
| Claim key, Ed25519, one per intent | The device keychain, from find_offers | Claim reads, claims and disputes for that intent |
| Result key, x25519, one per intent | The device keychain, from find_offers | Encrypts the intent and decrypts its sealed top three |
The package stores its keys in small entries sized for Windows Credential Manager. Private keys never appear in API requests, tool output or diagnostics. Sway keeps no copy of a claim key, so a rebate is claimed from the device and OS account that ran find_offers for it. Keep that device and its keychain until each intent's claims are paid.
Disputes
Disputes are not among the five tools; the agent opens one through the API with the intent's claim key. Send POST /v1/disputes with the intent ID, the conversion ID or null, the brand ID, campaign ID and slot from topOffers, a reason (MISSING_CONVERSION, INCORRECT_AMOUNT or UNEXPECTED_REFUND), the evidence hash, an opaque: evidence reference, an optional payoutAddress for compensation, and an open_dispute challenge proof bound to the intent ID. A missing-conversion dispute requires an earlier report_purchase for the same slot with the same evidence hash. Each intent slot takes one dispute. Read it back with POST /v1/disputes/{id}/query and a claim_read proof.
What you earn
| Source | Your share | Example |
|---|---|---|
| Brand rebate | 5% of the rebate, paid by the brand on top of it | For example, a 15 USDC rebate pays you 0.75 USDC |
Your share is paid to the payout wallet's USDC account in the same claim transaction that pays the shopper. On a brand offer it is added on top of the rebate, so the shopper receives the full rebate the brand offered. See Fees and settlement.