For AI agents and bots
Give your agent a wallet, not a blank cheque.
Each swap is checked against your limits, on your own RPC, before the agent signs.
- Signs only what it asked forAn extra instruction, another token or a worse price, and the agent does not sign.
- Your limitsPer swap, per day and which tokens it may buy, set by the owner.
- Safe after a crashEach order is recorded and settled after a restart.
- No sign-upThe wallet signs a message and gets a key.
Overview
Your agent decides what to trade. Orientim builds the swap, your wallet signs it, and if less than your minimum would arrive, the transaction reverts. Pick a way in:
| Use | For |
|---|---|
| Solana Agent Kit | Agents built on Solana Agent Kit. |
| Agent skill | Coding agents such as Claude Code. |
| Command line | Bots in Python, Rust, Go or any language. |
| MCP server | Claude Desktop, Cursor and other assistants. |
| API | Your own client. It must run the check before signing and keep its own records. |
All but the API check, record and recover each swap for you. You need:
- An API key for the wallet (the MCP server gets its own).
- Your own RPC, and your own Jupiter API key (free).
- A wallet that signs and hands the transaction back: a key file or a signing service.
- Node 20.18 or later.
Quickstart
On Claude Desktop, open orientim.mcpb and answer its questions (MCP server). Otherwise:
- Get an API key for the wallet: API keys.
- Install
@orientimhq/swapv2.1.0 (source, Apache-2.0).npm install --save-exact @orientimhq/swap@2.1.0 - Check it was built from the GitHub source.
npm audit signatures - Configure (all settings: guide).
export ORIENTIM_API_URL=https://orientim.com export ORIENTIM_API_KEY=ori_... export SOLANA_RPC_URL=<your own RPC> export JUPITER_API_KEY=<your Jupiter key> export ORIENTIM_POLICY=/home/agent/orientim-policy.json - Set the owner's limits in a file the agent cannot edit: 50 USDC a swap, 200 a day, only into SOL. A daily limit needs a
stateDir.{ "maxAmountIn": { "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": "50000000" }, "maxAmountInPerDay": { "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": "200000000" }, "allowedOutputMints": ["So11111111111111111111111111111111111111112"], "stateDir": "/home/agent/.orientim-state" } - Try it without signing.
npx orientim swap --in EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ --out So11111111111111111111111111111111111111112 --amount 5000000 \ --owner <wallet> --dry-run - Swap, with the same order id on every retry.
export ORIENTIM_WALLET_KEYPAIR=/path/to/wallet.json npx orientim swap --in EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ --out So11111111111111111111111111111111111111112 --amount 5000000 --id order-1 - Make restarts safe: keep the state directory on a lasting disk, run
recoveron every start, and count onlyconfirmedas done (Results and recovery).
API keys
The wallet signs a message and gets a key for itself. Signing moves nothing. The wallet needs 0.01 SOL; a key lasts 90 days.
A key file or a signing service (Turnkey, Privy). The key never leaves your machine.
export ORIENTIM_API_URL=https://orientim.com
echo '{"wallet": "<address>"}' | npx orientim key-challenge
# sign the bytes of "message" with the wallet, then send "message" unchanged:
echo '{"message": "...", "challenge": "...", "signature": "<base58>"}' | npx orientim keyIn code: requestApiKey, which signs only Orientim's key message. Over HTTP: Keys.
Phantom, Solflare or Backpack: connect and sign.
By getting a key, you accept the Terms of Use and the Privacy Notice.
Solana Agent Kit
Adds one action, ORIENTIM_PROTECTED_SWAP, to the kit. It uses the kit's wallet and RPC.
npm install --save-exact @orientimhq/solana-agent-kit @orientimhq/swap@2.1.0Not on npm yet: build it from its source.
import { SolanaAgentKit, KeypairWallet } from 'solana-agent-kit';
import { orientimPlugin } from '@orientimhq/solana-agent-kit';
const agent = new SolanaAgentKit(new KeypairWallet(keypair, RPC_URL), RPC_URL, {}).use(
orientimPlugin({
apiKey: process.env.ORIENTIM_API_KEY!, // this wallet's key (API keys, above)
jupiterApiKey: process.env.JUPITER_API_KEY, // your own: the routes are fetched with it
policy: {
maxAmountIn: { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: '25000000' }, // at most 25 USDC a swap
allowedOutputMints: ['So11111111111111111111111111111111111111112'], // and only into SOL
},
}),
);import { createVercelAITools } from 'solana-agent-kit'; // or createLangchainTools, createOpenAITools
import { ACTION_NAME } from '@orientimhq/solana-agent-kit';
const tools = createVercelAITools(agent, agent.actions.filter(a => a.name === ACTION_NAME));amountis in whole tokens, as text ("0.5").- A
policyis required. The sameorderIdnever swaps twice. - Slippage is capped at 3% unless the policy raises it.
signOnlykits are not supported.
AI agents: the skill
For coding agents such as Claude Code. @orientimhq/swap holds the skill, the orientim command and the library.
npm install --save-exact @orientimhq/swap@2.1.0
cp -r node_modules/@orientimhq/swap/skills/orientim-protected-swap .claude/skills/Other agents: point them to SKILL.md (GitHub). Settings: Quickstart.
npx orientim swap --in <mint> --out <mint> --amount 5000000 \
--owner <wallet> --dry-runShows the swap and its cost. Nothing is signed.
npx orientim swap --in <mint> --out <mint> --amount 5000000 --id order-42Or ask your agent: “swap 5 USDC to SOL with Orientim”.
import { createKeyPairSignerFromBytes, createSolanaRpc } from '@solana/kit';
import { readFileSync } from 'node:fs';
import { acquireLock, createFileStore, loadPolicy, protectedSwap, recoverPending, stateDirFor } from '@orientimhq/swap';
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!);
const wallet = await createKeyPairSignerFromBytes(
new Uint8Array(JSON.parse(readFileSync(process.env.ORIENTIM_WALLET_KEYPAIR!, 'utf8'))));
const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const SOL = 'So11111111111111111111111111111111111111112';
// The owner's limits, if set: they hold only when passed to protectedSwap below.
const policy = process.env.ORIENTIM_POLICY ? loadPolicy(process.env.ORIENTIM_POLICY) : undefined;
// One state directory for every run of this wallet (the policy's own stateDir with a daily limit).
const { dir } = stateDirFor(policy, process.env.ORIENTIM_STATE_DIR);
const store = createFileStore(dir);
// One swap at a time per wallet: a second run waits, and retries with the same order id.
const release = acquireLock(dir, wallet.address);
try {
// Settle what a stopped run left before anything new.
const { unknown, bookkeepingErrors } = await recoverPending(store, rpc, { orders: store, spends: store });
if (unknown.length || bookkeepingErrors.length) throw new Error('An earlier swap may still land');
const result = await protectedSwap({
apiUrl: process.env.ORIENTIM_API_URL!,
apiKey: process.env.ORIENTIM_API_KEY!,
jupiterApiKey: process.env.JUPITER_API_KEY,
rpc, wallet, pending: store, orders: store,
policy, spends: store,
intent: { id: 'order-42', inputMint: USDC, outputMint: SOL, amountIn: '5000000' },
});
// result.outcome: 'confirmed' | 'failed' | 'expired' | 'unknown' | 'rejected'
} finally {
release();
}- Use the same order id on every retry. An order never swaps twice.
- In your own code, pass
policyandspends, or the owner's limits do not apply. - Signing service: pass
signerFromSignTransaction(Turnkey, Privy) orsignerFromSignBytes(KMS, HSM) aswallet.
Production setup
- Keep the key out of the agent's reach: a signing service, a KMS or an HSM.
- Put the policy at
/etc/orientim/policy.json(C:\ProgramData\Orientim\policy.jsonon Windows), read-only to the agent. - Fund the wallet with only what the agent may swap.
Bots: the command line
For bots in any language: JSON on stdin, JSON on stdout, and an exit code. Your bot signs one message.
npm install --save-exact @orientimhq/swap@2.1.0- recover: settles earlier swaps. Exit 3: start nothing new.
- prepare
{"intent": {...}}: returnsmessageandchecked. - Sign the bytes of
message(base64) with the wallet's key. - finalize
{"checked": ..., "signature": "<base58>"}: returnsoutcomeandreceived.
import base64, json, subprocess
ORIENTIM = "node_modules/@orientimhq/swap/dist/bin/orientim.mjs"
def orientim(command, payload=None):
# No short timeout: finalize waits for the chain. If it is stopped anyway, run recover first.
run = subprocess.run(["node", ORIENTIM, command],
input=json.dumps(payload or {}), capture_output=True, text=True)
return run.returncode, json.loads(run.stdout)
code, _ = orientim("recover") # 3: an earlier swap may still land; stop
if code == 0:
code, ready = orientim("prepare", {"intent": {
"id": "order-42", "owner": str(keypair.pubkey()),
"inputMint": USDC, "outputMint": SOL, "amountIn": "5000000"}})
if code == 0:
# keypair: the wallet's solders Keypair, loaded from its file
signature = keypair.sign_message(base64.b64decode(ready["message"]))
code, result = orientim("finalize", {
"checked": ready["checked"], "signature": str(signature)})| Code | Meaning |
|---|---|
0 | Done: ready to sign, confirmed, all settled, or safe to sign. |
1 | Refused or not swapped; error.code says why. Nothing is left to settle. |
2 | Usage or configuration error; the answer says what is missing. |
3 | An earlier swap may still land. Run recover; start nothing new. |
4 | Orientim refused: error.code is one of the Errors below. |
5 | This order id already swapped, or may still land. It never swaps twice. |
- Branch on
error.code.untrustedServerMessageis for logs only. - Rust:
keypair.sign_message(&message).to_string(). resolvesettles a swap by hand;key-challengeandkeyget an API key.
Assistants: the MCP server
For Claude Desktop, Claude Code, Cursor and any client that runs a local MCP server. It gets its own API key.
gh attestation verify orientim.mcpb -R orientimhq/orientim-protected-swapDownload orientim.mcpb (v2.1.0), verify it and open it. It asks for your RPC and keypair file.
{
"mcpServers": {
"orientim": {
"command": "npx",
"args": ["-y", "@orientimhq/swap@2.1.0", "mcp"],
"env": {
"SOLANA_RPC_URL": "https://your-own-rpc.example/...",
"ORIENTIM_WALLET_KEYPAIR": "/path/to/agent-wallet.json",
"JUPITER_API_KEY": "..."
}
}
}
}claude mcp add --transport stdio orientim --env SOLANA_RPC_URL=... --env ORIENTIM_WALLET_KEYPAIR=... -- npx -y @orientimhq/swap@2.1.0 mcpStart the same server from your agent: Vercel AI SDK and LangChain examples.
| Tool | What it does |
|---|---|
orientim_wallet | Address and balances. |
orientim_quote | Prices and verifies a swap. Nothing is signed. |
orientim_swap | Swaps the quote you approved, within 10 minutes. |
orientim_recover | Settles an interrupted swap. |
- Your client asks you before each swap, when it supports it.
- Caps: 3% slippage and 5% price impact. Raise them with
ORIENTIM_MAX_SLIPPAGE_BPSandORIENTIM_MAX_PRICE_IMPACT_BPS. - It runs locally, next to the key. Remote-only assistants such as ChatGPT cannot use it.
How a protected swap works
- Prepare: Orientim builds the transaction and returns it unsigned, with a ticket.
- Verify: your agent checks the exact bytes on its own RPC.
- Sign: your wallet signs. Orientim never holds your key.
- Finalize: Orientim adds its one-time signature and sends it.
A transaction lives about 40 seconds. With fewer than 30 blocks left, prepare again.
Verify before you sign
With the check, even a compromised server cannot make your wallet sign more than you approved, or below your own floor. Without it, you trust Orientim's server with your wallet.
checkPrepared(prepared, intent, yourRpc) returns the problems it finds. Sign only when there are none. The skill and the command line run it for you. It checks:
- The fee, the network fee and your minimum.
- The exact bytes, simulated on your own RPC.
- Rent kept by the route: at most 0.001 SOL (
maxRouteCostLamports).
Your minimum must come from your own price, never Orientim's; ownMinimum gets one from Jupiter.
Any language: pipe {"prepared": ..., "intent": {...}} into npx orientim check. Exit 0 means safe to sign.
Your own Jupiter key
With JUPITER_API_KEY set, the skill fetches routes with your key, which never leaves your process. ORIENTIM_OWN_ROUTES=0 (ownRoutes: false in code) uses Orientim's key instead.
Each round counts as one prepare toward the rate limit. Direct API clients: the rounds are in reference/AGENT-API.md.
Results and recovery
The skill and the command line read the result on chain and return an outcome: confirmed, failed, expired, rejected, or unknown (check again before anything new).
For a direct API client
Your wallet's signature is the transaction id. Store it with the ticket before you call finalize; it is how you find out what happened.
| status | Meaning |
|---|---|
sent | Accepted. Confirm on chain; re-send signedTransaction until it confirms or expires. It lands only once. |
unknown | The answer was lost. Check the signature first. |
signed | With "send": false: signed, not sent. Send it yourself. |
rejected | Not sent, usually because the price moved. |
Only a swap confirmed on chain is done. sent and unknown mean it may still land.
Orientim keeps no order database. Your store and the chain are the record. The skill keeps that store for you.
- Finalizing the same ticket again is safe: the transaction lands only once.
- Two prepares are two transactions. Use one order id per decision and one swap per wallet at a time.
- Prepare the same swap again only once the signed one can no longer land.
On every start, for a direct API client
- Load every signed swap not yet settled.
- Look up each signature on your RPC and record confirmed or failed.
- Not found: re-send the same bytes, and mark it expired only once the network is past its last valid block.
- Only then prepare anything new for that wallet.
Authentication
Base URL https://orientim.com. Every request carries an API key:
Authorization: Bearer ori_...A key prepares swaps for its own wallet only.
Prepare
POST /api/v1/prepare
Authorization: Bearer ori_...
Content-Type: application/json
{
"owner": "<your wallet address>",
"inputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"outputMint": "So11111111111111111111111111111111111111112",
"amountIn": "5000000",
"minOut": "42400000",
"slippageBps": 100
}| Field | Meaning |
|---|---|
ownerrequired | The wallet that pays, receives and signs first. |
inputMint, outputMintrequired | Mint addresses. SOL is So11111111111111111111111111111111111111112. |
amountInrequired | Base units, as a string ("5000000" is 5 USDC). |
minOutrequired | The least your wallet must receive, in base units, from a price you got yourself. |
slippageBpsoptional | 10 to 1500. Default 50, or 300 on a Pump.fun curve. |
acceptCostBpsoptional | Accept a protected route this many bps below the market (costs-more). minOut still holds. |
ownRoutes, session, routesoptional | Routes fetched with your own Jupiter key. See Your own Jupiter key. |
Answer
All amounts are strings in base units.
| Field | Meaning |
|---|---|
ticket | Pass it to finalize. |
transaction | The unsigned transaction, base64. Verify it, then sign. |
messageSha256 | The hash of the transaction message. |
temporaryAuthority | The swap’s one-time key. |
lastValidBlockHeight, blocksLeft | The lifetime: 150 blocks, about 40 seconds. |
amounts | amountIn, fee, feeMint, feeBps, quotedOut, minOut, priceImpactPct (null means unknown). |
costs | Network fee, rent, Orientim’s fee, and keptSolLamports: all the SOL the swap does not return. |
notices, tokens | A busy network, and whether a token’s issuer can freeze, mint or move balances. |
certificate, policy | What the transaction does, and the rules it was checked against. |
A prepare answers within 45 seconds, or 503 unavailable. Bodies may be up to 1 MiB.
Finalize
POST /api/v1/finalize
Authorization: Bearer ori_...
Content-Type: application/json
{ "ticket": "eyJ2Ijox....", "signedTransaction": "<base64, signed by your wallet>" }Orientim signs and sends. The same ticket again returns the same transaction.
{
"signature": "5h...",
"status": "sent",
"signedTransaction": "<base64, fully signed>",
"lastValidBlockHeight": "312345678"
}status: see Results and recovery. To send it yourself (staked RPC, bundle), add "send": false and re-send until it confirms or expires.
Keys
GET /api/v1/keys/challenge?wallet=<address>
→ { "message": "...", "challenge": "...", "expiresAt": "..." }
POST /api/v1/keys
{ "message": "<the message, unchanged>", "challenge": "...", "signature": "<base58 or base64>" }
→ { "key": "ori_...", "wallet": "<address>", "expiresAt": "..." }- Sign the challenge within 10 minutes.
- Sign only Orientim's key message. Other bytes could be a transaction.
- Keep your key; do not request a new one on every start.
Errors
Every error is { "error": { "code": "...", "message": "..." } }, and nothing was signed or sent. requiresApproval: true means ask the user, do not retry.
| HTTP | code | What to do |
|---|---|---|
| 400 | bad-request | Fix the field named in message. |
| 400 | invalid-ticket | The ticket is not this key’s, or was altered. |
| 400 | transaction-changed | Sign the transaction exactly as returned. |
| 400 | bad-signature | Keys: the signature or challenge is not valid. Ask for a new challenge. |
| 400 | wallet-changed-transaction | Your wallet’s signature is missing or wrong. |
| 400 | bad-session | ownRoutes: the session expired or is another swap’s. Prepare again without it. |
| 401 | unauthorized | The API key is missing, expired or revoked. Get a new one. |
| 403 | wrong-wallet | A key prepares swaps for its own wallet only. |
| 403 | wallet-empty | Keys: the wallet holds less than 0.01 SOL. |
| 404 | not-enabled | The agent API is not available. |
| 409 | price-moved | The market cannot meet minOut. Retry with newMinOut only if the user approves. |
| 409 | costs-more | The protected route is gapBps below the market. Retry with acceptCostBps only if the user approves. |
| 409 | routes-needed | ownRoutes: fetch the routes with your Jupiter key and prepare again. The skill does this. |
| 409 | session-used | ownRoutes: this session already signed a transaction. Check its signature first. |
| 409 | output-balance-changed | Your output balance moved since prepare. Check the signature, then prepare again. |
| 409 | output-in-flight | Another swap into the same account has not landed yet. Finalize again after Retry-After. |
| 410 | expired | The transaction expired. Check the signature, then prepare again. |
| 422 | amount-too-small | Below about 0.004 SOL or $1. Selling a token’s whole balance is allowed at any size. |
| 422 | unsupported-token, no-route, insufficient-sol, insufficient-balance, simulation-failed, … | This swap cannot be built safely now; message says why. |
| 426 | skill-outdated | Update the skill. A swap already signed still finalizes. |
| 429 | rate-limited | Too many requests. Wait Retry-After seconds. |
| 500 | internal | Nothing was signed or sent. Retry later. |
| 503 | busy, unavailable | At capacity, or a provider is slow. Retry after Retry-After. |
| 503 | fee-unavailable | Nothing was built. Retry later. |
| 503 | paused | Swaps and new keys are paused. Your funds are not affected. |
| 503 | route-format | Swaps are briefly unavailable. Wait at least Retry-After. |
Rate limits
- Per wallet: 60 prepare and 30 finalize a minute.
- Per IP address: 120 requests a minute.
- Keys: 30 challenges and 10 keys an hour per IP, 5 keys a day per wallet.
- A
429carriesRetry-After. GET /api/healthneeds no key: 200 when swaps can go through, else 503.
Fees and limits
- 0.25%, inside the transaction, in SOL, USDC or USDT when the swap has one, else the input token (
amounts.feeMint).- On the input: that share of
amountIn. - On the output: that share of the enforced minimum. With your own routes, it must be within 1% of Orientim's price.
- Otherwise: that share of the swap's value, in SOL from the wallet.
- On the input: that share of
- The skill refuses an Orientim fee above 0.25% (
maxFeeBps, up to 0.3%) and a network fee above 0.001 SOL. - Slippage: 0.5% by default, 3% on a Pump.fun curve (
slippageBps, 0.1% to 15%). - Price impact: at most 5% (
maxPriceImpactBps, up to 20%). - The minimum is never more than 20% below Jupiter's price.
- The owner's limits per swap, per day and per token (
ORIENTIM_POLICY). - Smallest swap: about 0.004 SOL or $1, except selling a whole balance.
Tokens and wallets
See supported tokens. Key files and signing services work; sign-and-send-only wallets and multisig vaults do not.
Downloads
- @orientimhq/swap v2.1.0 on npm: library, CLI, MCP server and skill.
- orientim.mcpb: the MCP server for Claude Desktop.
- Verify:
gh attestation verify <file> -R orientimhq/orientim-protected-swap, or the sha256 inRELEASE-SHA256SUMSon the release. - Source on GitHub, Apache-2.0, with a reproducible build.
- Full API reference:
reference/AGENT-API.mdin the skill folder.