Can a Solana Transaction Prove the Router?
Part I — DFlow Shows Where Solana Trading Is Going. Retail Pricing Still Happens at the App. — measures the retail price effect. This article opens one unsigned transaction and follows its program and account lineage with Rust.
One Quote, One Transaction Artifact
A Rust decoder opened one live JTX unsigned Solana v0 transaction, resolved the exact accounts loaded through three address lookup tables, and found that two of five compiled instructions directly target DFlow Aggregator v4. The transaction did not preserve a readable JTX identity, downstream venue path, delivery receipt, or settlement result.
The JTX interface displayed “Route via DFlow” on a plain USDC→SOL quote. The browser saw JTX and wallet-service hosts, not a DFlow hostname. That absence cannot exclude a server-side DFlow call. The unsigned transaction is the first artifact in this workflow that can test router attribution directly.
Public docs place DFlow deeper than a quote label: named integration, quote API, ready-to-sign construction, submission tooling, streaming, and agent-facing interfaces. This piece opens one live capture at the ready-to-sign step—not a census of integrators, and not a submitted fill.
One live quote returned that unsigned transaction. It was retained, sanitized, hashed, and decoded. It was never signed or submitted. The app returns quote metadata and an unsigned transaction; the wallet authorizes it; delivery and Solana execution occur later. Why DFlow is economically material enough to study as wholesale execution infrastructure is Part I—not this piece.
RESULT
| Field | Value |
|---|---|
| transaction type | Solana v0 unsigned transaction |
| address lookup tables | 3 |
| compiled instructions | 5 |
| instructions targeting DFlow Aggregator v4 | 2 |
| addresses available across referenced tables | 759 |
| addresses actually loaded | 22 |
| candidate integrator marker | 1, unconfirmed |
| Jito tip-account matches | 0 |
| settlement | not submitted |
| sample size | n = 1 |
Decoding the Transaction with Rust
The previous article’s quote panel kept the transaction field out of scope on purpose—those runs measured price. The field was there in the response; this piece opens it.
Rust is the practical choice: the Solana SDK provides the canonical representations (VersionedTransaction, MessageAddressTableLookup, AddressLookupTable). Malformed input can fail at bincode; semantic mistakes—such as incorrect ALT ordering—can still produce a plausible but false account map. Typed deserialization makes structural failures visible; it does not prevent a confident wrong map.
Each stage is a library function with a CLI subcommand over it, so a capture can be re-decoded months later without a browser. The pipeline is read-only end to end—no key material, no signing path, nothing that could submit.
let raw = STANDARD
.decode(b64.trim())
.context("invalid base64 transaction")?;
let vtx: VersionedTransaction =
bincode::deserialize(&raw).context("failed to deserialize as VersionedTransaction")?;
let message = &vtx.message;
let static_keys: Vec<String> = message
.static_account_keys()
.iter()
.map(|k| k.to_string())
.collect();
let alt_refs = message.address_table_lookups().unwrap_or_default();
let transaction_type = if alt_refs.is_empty() {
"legacy_or_v0_no_alt"
} else {
"v0_with_alt"
};
This capture is v0_with_alt, with three address-table lookups. Static keys alone are incomplete—indexes into shared tables still have to be resolved before any instruction account can be named.
Why Address Lookup Tables Are the Hard Part
A v0 transaction carries static keys plus indexes into shared on-chain tables. Fetching a table and reading its hundreds of addresses tells you what the table contains—not what the transaction uses. Shared tables are infrastructure; a single swap message borrows a thin slice of them.
The first implementation treated table membership as transaction use and produced a complete, plausible, false account map. Two eye-catching addresses—DFlow’s referral program, and a vanity address spelling marketing copy—sat in a referenced table but were never loaded. Their indexes are never requested. Being one of 251 addresses in a shared table is not evidence about this transaction.
The decoder must apply the transaction’s writable and readonly indexes and concatenate them in Solana’s order: writable across all tables first, then readonly.
for writable_pass in [true, false] {
for alt in &decoded.address_lookup_table_references {
let table = resolved_tables.get(&alt.lookup_table_account)?;
let indexes = if writable_pass {
&alt.writable_indexes
} else {
&alt.readonly_indexes
};
for &table_index in indexes {
let address = table.get(table_index as usize)?;
vector.push(LoadedAddress {
account_vector_index: vector.len(),
address: address.clone(),
writable: writable_pass,
lookup_table_index: Some(table_index),
..Default::default()
});
}
}
}
Get that order wrong and every instruction’s account indexes resolve to the wrong addresses—silently. The output still looks complete. False marker candidates from whole-table attribution disappeared once exact indexes were applied.
Two guards catch the failure. Unit tests build a synthetic transaction whose tables contain far more addresses than the transaction loads, then assert the vector is exactly [static…, writable…, readonly…] and that an index past the end of a table errors rather than skips. Externally, the Associated Token Account instruction resolves to the canonical layout—funder, token account, owner, mint, System Program, SPL Token—in the slots the ATA program requires. Wrong ordering would scatter System Program and SPL Token out of those slots.
writable_indexes and readonly_indexes, concatenated after the static keys. Reading a whole table instead of applying its indexes yields a complete, plausible, false account map.
On this capture: 759 addresses available across the three tables; 22 loaded; final account vector 32 entries (10 static + 22 loaded, split 7 writable / 15 readonly). Every loaded address is referenced by at least one compiled instruction.
Mapping Five Compiled Instructions
With the account vector built, mapping an instruction is mechanical: resolve its program by index, then resolve each account index against the same vector.
for ix in &decoded.instructions {
let program_slot = vector.get(ix.program_id_index as usize)?;
let mut accounts = Vec::new();
for (pos, &acct_index) in ix.account_indexes.iter().enumerate() {
let slot = vector.get(acct_index as usize)?;
accounts.push(InstructionAccountRef {
position_in_instruction: pos,
account_vector_index: acct_index as usize,
address: slot.address.clone(),
writable: slot.writable,
label: slot.label.clone(),
});
}
instructions.push(MappedInstruction {
instruction_index: ix.index,
program_id: program_slot.address.clone(),
program_label: program_slot.label.clone(),
accounts,
});
}
Two of the five compiled instructions target DF1ow4…. The full program address DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH sits in the static account keys and stores no name. The decoder first returned it as unknown; “DFlow Aggregator v4” comes from outside the transaction—Solana Explorer and Solana Compass. The address is evidence in the unsigned message; the human-readable name is external attribution. Nothing was signed or submitted, so there is no landed transaction hash on Explorer.
The five instructions divide plainly: one Associated Token Account CreateIdempotent; two Compute Budget calls (limit 324,189 CU; price 50,000 micro-lamports per CU); two DFlow instructions (36 accounts / 134 bytes, then a 3-account follow-up). No top-level SPL Token transfer. If signed and landed unchanged, Solana would dispatch two instructions to that program. Downstream venue, runtime CPI, and settlement remain unresolved. A priority-fee compute-budget instruction is present; the Jito tip-account check returned zero hits.
What Survived Compilation?
Compilation is selective. Some quote-JSON fields never become readable transaction fields. Others only exist at runtime or settlement and were never present to erase.
Survived directly. DFlow program address; instruction structure; static accounts; exact ALT references and the 22 loaded addresses; compute-budget parameters.
External attribution. The label “DFlow Aggregator v4”; ownership and executability labels from read-only RPC on 2026-07-29.
Did not survive as readable metadata. Request ID; routePlan JSON; venue labels such as “DFlow JIT Router”; app name; displayed fee label. The string “DFlow JIT Router” appears nowhere in the compiled bytes. What survives is a program address that a registry happens to name.
Opaque. The 134-byte DFlow payload may encode amounts, limits, or route parameters. Without a verified layout, this analysis records length and hash only. Static decode therefore cannot place the displayed fee inside the instruction, the routed amount, or a later CPI transfer.
Requires runtime. Downstream CPI venue path; fee transfers and balance effects; Jito bundle delivery; settlement. Those are not compilation losses.
One result moved the other way. The ATA-owner slot of CreateIdempotent references Cb1uxfFv…, owned by executable program JTXJTX…. That vanity prefix belongs to the owner program, not to an address the transaction names as an instruction program. Classify it only as a candidate integrator-associated owner program—not proven app origin, not a confirmed fee account.
DF1ow4…, and registry name stay on separate rows. Runtime and settlement are out of scope here. One capture, n=1.
Part I measures the displayed fee as a retail pricing object at quote stage. Here that fee does not reappear as a readable top-level transfer. That is a provenance boundary, not a redo of the fee decomposition.
Closing
The interface named the router. Rust found the same router program inside the compiled transaction. The transaction did not preserve a readable app identity, and it could not reveal a runtime path, delivery channel, or settlement result that did not yet exist.
Transaction transparency begins after some execution decisions have already been made—and ends before others occur.
Appendix
- Part I: DFlow Shows Where Solana Trading Is Going. Retail Pricing Still Happens at the App.
- Decoder:
egpivo/dflow-transaction-lineage@0e79e1f— capture 2026-07-29T13:20:14Z · USDC→SOL · 1,000 USDC · unsigned v0 · SHA-256 of serialized bytes87a4fa3b06dbdad18825004f2a2446d8e05d5893c819e2f9f6a3f478a60b5c3a· n = 1 · not signed or submitted - Program ID
DF1ow4…: Explorer · Compass - Candidate owner program
JTXJTX…; owned accountCb1uxfFv… - Jito tip accounts: on-chain addresses