DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path foray-reference/foray-adapter-deep-read-v0_1.html
Loomworks · FORAY Reference

FORAY Adapter Deep-Read — v0.1

Version: 0.1
Date: 2026-05-24
Scope: Comprehensive read of all adapter material on the local filesystem and the live foray.dunin7.com site.
Audience: The Operator and Claude.ai. Read alongside the protocol and challenge deep-reads.
Read this first — the load-bearing finding of this deep-read

The word "adapter" carries two distinct meanings in FORAY material. Both meanings are present in the canonical material, and conflating them produces misleading conclusions about how Loomworks would integrate FORAY.

1. Source-side adapter — translates a calling system's native events into FORAY 4A JSON. The kaspathon-repo quickbooks-adapter.js and salesforce-adapter.js are examples. These consume ERP/CRM data and produce structured FORAY transactions.

2. Persistence-side adapter — takes a constructed-and-hashed FORAY event and writes it to a storage destination (PostgreSQL, blockchain, NoSQL, S3). The dunin7-foray Node package (v3.0.0) is the reference PostgreSQL persistence adapter. The "Pluggable Persistence Architecture" doc defines this layer formally.

The protocol deep-read found foray-sdk.js referenced but absent. The reason is now clear: the SDK shape implied by the kaspathon adapters (createArrangement / createAccrual / ... / anchorToBlockchain) is not the SDK that DUNIN7 actually ships. The shipped SDK is dunin7-foray v3.0.0, which has a fundamentally different shape — emit(chainId, eventType, triggeredBy, payload) against a PostgreSQL chain rather than createArrangement(...) against blockchain anchoring. The kaspathon adapters were written against a hypothetical SDK that never materialised in the shape they assumed; the actual shipped SDK is what dunin7-workforce uses today.

What this document is for

The protocol and challenge deep-reads touched adapter source code lightly — pulled a few line references to identify the SDK call pattern, but did not read adapters substantively. The Operator surfaced the gap by observing that Claude.ai had not actually read the adapter material despite it being central to the FORAY-Loomworks integration question. This document fills that gap.

Scope: all adapter source code, all adapter descriptive material, all SDK source on the filesystem. The Operator's discipline — "look at everything, we cannot afford to miss anything during an investigation" — has been honoured. The output is large because the directive is to preserve detail rather than compress to a summary.

Source material read

Source code (all read end-to-end):

