@loopprotocol/sdk-byoaa
v0.1.0-alpha.14
Published
Bring Your Own Attested Agent — TypeScript SDK for submitting bank-attested receipts to Loop Protocol from inside an attested enclave.
Maintainers
Readme
@loopprotocol/sdk-byoaa
Bring Your Own Attested Agent — TypeScript SDK for submitting bank-attested receipts to Loop Protocol from inside an attested enclave.
Status: alpha candidate (
0.1.0-alpha.14). On-chain primitives are deployed on devnet (loop-shoppingsubmit_attested_receipt+BankAttestedReceiptPDA). Mainnet deploy gated on Bundle 2 (loop-protocol PR #27 + #28 + #29) + the mainnet ShoppingState bump repair.
What this is
Loop Protocol's bank-data layer is built on the BYOAA model: users (or their hosted-agent provider — Anthropic Claude with Computer Use, OpenAI Operator, self-hosted Nitro Enclaves, etc.) run an agent inside an attested enclave that:
- Logs into a bank (the agent has the credentials; Loop never touches them).
- Reads transactions.
- Constructs a
BankReceiptand signs it with the enclave-bound session key. - Submits the receipt to the on-chain
submit_attested_receiptinstruction.
This package provides the TypeScript primitives for steps 3 and 4. The enclave + bank scraping is the user's responsibility (see the reference implementation at loop-byoaa-reference-agent).
Spec: docs/roadmap/08-byoaa-sdk.md.
Install
npm install @loopprotocol/[email protected]
# optional peer deps for /solana and /runtime-sandbox surfaces (Node 20+ runtime):
npm install @coral-xyz/anchor @loopprotocol/sdk @solana/web3.js@loopprotocol/sdk-byoaa default install is lightweight: production dependencies are limited to browser-safe hashing, encoding, and policy-signature primitives. Solana/on-chain helpers require the optional peer packages above; REST-only and receipt-only consumers do not need to import those surfaces.
Runtime support
0.1.0-alpha.14 publishes explicit entry points:
@loopprotocol/sdk-byoaa/receipt— browser-safe receipt helpers only; no Solana, Anchor, or Nodecryptoimports. Node 18+ compatible.@loopprotocol/sdk-byoaa/rest— REST/API-key client surface. Node 18+ and browser-safe.@loopprotocol/sdk-byoaa/proof— browser-safe shape-only proof helpers; no Solana, Anchor, Buffer, or Nodecryptoimports. Current proof verification is explicitlyshape_only; it is not full cryptographic authenticity verification. Node 18+ compatible.@loopprotocol/sdk-byoaa/solana— Node 20+ only. Uses optional peer packages (@coral-xyz/anchor,@loopprotocol/sdk,@solana/web3.js) for Solana/on-chain helpers: network config, PDA derivation, instruction encoding, receipt submission, and on-chain receipt verifier/account decoders.@loopprotocol/sdk-byoaa/runtime-sandbox— Node 20+ hosted Runtime Sandbox signer adapter. Also uses the same optional peer packages and implementsEnclaveSignerover a scoped bearer-token broker proxy; the SDK does not include any public token.@loopprotocol/sdk-byoaa— browser-safe alpha aggregate root with receipt, REST, and shape-only proof helpers. Node 18+ compatible. Use/solanafor chain/RPC helpers and/runtime-sandboxfor hosted signer access.
For browser/frontend code, prefer the smallest subpath. Do not ship REST SDK keys to browsers.
Mainnet and localnet fail closed in config resolution:
network: "mainnet-beta"throws until BYOAA mainnet shopping program ids are live. Controlled dry runs must pass explicit non-placeholder program ids plusdangerouslyAllowUnreleasedMainnet: true.network: "localnet"requires explicitshoppingProgramIdandvaultProgramId; the SDK will not silently reuse devnet ids for local transaction construction.
Security and API keys
Runtime Sandbox signer tokens are bearer credentials too. They must be scoped per user/profile/runtime, expiry-bound, and revocable. Do not embed a shared public token in apps or docs. A logged-in app should retrieve a short-lived runtime token from the user's Loop profile/session, then inject it into RuntimeSandboxSigner via tokenProvider.
import { RuntimeSandboxSigner } from "@loopprotocol/sdk-byoaa/runtime-sandbox";
const enclaveSigner = new RuntimeSandboxSigner({
brokerUrl: "https://runtime-api-devnet-v0.looplocal.io/api/v1/runtime-sandboxes/byoaa-devnet-nitro-v0/broker",
tokenProvider: async () => {
const res = await fetch("/api/me/runtime-sandbox-token", { method: "POST" });
if (!res.ok) throw new Error("runtime token unavailable");
return (await res.json()).token;
},
});The signer implements the existing EnclaveSigner interface used by AttestedReceiptSubmitter; it maps getSessionPubkey() and signInstruction(...) to the Runtime Sandbox broker wire protocol. For manual probes, the broker proxy accepts POST at the broker URL above with { "method": "getSessionPubkey" } or the documented signInstruction payload.
Plug-and-play hosted Runtime Sandbox submitter
Agents should prefer the helper below instead of manually wiring the signer, live session pubkey, and submitter. On-chain attestation requires a paid/runtime entitlement; free-tier tokens must fail closed at the broker before signing.
import { PublicKey } from "@solana/web3.js";
import { createRuntimeSandboxSubmitter } from "@loopprotocol/sdk-byoaa/runtime-sandbox";
const runtime = await createRuntimeSandboxSubmitter({
brokerUrl: "https://runtime-api-devnet-v0.looplocal.io/api/v1/runtime-sandboxes/byoaa-devnet-nitro-v0/broker",
vault: new PublicKey(process.env.LOOP_VAULT_PUBKEY!),
network: "devnet",
tokenProvider: async () => {
const res = await fetch("/api/me/runtime-sandbox-token", { method: "POST" });
if (!res.ok) throw new Error("runtime token unavailable");
return (await res.json()).token;
},
});
console.log("live Runtime Sandbox session", runtime.sessionPubkey.toBase58());
const result = await runtime.submitter.submit(receipt);If submit() returns transaction_send_failed with insufficient funds, SessionAccountInvalid, or an entitlement/auth error, do not ask the end user to debug Solana. Refresh the runtime bundle, retrieve a paid runtime token, or ask the operator control plane to fund/register the current session.
LoopByoaaClient SDK keys are bearer credentials. Handle them like passwords:
- keep SDK keys in server-side environment variables or a secret manager;
- do not ship SDK keys to browser/frontend bundles;
- do not log or serialize SDK keys, clients, headers, request objects, or errors containing request data;
- rotate any key that appears in logs, snapshots, telemetry, or crash reports.
By default, the REST client derives the Loop API host from the key environment. If you pass a custom baseUrl, the SDK will send Authorization: Bearer <apiKey> to that host. Custom hosts are blocked unless you explicitly opt in:
import { LoopByoaaClient } from "@loopprotocol/sdk-byoaa";
const loop = new LoopByoaaClient({
apiKey: process.env.LOOP_BYOAA_API_KEY!,
baseUrl: "https://trusted-test-gateway.example.com",
dangerouslyAllowCustomBaseUrl: true, // sends bearer credentials to this host
});Only use dangerouslyAllowCustomBaseUrl for trusted test harnesses or approved federated deployments.
Quick start
Official authority loop (copy/paste; no custom harness)
Use the packaged examples/authority-loop.ts as the official adopter path. It uses only declared LoopByoaaClient methods and performs the complete authority sequence:
loop.bridge.registerAgentregisters with a liveagent_key.loop.permissions.grantgrants explicit bounded authority for that agent.loop.decisions.requestrequests a decision withpermission_key.- For a review result,
loop.approvals.createLinkproduces the human Authority Center URL. loop.approvals.waitForDecisionpolls without approving or fabricating proof.loop.approvals.resumeresumes only after a real human approval receipt exists.loop.audit.exportPackexports a top-level portable audit pack and validates required live fields.
npm install @loopprotocol/[email protected] [email protected]
export LOOP_BYOAA_API_KEY=<dev-sdk-key>
export LOOP_PRINCIPAL_ID=<dev-principal-id>
npx tsx node_modules/@loopprotocol/sdk-byoaa/examples/authority-loop.tsOptional: set LOOP_AUTHORITY_CENTER_URL only when an operator supplies a different approved Authority Center origin. Expected outcomes are either an immediate allow, a printed approval URL followed by a real human approve/resume, or a clear fail-closed error (deny, missing review_request_id, timeout/denial/expiry, missing final decision ref, or invalid audit pack). This example never approves for the human, fabricates a receipt/proof, or mutates production. No copied snippets, endpoint guesses, or custom troubleshooting harness are required.
Credentials are split by runtime:
LOOP_BYOAA_API_KEYis the server-side KYA/BYOAA REST credential used by the authority loop and dev-gateway smoke.LOOP_PRINCIPAL_IDis required by the authority-loop decision request so the gateway can bind authority to an existing dev principal.LOOP_RUNTIME_TOKENis only for the separately documented hosted Runtime Sandbox signer flow; the REST authority loop and dev-gateway read smoke do not use it.
Official smokes
From a fresh consumer directory, install the exact package plus its documented TypeScript runner, then run the shipped credential-free installed-artifact smoke. The smoke uses only public package exports and exercises both CJS and ESM REST imports, canonical dev-host derivation, and bearer-key serialization guardrails. It does not build, pack, read source, or make a network request:
npm init -y
npm install @loopprotocol/[email protected] [email protected]
npx tsx node_modules/@loopprotocol/sdk-byoaa/scripts/packed-artifact-smoke.tsExpected result: packed CJS+ESM artifact smoke passed. A missing public REST export, key-parser mismatch, non-canonical dev host, or bearer-key serialization/reflection failure exits non-zero.
Run the credentialed dev-gateway parity smoke with a scoped dev SDK key:
LOOP_BYOAA_API_KEY=<dev-sdk-key> npx tsx node_modules/@loopprotocol/sdk-byoaa/scripts/dev-gateway-smoke.tsThe smoke derives https://dev-api.kya.looplocal.io from the dev key and calls only the declared harmless loop.agents.list({ limit: 1 }) route. It performs no registration, decision, approval, resume, audit export, or production mutation. Expected result is one JSON line with "ok":true, "environment":"dev", the route, item count, and "mutation_performed":false.
It fails closed when LOOP_BYOAA_API_KEY is missing/malformed or is a staging/production key. The official smoke refuses custom gateway hosts so a bearer key cannot be redirected away from the canonical dev gateway.
CI never receives a live key and therefore runs the explicit non-live gate only:
LOOP_BYOAA_SMOKE_MODE=skip npx tsx node_modules/@loopprotocol/sdk-byoaa/scripts/dev-gateway-smoke.tsThis is the official no-troubleshooting quickstart/smoke path: use these commands and package artifacts directly; do not build a custom harness or invent endpoints.
Maintainers working from a source checkout use the separate npm run smoke:fresh-consumer gate to build, pack, install, production-audit, import-smoke, and Vite-build the candidate tarball. That maintainer command requires source and development tooling and is not an installed-package adopter command.
Discover Loop authority metadata and register an external agent
Loop publishes WorkOS/Auth.md-compatible discovery surfaces at looplocal.io, but the SDK keeps Loop-native proof semantics: AuthJWT identity, KYA decisions, BYOAA receipts, Authority Center approval, Clearing House audit refs, and public/private receipt modes.
Discovery calls never send your SDK bearer key:
import {
LoopByoaaClient,
buildLoopAgentRegisterInput,
discoverLoopAuthority,
} from "@loopprotocol/sdk-byoaa";
const authority = await discoverLoopAuthority();
console.log(authority.protectedResource.loop_authority?.receipt_modes);
const loop = new LoopByoaaClient({
apiKey: process.env.LOOP_BYOAA_API_KEY!,
});
const agentInput = buildLoopAgentRegisterInput({
client_id: "acme-procurement-agent",
client_name: "Acme Procurement Agent",
redirect_uris: ["https://acme.example/callback"],
scope: "invoices:read payments:write",
software_statement: "sha256:attested-build",
jwks_uri: "https://acme.example/.well-known/jwks.json",
});
const agent = await loop.bridge.registerAgent(agentInput);The WorkOS/Auth.md adapter is only an input mapper. It does not add a WorkOS-hosted runtime dependency and does not replace Loop's AuthJWT/KYA/BYOAA/Clearing House proof spine.
Construct + validate a receipt (no on-chain interaction)
import {
deriveBankTxnId,
normalizeMerchantName,
validateReceipt,
type BankReceipt,
} from "@loopprotocol/sdk-byoaa/receipt";
const merchantNameRaw = normalizeMerchantName(
" starbucks #12345 SEATTLE WA "
); // → "STARBUCKS #12345 SEATTLE WA"
const postedAt = BigInt(Math.floor(Date.now() / 1000));
const bankTxnId = deriveBankTxnId({
merchantNameRaw,
amountCents: 875n,
postedAt,
accountLast4: 4242,
});
const receipt: BankReceipt = {
bankTxnId,
merchantNameRaw,
mcc: 5814,
amountCents: 875n,
postedAt,
accountLast4: 4242,
};
const validation = validateReceipt(receipt);
if (!validation.ok) {
throw new Error(`Bad receipt: ${validation.error}`);
}Submit from inside an attested enclave
AttestedReceiptSubmitter signs and submits one or more AttestedReceipt payloads from an enclave-bound session signer. It validates receipt bounds client-side, derives the receipt PDA, builds the submit_attested_receipt instruction, and sends it through the configured Solana connection.
AttestedReceiptVerifier is exported from @loopprotocol/sdk-byoaa/solana. It fetches recorded receipt PDAs and verifies the on-chain fields against the expected bank transaction id, merchant, amount, timestamp, and PCR/session metadata. It is a read/verify helper; it does not trust off-chain scraper output by itself.
Package surface
| Export | Purpose |
|---|---|
| BankReceipt | In-memory shape of one bank transaction |
| deriveBankTxnId(input) | Stable 32-byte sha256 over normalized fields. Re-fetches collide on the same on-chain PDA. |
| normalizeMerchantName(raw) | Canonical form: trim → collapse whitespace → uppercase → strip non-ASCII |
| validateReceipt(receipt, nowSeconds?) | Front-runs the on-chain handler's bounds checks; cheap, no I/O |
| MAX_RECEIPT_CENTS | $100M cap (mirrors on-chain) |
| MAX_MERCHANT_NAME_RAW_LEN | 64 bytes (mirrors on-chain) |
| MAX_RECEIPT_AGE_SECONDS | 365 days (mirrors on-chain) |
| AttestedReceiptSubmitter | Builds/signs/sends submit_attested_receipt transactions for enclave-bound sessions |
| createRuntimeSandboxSubmitter | Hosted Runtime Sandbox helper that resolves live session pubkey and returns { signer, sessionPubkey, submitter } for agent plug-and-play use |
| AttestedReceiptVerifier | Fetches and verifies recorded attested receipt PDAs against expected receipt/session fields |
What this does NOT do
- Hold bank credentials — that's the user's enclave's job.
- Run the bank scraper — that's the user's agent code.
- Manage the enclave attestation — see
@loopprotocol/sdkEnclaveClient(spec 07a). - Pay receipt rent — the session signer (the enclave) pays SOL fees + ~0.0018 SOL rent per receipt.
- Honor receipts as merchant payouts — that's spec 08a (
merchant_claim_attested_receipt), shipped as a separate flow.
Trust model
The receipt's on-chain pcr0 field is copied at submit time from the registered session, which was bound to the enclave's image at registration. To submit a forged receipt, an attacker would need an enclave whose PCRs match an audited image AND would be admin-approved in EnclaveImageRegistry — same trust assumption as the entire spec 07a system. See spec 08 § "Threat model".
Proof helper semantics
verifyAttestedReceiptProofShape() is a shape/fail-closed helper, not a cryptographic authenticity verifier. For the current cose_sign1_x509 arm it checks that signature and cert_chain are byte-shaped and returns:
{
ok: true,
proof_type: "cose_sign1_x509",
verification_level: "shape_only",
cryptographic_authenticity_verified: false,
}It does not validate the COSE signature, X.509 trust chain, enclave attestation root, audited-image binding, or bank/source authenticity. Reserved ZK proof arms fail closed with UnsupportedProofTypeError. The legacy verifyAttestedReceiptProof() export is a deprecated alias with the same shape-only semantics.
Development
npm install
npm run build
npm test
npm run typecheckTests are vitest, no on-chain dependency. End-to-end devnet rehearsal lives in loop-protocol/scripts/devnet-rehearsal/test-spec08-09-flow-devnet.ts.
Package B joined-proof settlement smoke
For the Package B checkpoint flow (runtime submitter + bridge action receipt + settlement commit + audit pack), run:
LOOP_BYOAA_API_KEY=<sdk-key> \
LOOP_VAULT_PUBKEY=<vault pubkey> \
LOOP_RUNTIME_BROKER_URL=<runtime broker url> \
LOOP_RUNTIME_TOKEN=<runtime scoped token> \
npm run smoke:package-bOptional environment variables:
DEVNET_RPC(defaults tohttps://api.devnet.solana.com)LOOP_BYOAA_BASE_URL(custom gateway host; sends bearer auth there)LOOP_AGENT_REFLOOP_PRINCIPAL_REF
The smoke script creates a live approval challenge via POST /api/v1/kya/approvals/challenges before runtime submit, requires status/result.approval_receipt in the response, derives approval_receipt_ref/authorization_ref/decision_ref from that receipt, then links those refs into action receipt + settlement commit.
It also logs verification_level and an explicit human step: not yet cryptographically verified line.
License
MIT
