Infrastructure demo
This app exists to demonstrate what Flap's on-chain AI video-generation infrastructure can actually do, end to end — not just claim to do. Every clip below was produced by a real backend watching a real smart contract on BNB Smart Chain Testnet: connect your wallet, describe what happens next, pay 0.01 tBNB, and watch a new episode get generated and settled fully on-chain in front of you.
Reference frames (auto-filled)
#1 Character
Loading…
#2 Previous last frame
No clip yet — only the character is sent.
Describe what happens next, then pay 0.01 tBNB (BNB Smart Chain Testnet) to extend the video.
Preset prompt (always prefixed)
added on-chain by the consumerReference image 1 is the main character: keep the exact same character design, colors, outfit and art style.
Why: the video model only gets images, not their roles. This prefix tells it which image is the character to keep and which frame the scene continues from. Without it a free prompt can change the character or jump to an unrelated scene, which breaks the continuous story. Your text below is added after it.
Connect your wallet to generate a video.
Extending consumer contract: 0xB7f4398BEECE5Ec0da06497e647107D697c55c66. Everyone extending this same address builds the same on-chain story, one clip at a time.
Default is the demo consumer contract deployed on BNB Smart Chain Testnet. You can paste any other EOA or contract address that has called generateVideo on the provider — the provider tracks a session per address. Add ?consumer=0x... to this page's URL (or use “Copy link” above) to open a specific consumer directly, without needing to paste it in manually.
Everything below reflects what is actually deployed and live on BNB Smart Chain Testnet — the request/fulfill flow, the append-only per-requester session, and the exact Solidity interface this demo app talks to.
A user or a vault pays a flat 0.01 BNB fee and calls generateVideo(prompt, referenceType, referenceCids). The referenceType (a VIDEO_REF_* constant) says how earlier images condition the new clip: 1 uses the requester's previous last frame as the exact opening frame, 2/3 pin caller-supplied first (and last) frames, and 4 passes up to three images as soft character / scene references. This demo uses type 4: the first clip sends [character image] and every later clip sends [character image, previous last frame], so the same character carries through every episode. The legacy one-argument generateVideo(prompt) still works and continues from the last frame automatically. The contract resolves and stores the reference CIDs and emits FlapAIProviderVideoRequested; our off-chain backend (rust-video-service) watches for this event, sends the prompt and reference images to the video model, uploads the resulting clip to IPFS, and calls submitGeneratedVideo back on the contract to append the clip and settle the request, notifying the requester through onVideoRequestSettled.
For every initiator — a vault, another contract, or a plain wallet — the contract maintains its own independent session: a plain append-only array of fulfilled clips, indexed by that address. A new clip is always appended to the end, never inserted or rewritten, and records the referenceType it was generated with. After the first clip, each new clip takes the previous clip's last frame as a reference, and each clip's startPtsMs continues the timeline. Playing every clip in a session back-to-back therefore produces one single, continuous video — and since there is no upper bound on how many times a requester can call generateVideo, a session can in principle grow forever: infinite prompts in → one infinite continuous video out.
FlapAIProvider (AI oracle / video provider)
0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744Default demo consumer (ReferenceStoryVideoConsumer)
0xB7f4398BEECE5Ec0da06497e647107D697c55c66IFlapAIProvider video-generation surface + IFlapVideoReceiver)Excerpted from the deployed contract's source (FlapTaxVaults, src/plugins/AIProvider/IFlapAIProvider.sol) — the full NatSpec is kept below so every field and function is explained inline.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
/// @title IFlapAIProvider (video-generation surface)
/// @notice On-chain video-generation half of the FlapAIProvider oracle. A requester (any EOA
/// or contract, e.g. a Flap vault) pays a flat BNB fee and submits a text prompt via
/// `generateVideo`. The off-chain `rust-video-service` backend watches for the emitted
/// event, generates a clip (optionally seeded from the requester's previous clip's last
/// frame for continuity), uploads it to IPFS, and calls back on-chain to append the
/// clip to the requester's append-only session.
interface IFlapAIProvider {
// ----------------------------------------------------------------
// Enums
// ----------------------------------------------------------------
/// @notice Lifecycle state of an on-chain video-generation request.
/// @dev NONE = default, request was never created.
/// PENDING = request submitted on-chain, awaiting backend fulfillment.
/// FULFILLED = backend uploaded the clip and appended it to the requester's session.
/// FAILED = backend could not produce a clip (terminal — see `VideoFailureReason`
/// for why). No clip is ever added to the session for a FAILED request.
/// FULFILLED and FAILED are terminal states. There is no refund path for FAILED — the
/// backend only transitions a request to FAILED after it has decided (possibly after
/// retries) that the request can never be fulfilled; purely transient upstream errors
/// (e.g. OpenRouter/Pinata temporarily unavailable) are expected to be retried by the
/// off-chain backend while the request stays PENDING, not surfaced as FAILED.
enum VideoRequestStatus {
NONE,
PENDING,
FULFILLED,
FAILED
}
/// @notice Machine-readable reason a video request could not be fulfilled.
/// @dev Only meaningful when `VideoRequest.status == FAILED`; `NONE` otherwise.
/// Lets `IFlapVideoReceiver.onVideoRequestSettled` implementations (e.g. a Flap vault)
/// branch on *why* generation failed rather than only *that* it failed.
/// - CONTENT_POLICY: The prompt or resulting content was refused by the upstream
/// model/provider (e.g. copyright or moderation/censorship policy).
/// Retrying the exact same request would fail again.
/// - PROVIDER_ERROR: The upstream generation provider (e.g. OpenRouter) returned a
/// non-retryable error after the backend's retry budget was
/// exhausted (as opposed to a transient outage, which the backend
/// retries without ever reaching FAILED).
/// - PIPELINE_ERROR: The backend's own local pipeline failed irrecoverably after the
/// video was generated (e.g. ffmpeg transmux rejected an unexpected
/// codec, or IPFS upload failed after retries).
/// - TIMEOUT: Generation did not complete within the backend's maximum wait
/// window across all retries.
/// - OTHER: Any other terminal failure not covered above.
enum VideoFailureReason {
NONE,
CONTENT_POLICY,
PROVIDER_ERROR,
PIPELINE_ERROR,
TIMEOUT,
OTHER
}
// ----------------------------------------------------------------
// Structs
// ----------------------------------------------------------------
/// @notice A single fulfilled video clip appended to a requester's append-only session.
/// @param requestId The request ID this clip fulfills.
/// @param videoCid IPFS CID of the MPEG-TS clip.
/// @param lastFrameCid IPFS CID of the clip's last frame (JPEG); used for continuity/thumbnail.
/// @param durationMs Clip duration in milliseconds (required by mpegts.js segment playback).
/// @param startPtsMs The clip's starting presentation timestamp (PTS), in milliseconds, on
/// the requester's continuous session timeline. Always 0 for the first
/// clip in a session; for every subsequent clip it equals the previous
/// clip's `startPtsMs + durationMs` — i.e. cumulative elapsed duration
/// across the session so far. The backend encodes each clip's MPEG-TS
/// stream with its PTS/DTS pre-offset to this same value (via ffmpeg's
/// `-output_ts_offset`), so concatenating clips' raw bytes back-to-back
/// (or handing them to a player as sequential segments) produces a
/// single stream with strictly monotonic, continuous timestamps and no
/// discontinuity at clip boundaries.
/// @param createdAt `block.timestamp` when the clip was fulfilled.
/// @param referenceType The `VIDEO_REF_*` type of the request that produced this clip.
struct VideoClip {
uint256 requestId;
string videoCid;
string lastFrameCid;
uint32 durationMs;
uint64 startPtsMs;
uint64 createdAt;
uint8 referenceType;
}
/// @notice Read-only summary of a video request, used exclusively by explorer view functions.
/// @param requestId The unique video request ID.
/// @param requester Address that submitted the request.
/// @param status Current lifecycle status of the request.
/// @param referenceType One of the `VIDEO_REF_*` constants (see below).
/// @param timestamp `block.timestamp` when the request was submitted.
/// @param feePaid BNB amount paid with the request (in wei).
/// @param failureReason Machine-readable failure reason; only meaningful when status == FAILED.
struct VideoRequestView {
uint256 requestId;
address requester;
VideoRequestStatus status;
uint8 referenceType;
uint64 timestamp;
uint128 feePaid;
VideoFailureReason failureReason;
}
// ----------------------------------------------------------------
// Custom Errors
// ----------------------------------------------------------------
/// @notice Reverts when `generateVideo` is called with `msg.value` not exactly equal to
/// `videoGenerateFee()`.
/// @param sent Amount of BNB sent (in wei).
/// @param required The exact fee required (in wei).
error FlapAIProviderWrongVideoFee(uint256 sent, uint256 required);
/// @notice Reverts when `generateVideo` is called with an empty prompt string.
error FlapAIProviderEmptyVideoPrompt();
/// @notice Reverts when `generateVideo` is called by a requester who already has a video
/// request in the `PENDING` state.
/// @dev Enforces one in-flight video request per requester at a time. Session continuity
/// (seeding the next clip from the previous clip's last frame) only makes sense in
/// strict sequence — allowing multiple concurrent PENDING requests for the same
/// requester would let the backend fulfill them out of order and silently corrupt the
/// last-frame chain. The requester must wait for `pendingRequestId` to reach a terminal
/// state (FULFILLED or FAILED, via `submitGeneratedVideo` / `markVideoFailed`) before
/// submitting another `generateVideo` call.
/// @param requester The requester address that already has a request in flight.
/// @param pendingRequestId The still-PENDING request ID blocking the new request.
error FlapAIProviderVideoRequestAlreadyPending(address requester, uint256 pendingRequestId);
/// @notice Reverts when `submitGeneratedVideo` or `markVideoFailed` is called with a
/// `requestId` that was never created.
/// @param requestId The unknown video request ID.
error FlapAIProviderUnknownVideoRequest(uint256 requestId);
/// @notice Reverts when `submitGeneratedVideo` or `markVideoFailed` is called on a video
/// request that is not PENDING.
/// @param requestId The video request ID that is not in the PENDING state.
error FlapAIProviderVideoRequestNotPending(uint256 requestId);
/// @notice Reverts when `submitGeneratedVideo` is called with an empty `videoCid` or
/// `lastFrameCid`.
error FlapAIProviderEmptyVideoCid();
/// @notice Reverts when `markVideoFailed` is called with `VideoFailureReason.NONE`.
/// @dev `NONE` is reserved to mean "not failed" (the default value for a PENDING or
/// FULFILLED request); a FAILED request must always carry a concrete reason so
/// `onVideoFailed` implementations always receive actionable information.
error FlapAIProviderInvalidVideoFailureReason();
/// @notice Thrown when `generateVideo(prompt, referenceType, cids)` is called with a reference
/// type that is unknown or currently disabled, or when an admin tries to enable an
/// unknown type.
error FlapAIProviderUnsupportedReferenceType(uint8 referenceType);
/// @notice Thrown when the number of reference CIDs is outside the bounds for the type
/// (or above `MAX_VIDEO_REFERENCE_CIDS`).
error FlapAIProviderBadReferenceCidCount(uint8 referenceType, uint256 count);
/// @notice Thrown when reference CID `index` is empty or longer than `MAX_VIDEO_REFERENCE_CID_LENGTH`.
error FlapAIProviderInvalidReferenceCid(uint256 index);
/// @notice Thrown when `VIDEO_REF_PREV_LAST_FRAME` is requested but the requester has no clip yet.
error FlapAIProviderNoPreviousClip(address requester);
// ----------------------------------------------------------------
// Events
// ----------------------------------------------------------------
/// @notice Emitted when a new on-chain video-generation request is submitted.
/// @dev The off-chain Rust worker (rust-video-service) watches this event to know when to
/// start generation. `lastFrameCid` is pre-resolved here (from the requester's most
/// recent fulfilled clip, when a session already exists) so the worker does not need an
/// extra `eth_call` on the happy path.
/// @param requestId Unique identifier for this video request.
/// @param requester Address that called `generateVideo` (EOA or contract, e.g. a vault).
/// @param prompt The text prompt for the video model.
/// @param referenceType One of the `VIDEO_REF_*` constants.
/// @param referenceCids Resolved reference-image IPFS CIDs: for `VIDEO_REF_PREV_LAST_FRAME` the
/// previous clip's last frame (resolved on-chain); for caller-supplied
/// types the caller's CIDs in order; empty for `VIDEO_REF_NONE`.
/// @param feePaid BNB amount paid with the request (full msg.value).
event FlapAIProviderVideoRequested(
uint256 indexed requestId,
address indexed requester,
string prompt,
uint8 referenceType,
string[] referenceCids,
uint256 feePaid
);
/// @notice Emitted when the backend fulfills a video request and appends the clip to the
/// requester's session.
/// @param requestId The fulfilled video request ID.
/// @param requester Address whose session the clip was appended to.
/// @param videoCid IPFS CID of the MPEG-TS clip.
/// @param lastFrameCid IPFS CID of the clip's last frame.
/// @param durationMs Clip duration in milliseconds.
/// @param startPtsMs The clip's starting PTS on the requester's continuous session timeline;
/// see {IFlapAIProvider-VideoClip}.
/// @param sessionIndex Index of the newly appended clip in the requester's session.
event FlapAIProviderVideoFulfilled(
uint256 indexed requestId,
address indexed requester,
string videoCid,
string lastFrameCid,
uint32 durationMs,
uint64 startPtsMs,
uint256 sessionIndex
);
/// @notice Emitted when the backend cannot produce a video for a pending request.
/// @dev Terminal — there is no refund; the requester paid for a generation attempt. No clip
/// is added to the requester's session. The backend only reaches this after exhausting
/// retries for transient issues — see `VideoFailureReason` for the machine-readable
/// category and `failureDetail` for a free-form human-readable detail string (e.g. the
/// upstream provider's raw error message).
/// @param requestId The failed video request ID.
/// @param requester Address that submitted the request.
/// @param reason Machine-readable failure category.
/// @param failureDetail Human-readable failure detail from the backend.
event FlapAIProviderVideoFailed(
uint256 indexed requestId, address indexed requester, VideoFailureReason reason, string failureDetail
);
/// @notice Emitted after a video request is settled (fulfilled or failed), reporting whether
/// the best-effort `IFlapVideoReceiver.onVideoRequestSettled` callback succeeded.
/// @dev EOA requesters (no code) always report `success = false` here — the callback is
/// skipped entirely for EOAs; the on-chain status write (FULFILLED or FAILED) is the
/// source of truth regardless of callback outcome.
/// @param requestId The settled video request ID.
/// @param requester Address that was (or would have been) called back.
/// @param success True if the requester's `onVideoRequestSettled` callback executed without reverting.
event FlapAIProviderVideoCallbackAttempted(uint256 indexed requestId, address indexed requester, bool success);
// ----------------------------------------------------------------
// Requester-Facing Functions
// ----------------------------------------------------------------
/// @notice Submit an on-chain video-generation request.
/// @dev Any EOA or contract (including a Flap vault) may call this and pays exactly the
/// current `videoGenerateFee()`. Fees accrue in the contract; only the admin can
/// withdraw them. No media is ever stored on-chain — only metadata (prompt, CIDs,
/// duration, timestamps).
/// Reverts with `FlapAIProviderVideoRequestAlreadyPending` if the caller already has a
/// request in the `PENDING` state — only one in-flight video request per requester at a
/// time, so the backend can never fulfill two of the same requester's requests out of
/// order and corrupt the last-frame continuity chain.
/// Continuity is automatic, not opt-in: every request after the requester's first is
/// always seeded from the previous clip's last frame; only the requester's very first
/// request in a session is exclusively text-to-video (there is no prior frame to seed
/// from yet). Emits {FlapAIProviderVideoRequested}.
/// @param prompt Text prompt describing the desired video clip.
/// @return requestId Unique ID for this video request.
function generateVideo(string calldata prompt) external payable returns (uint256 requestId);
// Reference types (uint8 codes; 5..255 reserved). Frames and soft references are mutually
// exclusive per request — the backend's video model ignores references when a frame is set.
// VIDEO_REF_NONE = 0 text-to-video
// VIDEO_REF_PREV_LAST_FRAME = 1 previous clip's last frame as the exact opening frame
// VIDEO_REF_FIRST_FRAME = 2 caller image as the exact opening frame (1 CID)
// VIDEO_REF_FIRST_AND_LAST_FRAME = 3 caller first + last frames (2 CIDs)
// VIDEO_REF_CHARACTER = 4 caller character/scene references, not pinned (1..3 CIDs)
// MAX_VIDEO_REFERENCE_CIDS = 3
/// @notice Submit a video request with an explicit reference type and caller-supplied
/// reference-image CIDs. Same flat fee and same one-pending-per-requester rule as
/// the 1-argument overload (which keeps its automatic NONE / PREV_LAST_FRAME behaviour).
/// @dev Reverts with `FlapAIProviderUnsupportedReferenceType` (unknown/disabled type),
/// `FlapAIProviderBadReferenceCidCount`, `FlapAIProviderInvalidReferenceCid`, or
/// `FlapAIProviderNoPreviousClip` (PREV_LAST_FRAME with an empty session).
/// Tip: to keep a character AND stay roughly continuous, pass
/// `[characterCid, getLastVideoFrameCid(address(this))]` as `VIDEO_REF_CHARACTER`.
function generateVideo(string calldata prompt, uint8 referenceType, string[] calldata referenceCids)
external
payable
returns (uint256 requestId);
/// @notice Returns the reference type and resolved reference CIDs of a video request.
function getVideoReferenceCids(uint256 requestId)
external
view
returns (uint8 referenceType, string[] memory referenceCids);
/// @notice Whether `referenceType` is currently enabled for the 3-argument `generateVideo`.
function isVideoReferenceTypeEnabled(uint8 referenceType) external view returns (bool);
/// @notice Returns the exact BNB fee (in wei) required by `generateVideo`.
function videoGenerateFee() external view returns (uint256);
/// @notice Returns the full VideoRequest struct for a given video request ID.
function getVideoRequest(uint256 requestId) external view returns (VideoRequestView memory view_);
/// @notice Returns the number of fulfilled clips in `user`'s video session.
function getVideoSessionLength(address user) external view returns (uint256);
/// @notice Returns a single fulfilled clip from `user`'s video session.
function getVideoClip(address user, uint256 index) external view returns (VideoClip memory clip);
/// @notice Returns up to `count` clips from `user`'s video session starting at `start`,
/// in chronological (append) order. Clamps to available entries; never reverts
/// out-of-bounds. Returns an empty array if `start >= session length`.
function getVideoSessionSlice(address user, uint256 start, uint256 count)
external
view
returns (VideoClip[] memory clips);
/// @notice Returns the IPFS CID of `user`'s most recent fulfilled clip's last frame, or ""
/// if the session is empty. Used internally by `generateVideo` for continuity, and
/// exposed here for off-chain convenience.
function getLastVideoFrameCid(address user) external view returns (string memory);
/// @notice Returns the request ID of `user`'s most recently submitted video request, or 0
/// if `user` has never called `generateVideo`.
/// @dev Used internally by `generateVideo` to enforce
/// `FlapAIProviderVideoRequestAlreadyPending`, and exposed here so off-chain callers
/// (e.g. a UI or the `rust-video-service` worker) can check whether a given address is
/// currently blocked from submitting a new request without needing to know the ID.
function getLastVideoRequestId(address user) external view returns (uint256 requestId);
// ----------------------------------------------------------------
// Backend-Only Functions (restricted to FULFILLER_ROLE)
// ----------------------------------------------------------------
/// @notice Fulfill a pending video request with the generated clip's IPFS CIDs.
/// @dev Restricted to `FULFILLER_ROLE` (the trusted off-chain backend). Appends a
/// `VideoClip` to the requester's session (checks committed before any external call),
/// then best-effort calls back the requester via `IFlapVideoReceiver.onVideoRequestSettled`
/// if it is a contract, capped at `videoCallbackGasLimit()`. EOA requesters are skipped
/// entirely. Emits {FlapAIProviderVideoFulfilled} and {FlapAIProviderVideoCallbackAttempted}.
/// @param requestId The ID of the pending video request.
/// @param videoCid IPFS CID of the generated MPEG-TS clip (must be non-empty).
/// @param lastFrameCid IPFS CID of the clip's last frame (must be non-empty).
/// @param durationMs Clip duration in milliseconds.
/// @param startPtsMs The clip's starting PTS on the requester's continuous session timeline
/// (0 for the requester's first clip; otherwise the caller-supplied value
/// is trusted as-is — the contract does not recompute or validate it
/// against the previous clip, since only the trusted `FULFILLER_ROLE`
/// backend can call this). See {IFlapAIProvider-VideoClip}.
function submitGeneratedVideo(
uint256 requestId,
string calldata videoCid,
string calldata lastFrameCid,
uint32 durationMs,
uint64 startPtsMs
) external;
/// @notice Mark a pending video request as failed (terminal — the backend has given up after
/// exhausting retries for transient issues, or hit a non-retryable content/policy
/// refusal).
/// @dev Restricted to `FULFILLER_ROLE`. Terminal — no refund; no clip is added to the
/// requester's session. Reverts with `FlapAIProviderInvalidVideoFailureReason` if
/// `reason == VideoFailureReason.NONE` (a FAILED request must always carry a concrete
/// reason). Best-effort calls back the requester via
/// `IFlapVideoReceiver.onVideoRequestSettled` if it is a contract, capped at
/// `videoCallbackGasLimit()` — same EOA-skip / swallow-revert semantics as
/// `submitGeneratedVideo`'s callback, so a broken vault can never block this transition.
/// Emits {FlapAIProviderVideoFailed} and {FlapAIProviderVideoCallbackAttempted}.
/// @param requestId The ID of the pending video request to fail.
/// @param failureReason Machine-readable failure category (must not be `NONE`).
/// @param failureDetail Human-readable failure detail (e.g. the upstream error message).
function markVideoFailed(uint256 requestId, VideoFailureReason failureReason, string calldata failureDetail)
external;
}
/// @title IFlapVideoReceiver
/// @notice Optional post-fulfill hook for contract callers (e.g. a Flap vault) of
/// `FlapAIProvider.generateVideo`.
/// @dev EOA requesters never receive this callback (there is no code to call). Contract
/// requesters that implement this interface are called back with at most
/// `videoCallbackGasLimit()` gas from `submitGeneratedVideo`/`markVideoFailed`, strictly
/// after the clip has already been appended to their session (or the request marked
/// failed) — so a missing implementation, a revert, or an out-of-gas callback can never
/// block or roll back fulfillment. Implement this in a Flap vault (or any other contract)
/// to react to newly generated clips (e.g. to mirror the CIDs into vault-local storage or
/// trigger further on-chain logic).
interface IFlapVideoReceiver {
/// @notice Carries the outcome of a settled (FULFILLED or FAILED) video request, passed to
/// `onVideoRequestSettled` so implementers can discriminate on `status` and read
/// only the fields that apply to that outcome.
/// @dev On FULFILLED: `videoCid`/`lastFrameCid`/`durationMs` are populated, `failureReason`
/// is `VideoFailureReason.NONE`, and `failureDetail` is "".
/// On FAILED: `videoCid`/`lastFrameCid` are "" and `durationMs` is 0; `failureReason`
/// and `failureDetail` describe why.
/// `status` is never `NONE`/`PENDING` here — this struct only exists for terminal
/// outcomes.
/// @param requestId The settled video request ID.
/// @param status `VideoRequestStatus.FULFILLED` or `VideoRequestStatus.FAILED`.
/// @param videoCid IPFS CID of the generated MPEG-TS clip (FULFILLED only).
/// @param lastFrameCid IPFS CID of the clip's last frame (FULFILLED only).
/// @param durationMs Clip duration in milliseconds (FULFILLED only).
/// @param failureReason Machine-readable failure category (FAILED only).
/// @param failureDetail Human-readable failure detail (FAILED only).
struct VideoRequestResult {
uint256 requestId;
IFlapAIProvider.VideoRequestStatus status;
string videoCid;
string lastFrameCid;
uint32 durationMs;
IFlapAIProvider.VideoFailureReason failureReason;
string failureDetail;
}
/// @notice Called by FlapAIProvider exactly once when a video request reaches a terminal
/// state — either fulfilled (clip already appended to this contract's session) or
/// failed (terminal; no clip was, or ever will be, added for this request).
/// @dev A single callback discriminated by `result.status` rather than two separate methods,
/// so implementers write one `if (result.status == FULFILLED) ... else ...` branch
/// instead of maintaining two entry points. Revert, out-of-gas, or simply not
/// implementing this interface are all silently tolerated by the caller
/// (`FlapAIProvider.submitGeneratedVideo` / `markVideoFailed`) — see
/// {FlapAIProviderVideoCallbackAttempted} for the on-chain record of whether this call
/// succeeded. No ETH is forwarded with this call (there is no refund path for FAILED).
/// Implement this to let a vault react differently depending on outcome — e.g.
/// distinguish a `CONTENT_POLICY` refusal (retrying the same prompt would fail again)
/// from a `PROVIDER_ERROR`/`TIMEOUT` (a fresh `generateVideo` call might succeed).
/// @param result The settled request's outcome; see `VideoRequestResult`.
function onVideoRequestSettled(VideoRequestResult calldata result) external;
}