PathLinesRole
foray-kaspathon/quickbooks-adapter.js627Source-side adapter (QuickBooks Online) — demo-grade
foray-kaspathon/salesforce-adapter.js801Source-side adapter (Salesforce) — demo-grade
dunin7-foray/lib/foray-client.mjs374The live FORAY SDK — ForayClient class, PostgreSQL persistence + hash chain + attestation queries
dunin7-foray/lib/{attestation,evidence,canonical-json,index}.mjs~100 totalSibling modules
dunin7-foray-validate/src/**~630TypeScript predecessor SDK — same conceptual design, older shape
mode-b-build/dunin7-foray/lib/foray-client.mjsEarlier v2.x of the same SDK (no attestation, no canonical-json, no mutex)
dunin7-workforce/tests/foray-client.test.mjsWorkforce-side test exercising ForayClient in production-shaped use

Documentation read substantively:


1. The adapter pattern at the protocol level

The canonical framing — per the public integration guide

From integration-guide.html:

"Every FORAY adapter follows the same three-phase architecture, regardless of source system:

Phase 1: Extract — Connect to source system API (REST, SOAP, BAPI, etc.); pull transaction data with all relevant fields; handle pagination for batch operations.

Phase 2: Transform — Map source fields to FORAY 4A components; apply privacy obfuscation (hash parties, round amounts); generate component hashes (SHA-256); compute merkle root.

Phase 3: Anchor — Submit merkle root to Kaspa blockchain; receive transaction ID and block confirmation; store full FORAY JSON with anchor reference."

Source System (SAP, Oracle, QuickBooks, Salesforce, ...)
         |
         v
    FORAY Adapter (extracts + transforms)
         |
         v
    FORAY JSON (4A components + hashes)
         |
         v
    Kaspa Blockchain (merkle root anchored)
         |
         v
    Archive (full JSON + anchor reference)

This is the source-side adapter meaning. The integration guide treats this as the universal pattern.

Why adapters exist — the philosophical framing

From docs.html:

"Zero Additional Data Entry — If your ERP system already processes your business transactions, you already have everything FORAY needs."

"FORAY adapters simply translate what exists" — verbatim section header.

Your ERP already hasFORAY organizes it as
Customer/Vendor recordsArrangements (who agreed to what)
Calculated amounts, taxes, allocationsAccruals (how values were derived)
Scheduled payments, deliveriesAnticipations (what is expected)
Completed payments, shipmentsActions (what actually happened)

The load-bearing constraint, verbatim:

"FORAY Cannot Create Additional Data. FORAY Protocol is a read-only audit layer — it can only organize, structure, and anchor data that already exists in your systems."

The adapter pattern exists because FORAY is a reflection layer, not a system of record. The adapter's job is to construct a tamper-evident reflection of the source system's existing events without ever writing back to the source.

The abstract contract every adapter shares

Reading across both kaspathon adapters and the persistence-side Pluggable Persistence interface:

The hypothetical model vs the live deployed model

Per the kaspathon adapter source (the hypothetical model):

Source System  →  Adapter  →  SDK  →  Anchor Service
                              (constructs)  (persists)

Per the actual deployed model (dunin7-foray v3.0.0):

Source System  →  Adapter (does not exist for live customers)
                  Application code calling SDK directly
                               ↓
                          ForayClient
                               ↓
                  PostgreSQL `foray_events` table
                  (hash-chained, append-only)
In the live model, there is no separate "anchor service"

The SDK is the persistence boundary. The PostgreSQL database is the persistence layer. Blockchain anchoring is anticipated by the Pluggable Persistence doc but not implemented in dunin7-foray v3.0.0.

Where adapters run — and where they don't

The integration guide describes adapters as running adjacent to the source system, not in-process; the diagram shows the adapter as a distinct hop between source and FORAY. The actual dunin7-foray SDK in dunin7-workforce runs in-process with the application: Workforce's API routes import ForayClient and call .emit() directly during request handling. The application is the adapter.

These are different patterns. The kaspathon adapters model a deployment where FORAY integration is a separable process; the dunin7-foray SDK models a deployment where FORAY is a library called inline by the application.


2. Per-adapter detail

2.1 — quickbooks-adapter.js (kaspathon, demo-grade)

Path: foray-kaspathon/quickbooks-adapter.js · 627 lines · v2.0.0 (Corrected) · BSL-1.1

"DISCLAIMER: This code is provided for demonstration purposes only. Production implementations require additional security hardening, error handling, testing, and validation. Consult qualified professionals before deploying in production environments."

Source system: QuickBooks Online (QBO). Six transaction types are handled — Invoice, Bill, Payment, CreditMemo, JournalEntry. Two more (PurchaseOrder, SalesReceipt) are listed in the file header but have no anchor methods.

FORAY component construction logic — Invoice example

// Step 1: Arrangement (invoice terms, parties)
const arrangement = this.sdk.createArrangement({
  source_system: 'quickbooks',
  transaction_type: 'invoice',
  reference_id: this._hashIdentifier(invoice.Id, 'invoice'),
  parties: [
    { role: 'seller', identifier: this._hashIdentifier(invoice.CompanyInfo?.CompanyName, 'company'), ... },
    { role: 'buyer', identifier: this._hashIdentifier(invoice.CustomerRef?.name, 'customer'), ... }
  ],
  terms: {
    payment_terms: invoice.SalesTermRef?.name || 'Net 30',
    due_date: invoice.DueDate,
    currency: invoice.CurrencyRef?.value || 'USD'
  }
});

// Step 2: Accrual (revenue recognition)
const accrual = this.sdk.createAccrual({
  arrangement_ref: arrangement.id,
  amount: this._obfuscateAmount(invoice.TotalAmt),
  currency: invoice.CurrencyRef?.value || 'USD',
  formula_id: 'invoice_total',
  formula_description: 'Sum of line items + tax',
  formula_inputs: {
    line_item_count: invoice.Line?.length || 0,
    tax_amount: this._obfuscateAmount(invoice.TxnTaxDetail?.TotalTax || 0),
    subtotal: this._obfuscateAmount(invoice.TotalAmt - (invoice.TxnTaxDetail?.TotalTax || 0))
  },
  debit_account: 'Accounts Receivable',
  credit_account: 'Revenue',
  fiscal_period: this._getFiscalPeriod(invoice.TxnDate)
});

// Step 3: Anticipation (expected payment)
const anticipation = this.sdk.createAnticipation({
  arrangement_ref: arrangement.id,
  expected_date: invoice.DueDate,
  expected_amount: this._obfuscateAmount(invoice.TotalAmt),
  currency: invoice.CurrencyRef?.value || 'USD',
  settlement_account: 'Cash',
  conditions: { payment_method: 'Any accepted method', partial_payment_allowed: true }
});

// Step 4: Action (only if paid)
let action = null;
if (invoice.Balance === 0 && invoice.TotalAmt > 0) {
  action = this.sdk.createAction({
    arrangement_ref: arrangement.id,
    anticipation_ref: anticipation.id,
    settlement_type: 'full_payment',
    actual_amount: this._obfuscateAmount(invoice.TotalAmt),
    settlement_date: invoice.TxnDate,
    payment_method: 'Applied'
  });
}

// Anchor to blockchain
const transaction = this.sdk.createTransaction({
  components: { arrangement, accrual, anticipation, action }
});
const anchorResult = await this.sdk.anchorToBlockchain(transaction);

Mapping decisions visible in the code

State, integration, error handling

State per instance: a 32-byte random entitySalt for cross-transaction unlinkability of hashed identifiers, a lastRequestTime for rate limiting, and a config object. No event journal, no replay queue, no persisted state. All state is in-process.

Integration is library-import with pull-based triggering: the application instantiates the adapter and calls adapter.anchorInvoice(invoiceObject) or adapter.batchProcess([...invoices], 'Invoice'). No webhook subscriber, no polling loop — the application decides when to anchor.

Error handling: ValidationError class for inputs; _withRetry wraps every anchor method with 3 retries by default; _enforceRateLimit queues calls behind a minimum inter-request interval; batchProcess swallows per-item failures into an errors array. What happens when FORAY is unreachable: retry 3 times, then throw to the caller. No queueing.

Observation

The file header lists "PurchaseOrder, SalesReceipt" as supported, but no methods for them exist. The header overstates implementation coverage. This is one signal among several that these adapters are illustration, not deployable.

2.2 — salesforce-adapter.js (kaspathon, demo-grade)

Path: foray-kaspathon/salesforce-adapter.js · 801 lines · v2.0 Corrected · same disclaimer

Six Salesforce object types: Opportunity, Quote, Order, Case, Account, Lead. The objectTypeMap lists Contract and Contact too — no handlers exist for them.

Component construction — Opportunity example

const arrangement = this.foray.createArrangement({
  parties: [accountId, ownerId].filter(Boolean),
  asset_type: this.objectTypeMap['Opportunity'],   // 'sales-opportunity'
  terms: { stage, type, lead_source, created_date },
  effective_date: opportunity.CreatedDate,
  metadata: { salesforce_id: opportunityId, object_type: 'Opportunity' }
});

const accrual = this.foray.createAccrual({
  arrangement_hash: arrangement.hash,    // NOTE: uses .hash not .id
  formula_id: this.foray.getFormulaId('weighted_revenue'),
  inputs: { amount, probability, currency },
  outputs: {
    expected_revenue: this.obfuscateAmount(amount * (probability / 100)),
    risk_adjusted_value: this.obfuscateAmount(amount * (probability / 100) * 0.85)
  },
  calculation_date: new Date().toISOString(),
  metadata: { stage, forecast_category }
});

const anticipation = this.foray.createAnticipation({
  accrual_hash: accrual.hash,
  expected_date: opportunity.CloseDate,
  expected_amount: this.obfuscateAmount(amount),
  conditions: [{ type: 'stage_progression', value: 'Closed Won', probability: probability / 100 }]
});

let action = null;
if (opportunity.StageName === 'Closed Won' && opportunity.IsClosed) {
  action = this.foray.createAction({
    anticipation_hash: anticipation.hash,
    actual_date: opportunity.CloseDate,
    actual_amount: this.obfuscateAmount(amount),
    settlement_type: 'opportunity_won',
    proof: { stage, is_won, closed_date }
  });
}

Mapping decisions and a striking inconsistency

A direct inconsistency between the two adapters

The Salesforce adapter uses hash-based component linkage (arrangement_hash, accrual_hash, anticipation_hash) rather than the QuickBooks adapter's id-based linkage (arrangement_ref, anticipation_ref). Both adapters reference the same hypothetical foray-sdk but assume different SDK return shapes — QB SDK methods return objects with .id; Salesforce SDK methods return objects with .hash. One of them is wrong about the SDK shape.

Other notable mappings:

A notable design difference from QuickBooks

The QB adapter anchors inline — one method call equals construct + anchor. The Salesforce adapter separates construction from anchoringopportunityToForay() returns the transaction object; only batchProcess() calls await this.foray.anchorTransaction(transaction) afterwards. Single-object processing through opportunityToForay() directly does NOT anchor. That's left to the caller.

2.3 — ForayClient (the live SDK, the persistence adapter to PostgreSQL)

Path: dunin7-foray/lib/foray-client.mjs · 374 lines · package dunin7-foray v3.0.0

"DUNIN7 FORAY Client — Append-only, hash-chained event emitter with FORAY Protocol v4.1 attestation support. Writes tamper-evident events to PostgreSQL. Each event's hash includes prev_hash, creating an unbroken chain. Separate database per product (foray_fieldpilot, foray_workforce, foray_forge). Uses per-chain mutex to serialize concurrent writes (parallel agents)."

Data shape persisted — the foray_events table

CREATE TABLE IF NOT EXISTS foray_events (
  id            SERIAL PRIMARY KEY,
  frame_id      TEXT        NOT NULL,
  sequence      INTEGER     NOT NULL,
  event_type    TEXT        NOT NULL,
  triggered_by  TEXT        NOT NULL,
  payload       JSONB       NOT NULL DEFAULT '{}',
  prev_hash     TEXT        NOT NULL,
  hash          TEXT        NOT NULL,
  hash_timestamp TEXT       NULL,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (frame_id, sequence)
);

CREATE INDEX IF NOT EXISTS idx_foray_frame   ON foray_events (frame_id);
CREATE INDEX IF NOT EXISTS idx_foray_type    ON foray_events (event_type);
CREATE INDEX IF NOT EXISTS idx_foray_agent   ON foray_events (triggered_by);
CREATE INDEX IF NOT EXISTS idx_foray_payload ON foray_events USING GIN (payload);
The shape is not the FORAY 4A protocol shape

It is frame_id + sequence + event_type + triggered_by + payload. The 4A model is layered on top via the payload JSONB, not as separate columns. An Arrangement-event would be event_type: 'arrangement.created' with the arrangement's full body in payload. The library has no Arrangement/Accrual/Anticipation/Action types as first-class concepts — those live in the application's choice of event-type strings.

Hash construction (the integrity mechanism)

function computeHash(event) {
  const hashInput = {
    frame_id:     event.frame_id,
    sequence:     event.sequence,
    event_type:   event.event_type,
    triggered_by: event.triggered_by,
    payload:      event.payload,
    prev_hash:    event.prev_hash,
    timestamp:    event.timestamp,
  };
  const content = canonicalStringify(hashInput);
  return crypto.createHash('sha256').update(content).digest('hex');
}

The first event in a chain uses prev_hash = '0'.repeat(64). The ChainMutex class serialises writes per chainId. Concurrent callers queue behind a Promise chain. This is the integrity-preserving mechanism for parallel agents writing to the same chain.

The actual deployed SDK API — 14 methods

MethodPurpose
constructor({product, connectionString?, host?, port?, user?, password?})Construct client. product is required and determines the database name (foray_${product}).
connect()Ensure the foray_events table exists. Call once at startup.
disconnect()Close the connection pool.
emit(chainId, eventType, triggeredBy, payload)Append one event to the chain. Acquires mutex, computes hash, commits.
attest(chainId, attestation)Convenience wrapper around emit() with event_type='attestation'.
getChain(chainId)Return all events for a chain, ordered by sequence.
getByType(chainId, eventType)Filter by event type.
getLatest(chainId)Most recent event in a chain.
count(chainId)Event count for a chain.
getAttestations(chainId, {type, attestor, outcome, limit, offset})Filtered attestation lookup.
getAttestationsFor(chainId, subjectType, subjectId)Attestations whose subject_refs[] JSONB array contains {type, id}.
getAttestationsByType(chainId, type)Convenience for getAttestations({type}).
getAttestationsByAttestor(chainId, attestor)Convenience filter.
verifyChain(chainId)Walk chain, recompute each hash, check sequence continuity and prev_hash linkage. Returns {valid, events} or {valid: false, broken_at, reason}.
exportChain(chainId)Full chain as pretty-printed JSON.

FORAY v4.1 Attestation support is first-class. The attestation event lives in the same foray_events table with event_type = 'attestation'. Four read methods are specific to attestations.

Integration, error handling, performance

Library import: import { ForayClient } from 'dunin7-foray'; The application creates one ForayClient per "system" (per dunin7-workforce convention: one for FieldPilot, one for PartsPilot, one for Forge, etc.). Each writes to a separate PostgreSQL database. The application calls .emit() inline during request handling.

Error handling is minimal: _writeEvent wraps the insert in BEGIN/COMMIT/ROLLBACK. If the insert fails, the transaction rolls back and the error propagates. No retry, no queue, no replay. The application must handle the error.

Performance: per-chain serialised writes (mutex). Different chains write concurrently. Connection pool capped at 5 per ForayClient.

2.4 — dunin7-foray-validate (TypeScript predecessor)

Same conceptual SDK in TypeScript form (private package, v0.1.0). Same append-only, hash-chained, per-frame event log design but split into two classes: FORAYStore (owns the pg Pool) + FORAYEmitter (wraps the store).

Differences from v3.0.0 ForayClient:

The typed event-type list is interesting — it enumerates a substrate-specific (FieldPilot/PartsPilot) frame lifecycle: frame.open · agent.invoked · architect.spec_complete · narrator.brief_vN · build.component_complete · strategist.checkpoint · gate.pass · gate.fail · frame.delivered, plus governance and process-discovery extensions.

The typed events were the design starting point; the v3.0.0 SDK then generalised by accepting any event-type string. The application defines the vocabulary; the SDK enforces append-only + hash chain regardless of what the events mean.

2.5 — Pluggable Persistence concept (no code; design doc only)

Path: dunin7-docs/foray/FORAY_PLUGGABLE_PERSISTENCE_V1_0_0.md · concept document, 2026-02-19. No code implementation found.

This document defines a persistence-side adapter interface explicitly:

interface ForayPersistenceAdapter {
  name: string;
  persist(entry: ForayHashEntry): Promise<PersistResult>;
  verifyChain(startId?: string, endId?: string): Promise<VerifyResult>;
  retrieve(filter: ForayFilter): Promise<ForayHashEntry[]>;
  isHealthy(): Promise<boolean>;
}

type ForayHashEntry = {
  id: string;
  forayJson: Record<string, unknown>;  // The complete 4A + Attestation
  hash: string;                         // SHA-256 of forayJson
  previousHash: string | null;          // Hash chain link
  sequence: number;                     // Monotonic sequence number
  createdAt: string;                    // ISO timestamp
};

Four destinations documented:

DestinationTrust LevelFamiliarityUse Case
PostgreSQL (append-only + hash chain)HighVery HighDefault for enterprise/ERP
NoSQL (MongoDB, DynamoDB)HighHighCloud-native deployments
FORAY on BlockchainHighestLowRegulated industries, max trust
S3/Blob Storage (signed JSON files)MediumHighArchival, compliance exports

Phasing: Phase 1 PostgreSQL only; Phase 2 multi-destination fan-out; Phase 3 blockchain anchor.

Current state — a real disconnect

The ForayClient in dunin7-foray v3.0.0 is the Phase 1 PostgreSQL adapter, but it does NOT implement the ForayPersistenceAdapter interface from the design doc (persist / verifyChain / retrieve / isHealthy). The actual API is emit / attest / various getters — closer to a producer SDK than the doc's pure-persistence interface. The Phase 2 and Phase 3 adapters do not exist.

2.6 — OpenClaw Trading Audit Skill (referenced but not located)

docs.html mentions a reference AI-agent adapter for an open-source AI agent platform that executes autonomous tasks including crypto trading on Hyperliquid and Polymarket. Source not found in /Users/dunin7/ — if it exists, it lives in the OpenClaw repository (external to DUNIN7). The text describes trade capture, risk parameters, agent rationale, human authorisation, exchange attestation, and natural-language commands. Treated as a documented-but-unread adapter for completeness.

2.7 — dunin7-workforce as a live application using the SDK

Not an adapter in the strict sense, but the canonical live consumer of ForayClient — worth surfacing because it shows the SDK in production use.

"FORAY is the blockchain-style audit trail system for DUNIN7. It records every significant transaction and event in a tamper-evident append-only ledger. Each system (FieldPilot, PartsPilot, etc.) has its own dedicated FORAY database."

Mapping from dunin7-workforce concepts to ForayClient invocations:

Operator submits frame with project_id
  → API fetches projects.foray_system_key
  → If set: foray.for(foray_system_key).emit(frame_id, 'frame.open', triggered_by, payload)
  → If NULL: no FORAY event written

The lesson recorded in the architecture doc: "Never derive foray_system_key from a name. Always set it explicitly on the project." A past incident silently derived the key from project name and corrupted audit data.

This is the deployed adapter pattern

For an AI-agent system using FORAY today, the dunin7-workforce pattern is the deployed reference. It is the closest production parallel to what Loomworks might become.


3. The SDK contract — hypothetical vs actual

Two SDK contracts exist in the canonical material — the hypothetical one referenced by the kaspathon adapters, and the actual deployed one. They have fundamentally different shapes. The most important correction this deep-read makes is naming this gap clearly.

3.1 — The hypothetical foray-sdk.js (referenced but never implemented)

Inferred entirely from kaspathon adapter call sites. The file foray-sdk.js does not exist in /Users/dunin7/foray-kaspathon/ or anywhere else under /Users/dunin7/.

QuickBooks call sites

LineCallInferred signature
24require('./foray-sdk')Module export: default class
96new ForaySDK(config.foray)Constructor takes a config object
225sdk.createArrangement({...})Sync; returns {id, ...}
252sdk.createAccrual({arrangement_ref, ...})Sync; returns {id, ...}
271sdk.createAnticipation({arrangement_ref, ...})Sync; returns {id, ...}
288sdk.createAction({arrangement_ref, anticipation_ref, ...})Sync; returns {id, ...}
300sdk.createTransaction({components: {...}})Sync; returns transaction object
309await sdk.anchorToBlockchain(transaction)Async; returns {kaspaHash, ...}

Salesforce call sites

LineCallInferred signature
94new ForaySDK(config.forayConfig)Different config key from QB
238foray.createArrangement({...})Sync; returns {hash, ...}note .hash not .id
258foray.createAccrual({arrangement_hash, ...})Hash-based linkage instead of id-based
260foray.getFormulaId('weighted_revenue')Not referenced by QB at all
278foray.createAnticipation({accrual_hash, ...})
298foray.createAction({anticipation_hash, ...})
722await foray.anchorTransaction(transaction)Method name anchorTransaction, not anchorToBlockchain
Inconsistencies between the two adapters' SDK assumptions
  1. .id vs .hash return: QuickBooks expects .id; Salesforce expects .hash.
  2. Reference field: QB uses arrangement_ref/anticipation_ref; Salesforce uses arrangement_hash/anticipation_hash.
  3. createTransaction vs anchorTransaction: QB builds an intermediate createTransaction({components}) then anchorToBlockchain. Salesforce skips the wrapping step.
  4. Config key: QB takes config.foray; Salesforce takes config.forayConfig.
  5. getFormulaId: only Salesforce references it. QB hardcodes formula_id strings.

The two adapters were written against different mental models of the same hypothetical SDK. Either both API shapes exist in some unreviewed source, or one (or both) adapters are wrong about the SDK shape. The actual foray-sdk.js is absent from every reviewed repository. The kaspathon adapters were written against a target SDK that either never shipped publicly or shipped in a different shape than the demos assumed.

3.2 — The actual deployed SDK: dunin7-foray v3.0.0

Fully catalogued in §2.3 above. The contract is fundamentally different from the hypothetical:

3.3 — Documentation cross-reference

The descriptive material at foray.dunin7.com does not document either SDK contract. The 4A schema is described, but the SDK methods that construct it are not. The integration phases (Extract / Transform / Anchor) are described, but the calling shape of an anchorToBlockchain (or equivalent) is not. There is no published SDK reference.


4. The translation boundary

For source-side adapters (kaspathon style)

The translation boundary sits where the source-system-native object is passed into the adapter's anchor method.

What goes in (Salesforce Opportunity, raw):

{
  "Id": "0061T00000abc123",
  "AccountId": "0011T00000xyz789",
  "OwnerId": "00558000abc",
  "StageName": "Proposal",
  "Amount": 50000,
  "Probability": 60,
  "CurrencyIsoCode": "USD",
  "CloseDate": "2026-03-15",
  "CreatedDate": "2026-01-10T...",
  "ForecastCategoryName": "Pipeline",
  "IsClosed": false,
  "IsWon": false
}

What comes out (after Salesforce adapter, before SDK):

{
  arrangement: {
    parties: ['<hashed-account-id>', '<hashed-owner-id>'],
    asset_type: 'sales-opportunity',
    terms: { stage, type, lead_source, created_date },
    effective_date: '2026-01-10T...',
    metadata: { salesforce_id: '<hashed-id>', object_type: 'Opportunity' }
  },
  accrual: {
    arrangement_hash: '<arrangement.hash>',
    formula_id: '<from sdk.getFormulaId("weighted_revenue")>',
    inputs: { amount: 50000, probability: 60, currency: '<hashed-USD>' },
    outputs: { expected_revenue: 30000, risk_adjusted_value: 25500 },
    calculation_date: '...',
    metadata: { stage, forecast_category }
  },
  anticipation: {
    accrual_hash: '<accrual.hash>',
    expected_date: '2026-03-15',
    expected_amount: 50000,
    conditions: [{ type: 'stage_progression', value: 'Closed Won', probability: 0.6 }],
    metadata: { next_step, days_to_close: 45 }
  },
  action: null   // only created when IsClosed && StageName === 'Closed Won'
}

Translation steps in detail

  1. Identifier hashing — every Salesforce Id, AccountId, OwnerId is run through hashIdentifier(id, type) which SHA-256-hashes ${entitySalt}:${type}:${id}.
  2. Amount obfuscation per privacy level. QuickBooks rounds to nearest $100 at standard. Salesforce rounds to nearest $1,000.
  3. Field selection — only fields the adapter knows how to map are carried. Description is dropped (privacy + structural mismatch). Line-item detail is summarised to counts.
  4. Derived computation — Salesforce computes weighted_revenue = amount * probability/100 and risk_adjusted_value = weighted * 0.85. These derived fields don't exist in the source; the adapter constructs them.
  5. Component skipping — Unpaid Invoice → no Action. Unconverted Lead → no Action. Account → no Anticipation, no Action.

What's lost, what's added

Lost in translationAdded in translation
Plain-text party names (replaced with hashes)formula_id and formula_description (FORAY-side conceptual labels not in source)
Exact monetary amounts (replaced with rounded or hashed)fiscal_period (computed from transaction date, not stored in QB)
Free-text fields like Description, PrivateNote bodydays_to_close (computed; not in Salesforce)
Line-item detail (summarised to counts and totals)Per-adapter entitySalt — adds cross-transaction unlinkability the source has no concept of
Internal Salesforce/QuickBooks metadata (SystemModstamp, LastViewedDate)For Cases: estimated_effort_hours and estimated_cost are internal heuristics explicitly noted as not from Salesforce

For persistence-side adapter (ForayClient style)

The translation boundary is different — the application has already done 4A construction, and the SDK's job is to persist + hash.

There is no 4A-specific translation in the SDK. The application chooses event-type strings and payload shapes. The SDK enforces: sequence monotonicity per frame_id, prev_hash linkage, canonical JSON for deterministic hashing. The Pluggable Persistence design doc envisioned a stricter contract (a ForayHashEntry with explicit forayJson + hash + previousHash), but the live SDK is looser. The application can emit('arbitrary.event.type', 'system', {anything: 'goes'}) and the SDK will chain it.


5. Persistence and replay

kaspathon adapters

There is no queue, no journal, no event log on the adapter side. Replay would require the application to re-extract from the source system and re-call the adapter.

ForayClient (dunin7-foray v3.0.0)

The architectural posture

Events are persisted synchronously or they fail synchronously. There is no eventual-consistency fallback. This is consistent with the FORAY "tamper-evident" claim — there is no buffer where events could be tampered with before persistence.

Pluggable Persistence design (not implemented)

The design doc envisioned fan-out persistence with admin-configurable retry. In the imagined Phase 2 architecture, an event would be fanned out to all enabled destinations in parallel. The retry policy would apply per destination. Best-effort mode would let a slow destination not block others. None of this is implemented today. The live SDK is single-destination PostgreSQL with no retry policy.


6. Configuration and deployment

kaspathon adapter configuration

new QuickBooksForayAdapter({
  quickbooks: qboClient,        // Required: a QBO client instance
  foray: {/* SDK config */},    // Required: SDK constructor arg
  privacyLevel: 'standard',     // 'minimal' | 'standard' | 'high' | 'defense'
  entitySalt: '<hex>',          // Optional: 32-byte hex; random if absent
  retryAttempts: 3,
  retryDelayMs: 1000,
  enableLogging: true,
  minRequestInterval: 100
});

