Milestone 2 · Callbacks
Nuvex maintainers
What the request stores
A request holds a callback_program and at most 128 bytes of callback_data. The default pubkey means there is no callback, and fulfilment simply writes the output. Because Milestone 2 passes zero accounts, there is no account list to hash and store on the request.
“A fulfiller cannot choose the callee's accounts.”
The CPI, byte for byte
On fulfilment, oracle core writes the CallbackExecuted status and then performs one cross-program invocation. The instruction is named callback, and its data is fully determined by the request and the verified output.
callback instruction data
[0..8) Anchor discriminator of "callback"
[8..72) VRF output, 64 bytes
[72..76) callback_data length, u32 little endian
[76..) the stored callback_data bytes
accounts: none. The request account is not included.The TypeScript SDK can build those bytes with buildCallbackInstruction, which returns the byte array and does not send a transaction. On the receiving side the Anchor handler takes the output and the data, and its accounts struct is empty:
pub fn callback(_ctx: Context<Callback>, output: [u8; 64], data: Vec<u8>) -> Result<()> {
msg!("nuvex-callback {}", output[0]);
Ok(())
}
#[derive(Accounts)]
pub struct Callback {}That is the test consumer in tests/callback-consumer, which exists so the layout is asserted against the shared writer rather than described in prose. It is not a protocol program.
Why the account list is empty
The rejected alternative explains the decision better than the decision does. If the fulfiller could forward its own remaining accounts, it would be choosing which accounts the callee program sees, which turns a callback into an instruction the fulfiller controls.
“Rejected: forwarding remaining_accounts from the fulfiller. The fulfiller would choose the callee's accounts.”
Oracle accounts are not writable by the callee either, so a callback cannot reach back into protocol state. Account-bearing callbacks are out of scope and would need their own record.
Failure is a revert, not a status
If the callback returns an error, the whole transaction reverts, so the request is not left marked fulfilled with nothing delivered. There is no callback-failed status and no retry instruction, and the ADR names the reason: a retry path lets a fulfiller burn transactions on a callee that always fails.
- COMMITTEDproof submitted with the output
- VERIFIEDECVRF checked by the program
- FINALIZED64-byte output written
- EXECUTEDcallback ran, no accounts passed
Building against an empty account list
A handler with no accounts cannot write state, so the callback is a notification, not a place to settle. The output is on-chain regardless of whether a callback is set, which is where a consumer reads it.
- Keep the callback handler cheap and total: a failure reverts the fulfiller's transaction.
- Use the 128 bytes of callback_data to carry the identifier your program needs to recognise the request.
- Read the VrfResult account under seeds ["verification", request] for the stored output, proof and VRF public key. The request account itself keeps the status and the assigned node, not the output.
- Settle in your own instruction, signed by whoever is allowed to settle, rather than inside the callback.

