@loopprotocol/sdk-byoaa
v0.1.0-alpha.7
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.6). 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/sdk-byoaa
# peer deps required for Solana/on-chain helpers (Node 20+ runtime):
npm install @coral-xyz/anchor @solana/web3.js@loopprotocol/sdk-byoaa has a dependency-light default install: production dependencies are limited to browser-safe hashing for receipt helpers. Solana/on-chain helpers require the peer packages above; REST-only and receipt-only consumers do not need to import those surfaces.
Runtime support
0.1.0-alpha.6 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. Solana/on-chain helpers: network config, PDA derivation, instruction encoding, receipt submission, and on-chain receipt verifier/account decoders.@loopprotocol/sdk-byoaa— browser-safe alpha aggregate root with receipt, REST, and shape-only proof helpers. Node 18+ compatible. Use/solanafor chain/RPC helpers.
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
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
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 |
| 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.
License
MIT