Credentials needed: QuickBooks OAuth tokens (via qboClient) + FORAY connection details (opaque to the adapter). The entitySalt is generated at construction if not supplied; persisting it is the application's responsibility (if you want consistent identifier hashing across runs).

Deployment: library, imported by the application. No standalone deployment. No start/stop lifecycle.

ForayClient configuration

new ForayClient({
  product: 'fieldpilot',          // Required: determines database name
  connectionString: 'pg://...',   // OR host/port/user/password individually
  host: 'localhost',              // Default: PG_HOST env or 'localhost'
  port: 5432,                     // Default: PG_PORT env or 5432
  user: 'postgres',               // Default: PG_USER env or 'postgres'
  password: '...'                 // Default: PG_PASSWORD env
});

Environment conventions: PG_HOST, PG_PORT, PG_USER, PG_PASSWORD. Workforce's TECHNICAL_REFERENCE notes that PG_USER must be explicitly set to dunin7 — the SDK defaults to postgres if absent.

Deployment: library, imported by the application. The PostgreSQL database itself must be provisioned separately (one database per product, with the foray_events table created on first connect()).

Per-system provisioning (from workforce architecture doc):

  1. CREATE DATABASE foray_{key};
  2. Run FORAY schema migrations against the new database (creates foray_events).
  3. Register a ForayClient in the application's services layer.
  4. Set foray_system_key on the relevant projects.

