Sway

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.

FieldWhat to enter
Agent or company nameUp 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.

FieldWhat to enter
Agent public key (printed by sway-mcp init)The publicKey that init printed
LabelUp to 120 characters, for example the device's name
Scopesintent: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

CommandWhat it does
initCreates 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
statusPrints public IDs, public keys, the audience, the last intent ID and the key store. Never a private key
serveStarts the MCP server on stdio
find_offers, get_voucher, report_purchase, claimable, claimRuns one tool from the command line. Hyphen forms such as find-offers work too
keygen-authority, import-authority, register-integrator, register-agentThe authority-on-a-machine path, below
helpLists 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.

VariableEffectDefault
SWAY_MCP_API_URLAPI base URL when --api is absentnone
SWAY_MCP_MAX_POLLSPolls per find_offers call60
SWAY_MCP_POLL_INTERVAL_MSMilliseconds between polls500
SWAY_MCP_CLAIM_TTL_SECONDSLifetime of a claim authorization, in chain seconds120
SWAY_MCP_KEYCHAIN_SERVICEKeychain service namesway-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"
CommandWhat it does
keygen-authorityCreates the integrator authority key in the keychain. Repeating it returns the same key; it never replaces one
import-authorityReads 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.

InputTypeMeaning
intent.categoryShardstringA category from allowedShards at https://api.swayoffers.com/v1/encryption
intent.sizenumberSize in whole or half steps
intent.widthnarrow, standard or wideWidth
intent.regionUSRegion
intent.maxPriceUsdcamountHighest product price the shopper accepts
intent.brandBonusesUsdcobject of brand ID to amountExtra value the shopper places on a brand. It counts in the score and is never paid
intent.existingBrandIdsarray of brand IDsBrands the shopper already buys from. Offers marked new customers only skip them
intentIdstring, optionalResume an earlier call. Omitted, a random ID is generated
cashbackOnlyboolean, optionalRegister 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.

OutputMeaning
intentIdThe intent's ID. Keep it: every later tool takes it
statuspending, matched, expired, cashback_fallback or failed
topOffersUp to three of { slot, campaignId, brandId, priceUsdc, rebateUsdc, scoreUsdc }, best first
fallbackReasonNO_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

InputOutput
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

InputMeaning
intentIdThe intent
slotThe slot bought from
orderHashSHA-256 of the order, 64 lowercase hex characters
evidenceHashSHA-256 of the checkout receipt the agent keeps
encryptedEvidenceReferenceOptional 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

InputOutput
intentIdA 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

InputOutput
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

  1. Call find_offers with the shopper's intent. Present topOffers to the shopper.
  2. When the shopper picks one, call get_voucher with the intent ID and that offer's slot.
  3. Buy through the merchant's normal checkout at exactly priceUsdc, with the voucher attached.
  4. Call report_purchase with the order and receipt hashes. Keep the receipt on the device.
  5. Call claimable from time to time. The item shows locked with its unlockAt, then claimable.
  6. Ask the shopper for a destination address and call claim. Check claimable until the item is claimed.

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

KeyHeld inSigns
Integrator authority, Ed25519Your browser wallet, in the integrator console. On the machine path, the keychain, from keygen-authority or import-authorityIntegrator registration, agent-key registration, SwayIntegrator requests
Agent key, Ed25519The device keychain, from init or register-agentSwayAgent requests: intents, vouchers, purchase reports
Claim key, Ed25519, one per intentThe device keychain, from find_offersClaim reads, claims and disputes for that intent
Result key, x25519, one per intentThe device keychain, from find_offersEncrypts 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

SourceYour shareExample
Brand rebate5% of the rebate, paid by the brand on top of itFor 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.