brickken-sdk
v0.2.0
Published
TypeScript SDK for the Brickken tokenization and agentic APIs (Dapp, x402/ERC-8004, RAMS/ERC-8226)
Readme
brickken-sdk
TypeScript SDK for the Brickken platform. One typed client for the Dapp API (tokenization, STOs, security tokens), the Agentic API (x402 payments, ERC-8004 identity and reputation, agent-owned tokens), and RAMS mandates (ERC-8226).
Runs on Node 20+, in browsers, and on edge runtimes. The core has one runtime dependency.
npm install brickken-sdkQuickstart
The Dapp API, with an API key:
import { Brickken } from 'brickken-sdk'
const bkn = new Brickken({ env: 'sandbox', apiKey: process.env.BRICKKEN_API_KEY })
const token = await bkn.tokenization.info({ tokenSymbol: 'EXMPL' })
const offerings = await bkn.sto.list({ tokenSymbol: 'EXMPL' })The Agentic API, paying per call with x402:
import { Brickken } from 'brickken-sdk'
import { fromPrivateKey } from 'brickken-sdk/adapters/private-key'
const bkn = new Brickken({
env: 'sandbox',
signer: fromPrivateKey(process.env.BRICKKEN_PRIVATE_KEY!),
payment: {
maxAmountBaseUnits: '50000', // ceiling in the asset's base units
onPaymentRequired: async quote => {
console.log(`about to pay ${quote.displayPrice}`)
return true
},
},
})
const agent = await bkn.agent.register(
{
chainId: '84532',
name: 'Research Agent',
image: 'https://example.com/agent.png', // required, must be publicly reachable
serviceName: 'A2A',
serviceEndpoint: 'https://agent.example/.well-known/agent-card.json',
},
{ execute: true }, // prepare is free; this also pays and sends
)
console.log(agent.info?.agentUuid) // save it: set-uri and set-metadata need it
console.log(agent.payment?.settlement) // what the payment settled toKYC and agent reads
KYC link creation requires an API key. It can create an investor record and send an invitation email, so treat the returned Sumsub URL as sensitive:
const result = await bkn.kyc.createLink({
email: '[email protected]',
needKyc: true,
})Agent getters accept either an API key or x402:
const agents = await bkn.agent.list({ chainId: '84532', limit: 20, offset: 0 })
const agent = await bkn.agent.get({ agentUuid: agents.agents[0]!.uuid })
const history = await bkn.agent.transactions({ agentUuid: agent.uuid, limit: 20, offset: 0 })References can use agentUuid, or agentId together with chainId. In x402 mode, agent.list additionally requires ownerWalletAddress, and the SDK verifies locally that the payment signer owns that wallet. Pagination limits are 1-100 and offsets are non-negative.
Two credentials, and they are alternatives
Brickken accepts an API key or an x402 payment, never both on one request. When x-api-key is present the API skips the payment path entirely, which means no payment is taken and brickken-relayed execution becomes unavailable.
| | apiKey | signer, no key |
| --- | --- | --- |
| KYC link creation | yes | no |
| Agent getters | yes | yes |
| Dapp API (tokenization, STOs, reads) | yes | no |
| Agentic API, client-signed | yes | yes |
| Agentic API, client-broadcast | yes | yes |
| Agentic API, brickken-relayed | rejected | yes |
| RAMS reads and typed data | yes | yes |
The SDK derives the mode from what you pass and refuses illegal combinations locally, before any request leaves:
const bkn = new Brickken({ apiKey: 'key', signer })
await bkn.agent.register(input, { executionMode: 'brickken-relayed' })
// throws RelayedRequiresPaymentError, naming both fixes, with no network round tripYour private key is never sent anywhere. It signs the x402 payment authorization, client-signed transactions, and RAMS typed data, all in your process.
Prepare is free; sending costs money
Every write follows prepare → sign → send. Preparing costs nothing, so the SDK defaults to prepare-only and execute: true is opt-in.
const prepared = await bkn.agentToken.create({ chainId: '84532', name: 'Agent Token', symbol: 'AGT' })
prepared.txId // the prepared transaction id
prepared.transactions // unsigned transactions
prepared.x402Requirements // what sending will cost
prepared.sent // undefined: nothing was sent
const done = await bkn.agentToken.create(
{ chainId: '84532', name: 'Agent Token', symbol: 'AGT' },
{ execute: true, waitForReceipt: true }, // waitForReceipt needs rpcUrl
)
done.payment?.requirement.amount
done.deployedAddress // recovered from the receiptPayment metadata is always a sibling field. It is never mixed into the response body.
Three execution modes
executionMode decides who signs the transaction and who puts it on chain. Each write has a sensible default, so most callers never set it.
| Mode | Signs | Broadcasts | Needs |
| --- | --- | --- | --- |
| client-signed | you | Brickken | a signer with signTransaction |
| client-broadcast | you | you, over rpcUrl | a signer with signTransaction, plus rpcUrl |
| brickken-relayed | Brickken's relayer | Brickken | an x402 payment, so no apiKey |
client-signed is the default for Dapp writes: you sign locally and hand the signed transaction to Brickken to broadcast.
client-broadcast submits the signed transaction yourself through your own JSON-RPC endpoint, then confirms { txId, txHash } back to Brickken. Use it when you need to own the mempool path — your own node, a private relay, or your own gas and nonce policy.
const bkn = new Brickken({ apiKey: 'key', signer, rpcUrl: 'https://your-node.example' })
const done = await bkn.tokenization.create(input, {
execute: true,
executionMode: 'client-broadcast',
signerAddress: await signer.address(),
})
done.sent?.transactionHashes[0] // the hash your node assignedIt takes exactly one prepared transaction: the SDK rejects an empty response or batch locally rather than reporting a partial execution. If your node refuses the transaction — for example nonce too low or insufficient funds — you get an RpcRejectedError carrying the node's own code and message. An already known response is recovered automatically from the signed transaction's deterministic hash.
After broadcasting, the SDK retries the Brickken confirmation while the backend RPC catches up with transaction propagation. If confirmation still fails, BroadcastConfirmationError preserves both .txId and .txHash; resume only that step without signing or broadcasting again:
import { BroadcastConfirmationError } from 'brickken-sdk'
try {
await bkn.tokenization.create(input, options)
} catch (error) {
if (error instanceof BroadcastConfirmationError) {
await bkn.tx.send({ txId: error.txId, txHash: error.txHash })
}
}Spending controls
x402 payments are non-refundable, and an autonomous agent with no policy is an unbounded spender. Three controls, all optional but strongly recommended:
new Brickken({
signer,
payment: {
maxAmountBaseUnits: '50000', // hard ceiling; throws before signing
onPaymentRequired: async quote => BigInt(quote.amountBaseUnits) <= 20_000n,
onPayment: record => audit.log(record), // called after settlement
},
})The SDK also refuses to pay the same txId twice, and every value it signs — chain, asset, transfer rail, amount, recipient, authorization window — is read from the live PAYMENT-REQUIRED header rather than hardcoded.
Signers
The SDK defines a minimal contract and ships three adapters:
import { fromPrivateKey } from 'brickken-sdk/adapters/private-key' // raw hex key
import { fromEthers } from 'brickken-sdk/adapters/ethers' // ethers v6
import { fromViem } from 'brickken-sdk/adapters/viem' // viem Account or WalletClientethers and viem are optional peer dependencies, imported only from their own subpath. A consumer using neither installs neither.
interface Signer {
address(): Promise<Address>
signTypedData(data: TypedDataDefinition): Promise<Hex>
signTransaction?(transaction: TransactionRequest): Promise<Hex> // optional
}signTransaction is optional by design: a KMS, MPC, or browser-wallet signer can sign typed data but not a raw transaction, and that is enough for the whole brickken-relayed path and every RAMS EIP-712 flow. Bring your own by implementing the three methods.
RAMS mandates
The four lifecycle operations — grantMandate, revokeMandate, extendMandate, setOperator — accept either direct authorization or a principal EIP-712 signature that lets any relayer send. With authorize: 'signature' the SDK fetches the typed data, signs it, and resubmits the same parameters and the same deadline, which is the constraint most easily broken by hand:
await bkn.rams.grantMandate(
{
agent, principal, asset, identityRef,
validUntil: '1893456000',
maxTransactionValue: 'max', // alias for 2^256-1
maxCumulativeValue: '1000000', // raw base units, no decimal scaling
actions: ['0x23b872dd'], // bytes32 or a bare selector
},
{ authorize: 'signature', executionMode: 'brickken-relayed', execute: true },
)Two asymmetries the types make visible:
- RAMS caps are
RawBaseUnits— no decimal scaling, because the backend cannot know an arbitrary asset's decimals. Agent-token amounts areHumanAmountand are scaled. - Only those four operations can be relayed.
execute,setExecutorAction,freezeAgent,unfreezeAgent,grantPrincipal, andrevokePrincipalrequiremsg.senderto be the role holder, so the SDK rejectsbrickken-relayedon them locally.
RAMS runs on Ethereum Sepolia (11155111), which every RAMS call defaults to.
Errors
Branch on the class, never on the status code — the API reports the same missing-key cause as 401 on one endpoint and 400 on another.
| Class | Meaning |
| --- | --- |
| ValidationError | refused locally; no request was made |
| AuthError | credential missing, wrong, or not accepted here |
| RelayedRequiresPaymentError | relayed execution with an API key configured |
| CreditsExhaustedError | plan balance for that write method is spent; carries .method |
| UnauthorizedTokenSymbolError | your key did not tokenize that symbol |
| PaymentRequiredError | x402 payment needed and none could be authorized |
| PaymentDeclinedError | your ceiling or hook refused to pay |
| RateLimitError | 429; carries .retryAfterSeconds |
| ApiError | any other non-2xx; carries .status and .body |
| NetworkError | transport failure or timeout |
| RpcRejectedError | the configured JSON-RPC node rejected a call |
| BroadcastConfirmationError | the transaction was broadcast but backend confirmation failed; carries .txId and .txHash |
| TxRevertedError | the transaction was mined and reverted |
429, 5xx, and transport failures retry with jittered backoff. A send for which a payment was already authorized is never retried; if it fails, the error carries .payment so you can reconcile the charge.
Escape hatch
Any backend method is reachable the day it ships, without waiting for a typed namespace:
await bkn.tx.prepare({ method: 'someNewMethod', chainId: '8453', custom: 'value' })
await bkn.tx.sign(transactions)
await bkn.tx.send({ txId, signedTransactions })
await bkn.tx.status({ txId })
await bkn.tx.waitForReceipt({ txHash, chainId: '8453' })Configuration
new Brickken({
env: 'sandbox', // 'sandbox' | 'production'
baseUrl, // overrides env
apiKey,
signer,
rpcUrl, // client-broadcast writes, receipt polling, and on-chain token metadata
timeoutMs: 30_000,
retry: { attempts: 3, baseDelayMs: 500, jitter: true },
payment: { /* see above */ },
fetch, // inject your own
})Brickken.fromEnv(process.env) reads BRICKKEN_API_KEY, BRICKKEN_ENV, BRICKKEN_BASE_URL, and BRICKKEN_RPC_URL, plus the BKN_* aliases. It deliberately does not read BRICKKEN_PRIVATE_KEY: turning a key into a signer is an explicit choice, never an implicit one.
| Environment | Base URL |
| --- | --- |
| sandbox | https://api.sandbox.brickken.com |
| production | https://api.brickken.com |
Sandbox and production are separate deployments with separate databases; a key for one is unknown to the other.
Prerequisites the API does not expose
Two things fail at request time and are invisible beforehand:
- The wallet you pass as
signerAddressmust be whitelisted by Brickken before any prepare accepts it. Request it together with your API key. - An API key may only act on token symbols whose tokenizer email matches a
newTokenizationperformed under that same key. Anything else is anUnauthorizedTokenSymbolError.
Each write method also carries its own credit balance, so one method can run out while others still work.
Preparing is free, but not unlimited: the API caps how many prepared transactions
a wallet may have outstanding and answers 429 with
Too many outstanding prepared transactions for this wallet. The SDK honours the
Retry-After it comes with, but only for the configured number of attempts —
three by default. After that it throws RateLimitError, carrying
.retryAfterSeconds. A loop that prepares without sending will therefore slow
down and then fail, so treat the quota as a limit to respect rather than a delay
to wait out.
Development
pnpm install
pnpm test # unit tests, no network
pnpm typecheck
pnpm build
pnpm audit:package # pre-publish: tarball contents, secret scan, export resolution
pnpm gen:manifest # refresh the OpenAPI snapshot the drift test compares against
pnpm test:integration # opt-in, needs BRICKKEN_SANDBOX_API_KEY and/or BRICKKEN_SANDBOX_PRIVATE_KEYpnpm verify runs typecheck, unit tests, build, package audit, and smoke install. Manifest generation and the live integration suite remain explicit because they access external services.
Publishing goes through the v* tag workflow in .github/workflows/publish.yml, which adds --provenance. Provenance needs CI OIDC, so it is not set in publishConfig — that would break a local npm publish.
pnpm test includes a parity suite asserting every agentic and RAMS request body is byte-identical to [email protected], whose behaviour is already validated against the live backend, and a drift test asserting the SDK reaches every operation in its generated OpenAPI manifest.
License
MIT