7. Documentation conventions

Organisation at foray.dunin7.com

integration-guide.html is the primary integration document, structured by per-ERP sections (SAP, Oracle, QuickBooks, Salesforce, NetSuite) with a closing Adapter Architecture / Privacy Levels / Batch-vs-Real-Time section.

Each per-ERP section follows a similar shape:

  1. Native Audit Capabilities — what the source system already offers
  2. The Audit Gap — table of source-system capability vs limitation (typically: admins can alter logs)
  3. FORAY + {System} Integration — table of source-module / FORAY-mapping / integration-point

The shape is descriptive, not prescriptive. It does not specify endpoint URLs, message formats, or wire protocols.

Templates and scaffolds

No templates or scaffolds exist in the repository. The two source adapters (QuickBooks, Salesforce) are the only working code examples, and they diverge enough from each other that they cannot serve as a single template.

Compliance expectations

From RFC-0001:

"Connector implementations. Build a FORAY-compliant connector for any source system. The connector specification defines the submission schema. Any system that can produce a valid FORAY submission is a compliant source. There is no certification required to build. Certification is available for those who want it."

The compliance bar is "produce a valid FORAY submission per the schema." There is no formal SDK-conformance test, no certification suite. Adapter compliance is structural — does your adapter produce valid 4A JSON per the schema reference.


