@botanary/sdk
v0.1.0-alpha.12
Published
Typed Botanary application, connection and operation API client.
Readme
@botanary/sdk
Alpha version 0.1.0-alpha.12, API contract 2026-09-12. MIT licensed. The package has not been verified against a deployed platform API. Node 22 or later is the server target.
// Server only. The value comes from a secret environment variable, never a browser bundle.
import { createBotanary } from '@botanary/sdk';
const botanary = createBotanary({ apiKey: process.env.BOTANARY_SECRET_KEY! });
const context = await botanary.app.context();
console.log(context.publicAppId, context.environment, context.chainIds);Create an organization, application and scoped Test key in the console. app:read is required for
this authenticated request. Test exposes only served testnet capabilities; Live exposes only served
mainnet capabilities. This call verifies a credential but grants no wallet authority.
client.rest exposes generated typed methods from the reviewed developer OpenAPI contract. It uses
openapi-fetch; unsafe URLs, unreviewed routes and owner-only routes are refused before a key is sent.
Each client instance retains its own key and context. Keys and payloads are not attached to errors.
Reads have up to two additional attempts by default. Retries respect Retry-After and the overall
30-second deadline. A Retry-After longer than the configured maximum is returned to the caller,
not shortened. Mutations retry only if their reviewed route explicitly requires idempotency and
the same valid key is present; operation creation supports this contract. Cancellation
stops retries. BotanaryError carries status, code, request ID, retry delay and available operation
reconciliation handles. A timeout never proves that an operation failed to submit.
@botanary/sdk/browser contains browser-safe errors, exact base-unit amounts and generated types.
It has no server-key constructor. The package never signs for an owner. Account/connection actions
are added with their consent contracts; do not use an app key on existing owner API routes.
When rotating a key, create a replacement with the required scopes, verify it server-side and revoke the old key after the chosen overlap. If the secret was lost before saving, create another and revoke the uncertain key. App/key revocation stops API access; on-chain permission revocation is a separate owner-signed operation.
Hosted customer connections
The server creates the request and stores its verification record in the initiating customer's
server session. Send only request.url to the browser:
const { request, verification } = await botanary.connections.begin({
redirectUri: 'https://your-app.example/botanary/callback',
scopes: ['accounts:read'],
});After the owner returns, atomically consume that server session's verification record and pass the
callback URL to botanary.connections.exchangeCallback(callbackUrl, verification). It checks state,
callback destination and result shape before exchanging the PKCE code. Store returned tokens on the
server. createConnectionClient({ accessToken }).account.context() reads only the selected account.
Neither an app key nor a connection token is an owner signer. These clients are not browser exports.
Request balances:read and activity:read in consent and the app key to use
customer.account.balances() and customer.account.activity({ limit: 25, cursor }).
Both select the consented account and chain without caller account overrides. Balances
retain exact amountRaw strings and nullable metadata/prices. building is unknown,
not a confirmed empty account; degraded keeps stored holdings. readAt is calculation
time, not last chain sync. Activity pages expose recorded Botanary audit events with
settlement: 'unverified', not complete chain history or confirmed operation receipts.
Pagination cursors bind to the connection and consent version. These safe reads support
the client's bounded retry, timeout and cancellation policy.
connections.refresh(refreshToken) rotates both tokens. Coordinate concurrent refreshes with shared
server storage: reuse revokes the token family, including its newest access token. The SDK never
retries refresh or code exchange automatically. Reconnect after an uncertain token response. Use a
replacement active same-app/environment key with every consented scope during key rotation.
connections.list(), get(id) and revoke(id) manage connection API access. Revocation does not
revoke existing on-chain grants. For a local consent page, configure consentOrigin explicitly, for
example http://localhost:3000; the default is https://app.botanary.xyz. The API base URL is configured
separately. This custom hosted protocol does not claim OAuth standards compliance.
To request access for a registered agent, pass agentRegistrationId to connections.begin with
accounts:read, agents:read and the additional customer scopes your integration needs. The app key
must also carry agents:write and connections:write. The owner reviews the exact agent key and
chooses its API access separately. Account consent alone does not connect the agent. Use a distinct
agent key for each customer connection. agents.get(id) reports the current API access status and
connection ID; neither field proves an on-chain grant.
Customer operation requests
Request operations:write and operations:read in both the server key and customer consent.
Persist an idempotency key with each customer intent before making the request. The selected
connection fixes the account and network; all amounts are positive integer base-unit strings.
import { createConnectionClient } from '@botanary/sdk';
const customer = createConnectionClient({ accessToken: process.env.BOTANARY_CONNECTION_TOKEN! });
const operation = await customer.operations.create({
kind: 'send', chainId: 84532, assetRef: 'eip155:84532/slip44:60',
amountRaw: '1000000000000000', to: '0x1111111111111111111111111111111111111111', gasMethod: 'native',
}, { idempotencyKey: 'persisted-customer-action-id' });
const current = await customer.operations.get(operation.id);Send and same-chain swap intents return a durable operation ID and an origin-checked
approvalUrl. The hosted /sign screen reviews the selected account, exact amount, fees and
expiry before local owner signing and relay. App and connection credentials cannot build or
sign for the owner. A confirmed or failed result is returned only when its non-simulated
receipt matches the operation, submission attempt, UserOp hash, account, chain and finalized
execution. Confirmed effects also match the approved asset identities and exact base-unit amounts.
Local API and browser preview tests do not establish a deployed
customer journey or settlement.
operations.wait(id, { timeoutMs: 60000, signal }) polls only the existing operation. A timeout
or access error preserves BotanaryError.operationId; a create transport error preserves its
idempotencyKey. Reuse the same creation key and identical intent to recover a lost response.
Never start another financial action merely because a wait timed out. The SDK accepts keys of
8 to 128 letters, digits, underscores or hyphens. Read/build/relay acknowledgments remain
settlement: 'unverified'. Keep submission.attemptId, submission.userOpHash and any
transactionHashHint while status is unresolved. The hint helps discovery and is not proof.
After a reversal, the same operation returns to unresolved; reload it by ID and do not request
a fresh signature for the earlier uncertain attempt.
Public grants
Register and connect a distinct agent key, then use client.grants.create, list, get and revoke from the server-held app client. Creation takes a complete Test-only grant policy and an idempotency key. The response includes a durable grant ID, owner approval URL, permanent permission ID, exact compiled calls, independent API access status and strictly observed chain state. Preserve the ID and original idempotency key after uncertainty. Revocation is another durable owner operation and is not complete until its exact on-chain effect is finalized. The browser entry exports only the closed parsePublicGrant projection and public types, never an app credential constructor.
Webhook endpoints and raw-body verification
This package includes server-side webhook controls. Give an app key webhooks:read
for endpoint/history reads and webhooks:write for creation, changes, rotation, tests and redelivery.
Console owners, admins and developers can manage endpoints; viewers can inspect them.
Destinations must use public HTTPS on port 443. Test and Live endpoints and secrets are separate.
const created = await client.webhooks.create({
url: 'https://your-app.example/botanary-webhook',
eventTypes: ['connection.created', 'connection.updated', 'connection.revoked'],
});
// Store created.secret on the receiver server immediately. Reads never return it again.
const delivery = await client.webhooks.test(created.endpoint.id);
const history = await client.webhooks.deliveries(created.endpoint.id, { limit: 50 });
const details = await client.webhooks.delivery(created.endpoint.id, delivery.id);webhooks.list, get, update, delete, rotate and redeliver use the same instance's
app/environment credential. Delivery history uses nextCursor for stable pagination. A deleted
endpoint keeps scoped delivery history and loses its encrypted signing secrets. A pending delivery
cannot be manually redelivered. Delivery/rotation creation is not automatically retried after an
ambiguous response; inspect history or rotate again explicitly if a one-time secret response is lost.
Use the standalone server export in your HTTP handler, before any JSON body parser:
import { WebhookVerifier } from '@botanary/sdk/webhooks';
const verifier = new WebhookVerifier({
secrets: process.env.BOTANARY_WEBHOOK_SECRET!,
appId: process.env.BOTANARY_APP_ID!,
environment: 'test',
});
// request is the incoming Fetch API Request from your server framework.
const rawBody = new Uint8Array(await request.arrayBuffer());
const event = verifier.verify(rawBody, request.headers);For Express, install express.raw({ type: 'application/json', limit: '1mb' }) on this route before
express.json() and pass its Buffer plus req.headers. Never parse and reserialize the signed body.
Verification checks the HMAC, default five-minute past/future timestamp tolerance, event ID, payload
version, app and environment. Signature headers follow
Standard Webhooks.
Commit the event ID to a durable transactional inbox before returning a 2xx acknowledgement; a retry
can deliver the same event again. Deduplicate on (appId, environment, event.id). The unsigned
botanary-delivery-id header is diagnostic, not a deduplication or authorization boundary. Event IDs
and bodies survive redelivery, while signature timestamps change. Events can arrive out of order:
fetch current resource state when necessary, and never infer financial success from a diagnostic
webhook.test event or an HTTP relay acknowledgement.
webhooks.rotate(id, overlapSeconds) accepts zero to 86400 seconds. During overlap the sender emits
both signatures; receivers may configure secrets: [newSecret, oldSecret] during their own migration.
Remove the old receiver secret when the overlap ends. A second positive overlap is refused until the
first ends. Zero overlap immediately removes all previous sender keys. Rotating secrets does not
change an event's identity. Automatic delivery retries last up to three days, honor bounded
Retry-After, refuse redirects, and disable a matching endpoint on HTTP 410. Simulated local transport
is labeled in attempts and never completes real webhook onboarding progress.
Connection lifecycle and durable approval required/resolved producers are implemented in this checkpoint. Financial operation and grant notification producers still require authoritative settlement integration. Approval resolution by itself does not establish financial success.
Embedded account proof preview
The package exports client.embedded.challenge({ jwt, signerAddress, chainId, accountId?, scopes })
and client.embedded.complete({ challenge, signature }). Keep the original parsed challenge in the
authenticated customer BFF session; completion validates the returned exact account and scopes against it.
A lost completion response requires a fresh challenge, never a retry to recover the bearer.
createEmbeddedClient({ accessToken }) exposes session(), account(), portability(), revoke(),
agents.list(), agents.approve(registrationId), grants.create/list/get/revoke and the existing
financial operation methods.
The same public-grant resource is available under the developer-first mandates.create/list/get/revoke
name. grants remains a compatibility alias because the wire resource and durable grantId are unchanged.
An embedded mandate response must carry null approval URLs and is parsed without any consumer-app origin.
client.embedded.list(), .get(bindingId) and .revoke(bindingId) inspect or revoke public app access.
Credentials remain valid for the issuing key's lifetime. They still fail immediately when the session,
binding or key is revoked, the app or identity configuration is disabled or changed, or the exact owner
and account relationship no longer verifies on chain.
The browser entry exports createEmbeddedProofAdapter({ provider, sessionId, getSessionId, review }).
Use a non-secret customer session revision. The review callback must display the purpose, exact account,
app, environment, scopes and expiry before approval. The helper returns only { challengeId, signature }.
Server app keys, identity JWTs and owner bearers stay in the BFF.
Same-key account claims preserve the exact account; identity login alone cannot replace a signer. Stored signer/current sole on-chain owner mismatches are unsupported in this first profile. See the embedded account portability guide for provider-dependent key export and recovery limits. Final packed artifacts are refreshed separately after the complete embedded integration stabilizes.
Embedded financial access
createEmbeddedClient({ accessToken, baseUrl? }) stays on the authenticated application server.
The revocable embedded credential admits balances(), gasMethods(), activity({ cursor?, limit? }) and
operations.create(intent, { idempotencyKey }), operations.get(id),
operations.build(id, version), operations.submit(id, { attemptId, signed }) and
operations.reject(id). Grant create and revoke are separate resources:
grants.create(input, { idempotencyKey }) after agents.approve(registrationId).
Each grant create or revoke retains a grant_enable / grant_revoke operation on this binding;
the owner signs that operation through operations.build / operations.submit. No on-chain
authority exists until that operation is confirmed and observed. App keys and JWTs cannot read
customer financial data or authorize builds.
Persist the intent and idempotency key before creation, then retain the operation ID, attempt ID, original build hash and completed UserOp hash before relay. Submission has no automatic retry. After a lost response, read the original operation ID. Fresh proof to the same subject and exact account can read prior operations even after key/configuration/binding changes; the operation keeps its original attribution. Renewed access cannot revive an invalidated build or re-sign submitted work.
Browser-safe parseEmbeddedOperation, parseEmbeddedOperationBuild, parseEmbeddedBalances and
parseEmbeddedActivity project embedded data without a hosted connection or approval URL.
Pass the expected binding/account/app context at the BFF boundary. For old operation recovery,
bindingVersion is the original operation version; do not require it to equal the new session version.
The build parser checks the transport envelope only. Use @botanary/wallet canonical review to
validate calldata, deployment, hashes, fee limits and exact swap terms before invoking a signer.
Server code never signs. The existing wallet flow signs a raw USDC permit before the EIP-191 UserOp
when that explicit signer capability is available.
Only a finalized nonsimulated financial-effect receipt proves settlement. A relay acknowledgment or simulated receipt does not. Simulation contributes no real customer onboarding or paid usage. React inline mode, authenticated embedded BFF examples and final archive refresh remain Task 4; existing package archives do not contain these new source exports yet.
What ships in the npm tarball
Allowlisted declarations (no declaration maps), one bundled and minified JavaScript file per
documented entry (., ./browser, ./webhooks, ./types; no source maps, no comments), this
README, the changelog, and LICENSE. No .ts/.tsx source, no tests, no
fixtures, no build config. pnpm run audit:artifact verifies the exact tarball before publication
and -- --registry <file.tgz> re-verifies the downloaded copy after.
Every runtime export is classified in
docs/specs/2026-09-10-protected-sdk-integration-boundary.inventory.md:
all integration except createEmbeddedProofAdapter, which is a signer-defense check with a
written threat argument. No product, routing, policy, simulation, reconciliation, billing or provider
logic is distributed - it lives behind the reviewed developer API. Minification is not concealment;
the inventory is. bsk_* keys are server-only and never enter @botanary/sdk/browser.
