What it does
Dispatch delivers one recorded attempt and keeps enough local identity to recover a missing reply. Use it when you already have the authorized signed transaction, or when rehearsing delivery against Arena.
It stores the attempt before invoking the provider. Live mode derives the first signature and message/transaction digests from the supplied bytes. It does not choose a wallet, create an approval, or interpret arbitrary instructions as safe. Use Edge for the integrated reviewed-payment path.
Use independently
Select uk.capp.sidecar.solana-dispatch, then choose either a simulation provider or an intentional live provider. Retain a stable operationId and attemptId before sending. Use window.capp.solana.dispatch or the portable client.
For a first recovery test, choose arena-peer and select Arena with failureMode: drop-response. No Circuit, Latch or network is required for this rehearsal. The result must remain explicitly simulated.
Provider dependencies
| Mode | Send dependency | Reconciliation dependency |
|---|---|---|
arena-peer | Arena in deterministic-local mode | Reads the same Arena operation. |
conduit-peer | Matching writable Conduit; allowSubmission enabled | Matching Witness, which observes the retained signature. |
external-adapter | Configured HTTPS adapter; allowSubmission enabled | Adapter action reconcile, not an implicit Witness fallback. |
Live mainnet transport requires both Dispatch's and Conduit's relevant permission flags. That does not enable mainnet Edge or the built-in Latch signing recipe. Keep your application's authorization boundary separate from transport admission.
A lost-reply rehearsal
Copy the client module alongside this module. The helper checks for simulation before doing anything. Invoke it once from a labelled rehearsal action with saved IDs and a 64-character SHA-256 messageDigest for your synthetic scenario.
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);
export async function dispatchDemoOnce({operationId, attemptId, messageDigest}) {
const status = await client.call('dispatch', 'status');
if (!status.ready || status.providerMode !== 'arena-peer') {
throw new Error('This example requires Dispatch backed by Arena');
}
const input = {operationId, attemptId, messageDigest, network: 'demo'};
try {
return await client.call('dispatch', 'submit', input);
} catch (error) {
if (error.detail?.state !== 'unknown') throw error;
const retained = await client.call('dispatch', 'read', {attemptId});
if (retained.operationId !== operationId || retained.messageDigest !== messageDigest) {
throw new Error('Stored attempt differs; preserve it for investigation');
}
return retained; // Unknown is a result to reconcile, not permission to resend.
}
}
export async function reconcileDispatchAttempt(attemptId) {
return client.call('dispatch', 'reconcile', {attemptId});
}With drop-response, expect the first helper to return state: unknown, then call reconcileDispatchAttempt() with the same attempt ID. Arena's recorded result can recover to simulated finality without another execution. Do not put a fresh call to submit() in a generic catch or retry loop.
Live request and result boundaries
Supply operationId, attemptId, network, messageDigest and signedTransactionBase64; include transactionDigest when already known. Dispatch verifies byte/digest agreement and non-empty signature slots, but its wire inspection is not a cryptographic signature verification or a transaction-policy evaluator. Your signer/review boundary must establish those properties first.
The returned attempt contains requestDigest, transactionDigest, signature, state, responseKnown, provider/network and simulation flags. It does not retain the raw signed transaction in its attempt record. Keep the reviewed operation and its signed bytes at their existing protected owner for recovery—not in a public diagnostic log.
An exact existing attempt is returned without another provider invocation. Changing bound arguments under the same attempt ID is refused. Another attempt for an operation with an unresolved or successful attempt is refused too. maxRetries is passed to the RPC for the same transaction; it is not an application-level permission to rebuild or re-sign.
Scope and limitations
Once the provider was invoked, an error cannot generally prove that nothing happened. Dispatch records unknown, retains any signature computed before sending, and requires observation of the existing attempt. responseKnown: true is not synonymous with success: a known rejection or failure is still a failure.
Reconciliation is a single observation and can remain unknown, submitted or confirmed. Terminal results are finalised, failed, rejected and expired; elapsed client time alone does not produce one. A local store can survive reload without being a distributed queue or cross-device exactly-once guarantee.
Do not clear attempt storage to get past OPERATION_PENDING. If persistence fails after provider invocation, preserve every known signature/operation binding and reconcile outside the failed write path before considering further effects.
Application API
The owning interface is window.capp.solana.dispatch. The portable client calls the same methods. This table describes main arguments, not a stricter typed guarantee than the current provider implements. Exact transaction/provider bindings must be preserved; unsupported state or arguments are rejected.
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);
const status = await client.call('dispatch', 'status');status
client.call('dispatch', 'status', input)
Main input: No arguments required.
Read submission-provider readiness, network and configured gates.
submit
client.call('dispatch', 'submit', input)
Main input: operationId, messageDigest, network; signedTransactionBase64 for live submission; optional attemptId, transactionDigest.
Record an attempt before the provider can have an effect, then submit already signed bytes. Dispatch never signs.
read
client.call('dispatch', 'read', input)
Main input: attemptId or other supported recorded-attempt identity.
Read the retained submission attempt and its response-known/unknown state.
reconcile
client.call('dispatch', 'reconcile', input)
Main input: Recorded attempt identity.
Observe the same attempt through the configured observation provider. Do not create a replacement payment from an uncertain result.
Troubleshooting
| Symptom or error | Next step |
|---|---|
DISABLED / MAINNET_DISABLED | Review the intended provider and build permissions; never silently enable writes. |
MESSAGE_MISMATCH / TRANSACTION_MISMATCH | Use the exact approved signed bytes. Do not recompute identifiers around changed bytes. |
ID_CONFLICT | Recover the original attempt and compare all request fields. |
OPERATION_PENDING | Read/reconcile the reported attempt before considering a replacement. |
unknown after an exception | Preserve the attempt and signature; observation, not resending, is the next step. |
| Missing Witness in Conduit mode | Supply the matching observation provider before expecting live reconciliation. |
Verify your integration
Test a dropped reply after acceptance, identical submission replay, a changed byte binding and reload before reconciliation. Count actual provider sends: recovery must not increment them. Also test a failure to save the pre-send record; no provider effect should occur before that record succeeds.
Integration recipes
Composition · Devnet payment · Recovery · Typed client · Schemas
Related data contracts
These schemas describe payload shape, not evidence of approval or successful execution. Match the version in your payload; older versions remain listed for compatibility.
Configuration reference
These are the selected package's configuration fields. String credential values and installation-specific defaults are never published here. A field's presence does not bypass its runtime policy.
| Field | Type | Meaning and constraints |
|---|---|---|
providerMode | select | Select the implementation this build will use. Configuring an endpoint does not verify its connection or behavior.. Choices: arena-peer, conduit-peer, external-adapter |
network | select | Must match the live provider and requests. Arena and demo confirmation always use simulation.. Choices: devnet, testnet, mainnet-beta, demo |
allowSubmission | checkbox | Master gate for live submission. Keep off for Arena-only or read-only builds. |
allowMainnet | checkbox | Second explicit gate before Dispatch may submit to mainnet-beta. |
skipPreflight | checkbox | Ask the selected live RPC provider to skip its own preflight. Sentinel remains a separate policy layer. |
maxRetries | number | Maximum provider-level send retries for one attempt; operation recovery still belongs to Circuit/Witness.. Bounds: 0 to 20 |
maxAttempts | number | Maximum local records. At the limit, new records stop; existing history is preserved. Increase the limit or archive and reconcile the workspace.. Bounds: 8 to 4096 |
externalAdapterUrl | text | Absolute HTTPS endpoint, or loopback HTTP for development. Keep private credentials on the backend; packaged configuration is visible to users. |
Agent interfaces
These names are declared by the package for host/Agent integration, not JavaScript methods to invoke directly. The host must register a tool, grant its scope and enforce its approval policy. A manifest declaration alone does not prove availability in every Agent catalogue. Prefer the application API above when integrating your own app.
| Tool | Declared risk | Approval requirement |
|---|---|---|
solana.dispatch.status | low | mode-policy |