8. Other substantive findings

F1 — Two adapter meanings is the load-bearing observation

The single most important finding from this deep-read. "Adapter" in FORAY material refers to: (1) Source-side translation — kaspathon-style; (2) Persistence-side destination — Pluggable Persistence. The two adapters' lifecycles, contracts, and concerns are different. A Loomworks integration design that conflates them would design the wrong thing.

F2 — The "AI Agent Integration" section in docs.html is the canonical Loomworks-shaped use case

Reproduced verbatim from docs.html lines 1118-1148:

FORAY ComponentWhat It Captures for AI AgentsExample
ArrangementThe agent's mandate — what it was authorized to do, by whom, under what constraintsAgent authorized to approve purchase orders under $50K, referencing model version hash and system prompt hash
AccrualThe agent's reasoning — what information was analyzed, what it determinedAgent evaluates three vendor quotes, determines Vendor B is $2,400 below market rate
AnticipationThe agent's intent — what action it planned, with what expected outcomeAgent plans to approve $47,200 PO to Vendor B; expected outcome: delivery within SLA
ActionThe agent's execution — what it actually did, the result, variance from intentPO approved and submitted at 14:32:07 UTC; confirmation reference PO-2026-04471

This is the mapping a Loomworks-as-FORAY-anchored-system would use. Every Companion-mediated assertion commit decomposes:

