Your agent can think. This lets it act: send email, make calls, research, buy, build sites. It pays per action, gets a signed receipt, and spends nothing you did not allow.
npm install @oneshot-agent/sdkNo code? Use the MCP server in Claude, Cursor or Claude Code:
npx -y @oneshot-agent/mcp-server (setup).
Teach your coding agent the tools: npx skills add oneshot-agent/agent-skills
(skills).
import { OneShot } from '@oneshot-agent/sdk';
const agent = new OneShot({ privateKey: process.env.AGENT_PRIVATE_KEY! });
await agent.email({ to: '[email protected]', subject: 'Hello', body: 'Hello world.' });
const report = await agent.research({ topic: 'AI agents', depth: 'deep' });
const usdc = await agent.getBalance(agent.usdcAddress);Paid calls quote, pay and wait for the result. You write one line.
| Area | Methods |
|---|---|
| Talk | email voice sms inboxList inboxGet smsInboxList smsInboxGet notifications markNotificationRead |
| Send from your domains | listDomains pauseDomain resumeDomain |
| Find people and companies | personFind personProfile personEmail personEmailStatus personDossier personSocialAccounts personArticles personPosts personInterests personFollowers companyFind companyProfile businessFind businessResolve recordSolicitationsFind |
| Think | research predict solve |
| Web and buying | webSearch webRead productFind commerceBuy physicalMail |
| Browser | browser createBrowserProfile startBrowserProfileSetup createBrowserProfileSetupLink getBrowserProfileSetup finishBrowserProfileSetup listBrowserProfiles deleteBrowserProfile |
| Connected systems | listConnections, then route('POST', '/v1/tools/ledger/invoices', { paid: true, body: { connection_id } }) for each read |
linkedinConnect linkedinInvite linkedinWithdrawInvitation linkedinMessage linkedinReply reconnectLinkedInAccount and more |
|
| Build | build updateBuild |
| Goals | compute waitForComputeGoal getComputeGoal getComputeTasks getComputeOutputs respondToComputeTask pauseComputeGoal resumeComputeGoal cancelComputeGoal fundComputeGoal |
| Money and receipts | getBalance getUnifiedBalance budgets topUpCredits spendBreakdown rocs receiptsList tagReceiptValue |
| Anything else | tool(name, input) for any tool, route(method, path, { params, query, body, paid }) for any declared route |
- It pays in USDC on Base mainnet (
agent.chainIdis8453). Fund the wallet first. - A call costs what the quote says, or it does not run.
maxCostsets your ceiling. Above it, the SDK refuses to sign.currency: 'ETH'pays from ETH. The SDK swaps into USDC only when the wallet is short, buysswapBufferMultiplier× the charge (default 10) so one swap covers many calls. Calls in one process never double-swap. Run one process per wallet. Not withcdp.
Budgets cap what a runaway loop can spend.
const agent = await OneShot.create({
cdp: true,
budgets: { daily: 50, perTransaction: 5, alertAt: 0.8, pauseAt: 1.0 },
alerts: { email: '[email protected]' },
});- Budgets live on the server. Every process using the wallet shares them.
- If the server cannot confirm them, nothing is paid:
BudgetSyncError. - At
pauseAt, paid calls throwBudgetExceededErroruntil UTC midnight. - Omit
budgetsand the stored ones stay as they are. No budget means no limit.
Access tokens are for code that must not hold the key: a hosted MCP client, a cloud runner, someone else's platform.
const { token } = await agent.createAccessToken({ name: 'hosted-agent', expires_in_days: 30 }); // from the wallet session
const hosted = await OneShot.create({ accessToken: token });
await hosted.webSearch({ query: '…' }); // paid from credits- Optional
expires_in_daysis a positive whole number of days. Without it or an organization cap, new tokens expire after 90 days; existing tokens keep their expiry. An organization cap (up to 730 days) requires an explicit lifetime within the cap (400 token_lifetime_exceeds_policy). Expired tokens return401 access_token_expired. Lists includeexpires_atandcreated_by; OAuth bridge tokens are exempt from the cap. - A token spends prepaid credits only. It never signs a payment.
- Short of credits, it throws
InsufficientCreditsError(required,balance,shortfall). Top up from the wallet session withtopUpCredits(amount). - A token reads budgets but cannot change them, and cannot mint or revoke tokens.
Every paid call returns a receipt, signed with Ed25519. Check it offline:
import { verifyReceipt, receiptFromWire } from '@oneshot-agent/sdk/receipt';
const jwks = await fetch('https://win.oneshotagent.com/.well-known/oneshot-receipts.json').then(r => r.json());
const result = verifyReceipt(receiptFromWire(wireReceipt), jwks);
// result.valid, or result.reason: 'malformed_receipt' | 'key_not_found' | 'digest_mismatch' | 'signature_invalid'Or from a shell: npx @oneshot-agent/sdk receipt.json (exit 0 valid, 1 invalid, 2 bad input).
Old keys stay in the key set, so old receipts keep verifying.
Reads of private data (inbox, notifications, balances, browser profiles) carry a
signed proof that you hold the wallet. A wallet address is public; your inbox is not.
The SDK signs it for you and asks the runtime for a connection of its own per proof (Connection: close, keepalive: false), so in Bun the fetch has no pooled socket to re-send it on. A browser's fetch ignores both controls (Connection is a forbidden request header there), so in a browser the HTTP stack keeps its own retry rules. If the server says your clock is off, the SDK corrects once and retries. If the server says a read's proof was already used (something between you and the API delivered it twice), it signs a fresh proof and reads once more; a write is never repeated.
import { JobError, JobTimeoutError, ToolError, ValidationError, ContentBlockedError } from '@oneshot-agent/sdk';
try {
await agent.voice({ objective: 'Book a table for 2 at 7pm', target_number: '+14155551234' });
} catch (error) {
if (error instanceof JobError) {
// Branch on error.code, never on the message.
if (['rate_limited', 'service_unavailable', 'internal_error'].includes(error.code)) { /* retry */ }
else { /* fix the input or the funding; retrying as is will not help */ }
} else if (error instanceof ToolError && error.statusCode === 429) {
// error.retryAfterMs is the server's hint. The SDK never retries for you.
}
throw error;
}JobError.codeis one ofinsufficient_fundspayment_failedinvalid_inputcontent_blockedrate_limitedservice_unavailableservice_authenrichment_exhaustedtarget_unreachablesource_unsupportedcheckout_failedinternal_error. What each means.- Input the SDK can check throws
ValidationErrorbefore anything is paid. - Emergency numbers are refused:
EmergencyNumberError.
- Paid calls wait by default.
wait: falsereturns therequest_id;waitForResult(requestId)picks the result up later. - An
AbortSignalstops the call before payment and nothing is charged. After payment, the work happens and is billed. Aborting only stops your wait. - An abort rejects with
OneShotError('Operation cancelled')forcommerceBuy,voice,sms,build,refineBuild,updateBuildandbrowser; the other calls reject with the signal'sreason. Handle both. timeoutis how long you wait, in seconds. It never limits the job.- Goals run longer than calls. Watch one with
waitForComputeGoal:
const goal = await agent.compute({ objective: 'Research battery recycling', budget_usdc: 5 });
const done = await agent.waitForComputeGoal(goal.goal_id, {
timeout: 300,
onStatusUpdate: (status) => console.log(status),
});- It resolves on
completedand throwsJobErroronfailedorcancelled(codeis theend_reason).awaiting_confirmationandpausedwait for you; they do not end the wait. - A goal that failed with
budget_exhaustedcomes back afterfundComputeGoal. CallwaitForComputeGoalagain; the old wait does not resume. - Timeout defaults to 120 seconds and throws
JobTimeoutError. It stops your polling, not the goal. Stop the goal withcancelComputeGoal.
- Email. Omit
from_domainand the server rotates across your warmed domains. Pin one and you skip the warmup gates; the send goes out with awarning. Own no eligible domain and the quote fails withno_sending_domain. There is no shared sender.mailbox_mode: 'mailbox'applies only when a domain is first set up. Details. - People search. An empty result means nobody matched. A failed search
throws; it never pretends to be empty. Page with
limit(default 100, max 500) andoffset. A search that hits its time limit returns what it found; rows whose phone lookup ran out of time say_phone_enrich_reason: 'time_budget_exhausted'. Details. - Phone lookup is off by default.
includePhone: trueis charged for each person you ask for (the searchlimit), found or not; a failed job is refunded.waitForPhones: truewaits for numbers still arriving. It never buys any. Checkphones_pending. Price. - Account login.
startBrowserProfileSetupreturns asetup_url. It opens once and expires in 10 minutes. Give it only to the account's owner, never open it yourself, keep it out of logs and prompts. PollgetBrowserProfileSetupuntilcompleted_atis set. You get cookie names back, never values. Details. - LinkedIn. Invitations are capped at 25 per account per UTC day;
withdrawing one does not give the slot back. Use OneShot's
invitation_id. A session that expired comes back withreconnectLinkedInAccount; a deleted account throwsLinkedInConnectRequiredErrorand needslinkedinConnectagain. Reuse your idempotency key on retries. Details. - Connected systems. The reads of your accounting, CRM, tax, shipping and spend
systems have no typed method: call them with
route('POST', <path>, { paid: true, body }). Each takesconnection_id;listConnections()lists the agent's connections, pick one bysystemandaccount_label. Another agent's connection is refused before payment. Details. - Physical mail. U.S. letters and postcards. Look at the proof and the price,
then approve each quote yourself. Nothing approves it for you. Persist the
idempotencyKeybeforesend. Details.
- Documentation
- MCP Server: local, or hosted at
https://win.oneshotagent.com/mcpwith an access token - Python SDK (LangChain): 54 tools plus the lookup tools as LangChain BaseTool
- Python SDK (Core)
- Pricing
- GitHub
await agent.discoverTools({ query: "send a printed letter" }) searches the public
catalog for free. Omit options for category counts, or pass { category: "email" }
to browse five tools at a time. Pass nextCursor as cursor with the same query
and category. Results use MCP names and include purposes and required inputs;
your token grants filter the catalog before search.
MIT