The model version hash + system prompt hash bit is particularly relevant — Loomworks already has a system_prompt field on its specialists. A FORAY arrangement for a Loomworks engagement would hash these.

F3 — Privacy levels are adapter concerns, not SDK concerns

In both kaspathon adapters, privacy obfuscation happens inside the adapter. The SDK never sees plaintext values. The dunin7-foray SDK has no built-in privacy obfuscation — values are persisted as-is in JSONB. A Loomworks adapter would need to decide its privacy posture before calling the SDK.

F4 — entitySalt is misused as a primitive party identifier in some paths

In Salesforce's Quote/Order handlers: parties: [accountId, this.entitySalt].filter(Boolean). The entitySalt is meant to be a cryptographic salt; using it as a party id directly leaks cryptographic material into the FORAY transaction. This appears to be a demo-code shortcut. A Loomworks production adapter should not replicate this pattern.

F5 — No HTTP "anchor service"

Across all source and documentation read, no service called the "FORAY anchor service" exists as a deployable HTTP API. The Kaspa blockchain is the anchor service in the kaspathon adapters' model; PostgreSQL is the anchor service in the dunin7-foray model. Concrete choices for Loomworks today:

The pragmatic choice today is the first option.

F6 — The kaspathon adapters appear to be marketing artifacts, not deployable code

Considered together: (a) the disclaimer block calls them demonstration-only; (b) they reference an absent SDK; (c) the two adapters disagree on SDK shape; (d) batch processing is sequential; (e) no error replay; (f) entitySalt is mis-used. These adapters were written to describe what a FORAY adapter could look like, not to be deployable. They populate the integration-guide.html sections with link-out references but cannot themselves be installed and run.

The live deployed FORAY usage is dunin7-foray v3.0.0 + dunin7-workforce direct calls. There is no production source-side adapter in the kaspathon style.

F7 — The dunin7-foray SDK silently allows arbitrary event_type strings

The SDK accepts any TEXT for eventType. The application is the source of vocabulary — there is no SDK-side validation. Loomworks could choose its own vocabulary (assertion.committed, companion.responded); cross-system event-type collision is possible but harmless because each system has its own database.

F8 — Per-chain mutex serialisation is a real constraint for parallel agents

The ChainMutex serialises ALL writes to a given chain. If multiple agents in Loomworks write to the same engagement's chain in parallel, they queue. Per-chain throughput is single-threaded. This is necessary for hash chain integrity but is a throughput consideration.

F9 — There is no specified delivery semantic

Neither the kaspathon adapters nor the live SDK specifies a delivery guarantee (at-most-once / at-least-once / exactly-once). The live SDK is exactly-once if the transaction commits; if the application crashes after the DB commit but before processing the result, the event is durably persisted but the application doesn't know it. There is no idempotency token or replay-detection mechanism. For Loomworks: idempotency would have to be the application's concern — e.g., a memory_event has a content_hash that could double as an idempotency check.

F10 — TypeScript predecessor was more disciplined about event vocabulary

The dunin7-foray-validate package enforced a discriminated union of event types at compile time. v3.0.0 dropped this constraint. The trade-off: typed union means any change to the event vocabulary requires SDK changes; free TEXT means applications define their own vocabulary. The v3.0.0 choice was deliberate (the SDK is now generic across products), but a more disciplined application would re-establish a typed vocabulary at its own layer.

F11 — No observability emission from the SDK

Neither SDK emits Prometheus / OTel / structured logs at observability frequencies. The application would have to wrap SDK calls to capture latency / failure rate.


9. What changed in our understanding (corrections to prior deep-reads)

This deep-read corrects three things the protocol and challenge deep-reads got partially wrong by reasoning from the kaspathon adapter source alone.

Correction 1 — The "FORAY anchor service" has two architectures, and only one is deployed

The protocol deep-read concluded:

"There is enough material in the local repository to know the conceptual shape: the submitting system constructs a FORAY envelope with a declared persistence layer, submits it to a layer-specific service (probably via the absent foray-sdk.js), receives a blockchain_anchor block..."

That conclusion was inferred from the kaspathon adapter call sites. It is partially wrong. The actual deployed SDK (dunin7-foray v3.0.0) does NOT match that shape:

The protocol deep-read's conclusion accurately describes the kaspathon hypothetical model, but the live deployed model is the ForayClient-PostgreSQL chain. The two are different architectures.

Correction 2 — The challenge site's silence on persistence is incomplete

The challenge deep-read said the challenge site does not document a wire interface. That's correct, but incomplete. There is a live persistence layer — PostgreSQL via dunin7-foray — that is not what the challenge site documents but is what actually runs in dunin7-workforce. The challenge site's framing emphasises Kaspa; the deployed reality is PostgreSQL.

Correction 3 — There is no single canonical adapter pattern

Both prior deep-reads talked about "adapters" without distinguishing source-side from persistence-side. The kaspathon adapters were referenced as if they showed how Loomworks would integrate. This deep-read finds the kaspathon adapters are demo-grade and reference an absent SDK; the production pattern is dunin7-foray ForayClient called directly by application code. A Loomworks integration would more closely resemble dunin7-workforce's pattern (library-call-from-application) than the kaspathon ERP adapter pattern.

This is the most important correction to surface for the substrate review's next round.


10. Things asked for but not found

TopicWhat's missingWhere it likely is
The hypothetical foray-sdk.js sourceAbsent from kaspathon and the rest of the filesystemNever implemented in the assumed shape; or in DUNIN7-internal storage
OpenClaw Trading Audit Skill sourceNot in /Users/dunin7/External OpenClaw repository
A working Kaspa anchor implementationNot in any reviewed sourceWould need to be built per Pluggable Persistence Phase 3
A standardised ForayPersistenceAdapter implementationInterface in the design doc; ForayClient doesn't conformPhase 2/3 of Pluggable Persistence not yet built
End-to-end flow demonstration (source → SDK → anchor → verify)kaspathon's foray-api-server.js does source → LLM → 4A JSON, then stopsdunin7-workforce shows application → ForayClient → PostgreSQL but the source-ERP adapter layer is missing
SDK reference documentationNeither SDK has published reference docsShape inferred from source code (this deep-read §3)
Performance benchmarksNone publishedMutex behaviour implies single-chain throughput is low single-digits/sec
Adapter certification / compliance test suiteRFC-0001 says certification is "available for those who want it" but no test suite is referencedNot in any reviewed repo
NetSuite, SAP, Oracle adapters as source codeOnly QuickBooks and Salesforce have sourceDescribed in integration-guide.html but no code exists

Closing — implications for the Loomworks framing

Three reads now compose:

  1. Protocol deep-read — protocol-level: schema, attestation rules, framing drift
  2. Challenge deep-read — integration-pattern surrogate: pattern codes, library taxonomy, Loomworks-shaped use case (#472 Emergent Agent Transaction)
  3. This deep-read — adapter-level: source-side adapters demo-grade only, persistence-side via dunin7-foray ForayClient, AI-agent 4A mapping from docs.html
What this means for the substrate review work that follows
  • A Loomworks-as-FORAY-system integration would most directly resemble dunin7-workforce's pattern: ForayClient.emit(engagement_id, event_type, actor, payload) called from inside the Loomworks engine at significant lifecycle events.
  • The 4A model would be encoded via event-type strings and payload shape (e.g., event_type: 'arrangement.engagement_session' with the 4A Arrangement payload inside), not via SDK method names.
  • Privacy obfuscation is the integration's concern, not the SDK's. Loomworks already has a hashing posture in its substrate (the *_hash columns); this would extend to party identifiers in FORAY payloads.
  • Attestations are first-class via foray.attest(). The Companion's classifier / responder / committer would each emit attestation events chained to the same engagement.
  • Persistence is PostgreSQL today. Blockchain anchoring is deferred. The Loomworks substrate review can plan against PostgreSQL persistence as the default.
  • The kaspathon adapter style (createArrangement/Accrual/...) should be treated as descriptive illustration, not a deployment template. Loomworks does not need to write a kaspathon-style adapter; it needs application-layer integration with the live SDK.