liveness_flutter 0.5.0
liveness_flutter: ^0.5.0 copied to clipboard
Beta: Plug-and-play liveness detection for Flutter. Configure action sequences like blink and smile, capture images or video, and send verification results to your backend.
๐ก๏ธ liveness_flutter #
Face liveness checks for Flutter โ free, on-device, and built for your backend.
Blink. Smile. Turn left. Verified. โ
You give it a list of actions (blink, smile, turn leftโฆ). It shows the camera, guides the user through each action, and checks a real live person performed them. When it's done you get one result object โ optionally with photos and/or video โ and you send it wherever you want.
Everything runs on the phone. No cloud service. No license fees. No model downloads. No account.
๐งช Status: beta #
liveness_flutter is in beta. It's tested on real Android and iOS
devices, but it's still evolving and the API may change before 1.0. Until
then, a minor release (0.5 โ 0.6) can include breaking changes; each one
is listed under Breaking in the CHANGELOG. Depend on
^0.5.0 and you'll only get compatible updates.
Anyone can open an issue
โ bugs, a phone where detection misbehaves, confusing docs, or ideas. For
detection problems, include your phone model, OS version, and what
showDebugOverlay: true shows.
โจ Why this package? #
| ๐ฏ 13 challenge actions | blink, smile, fullTeethSmile, nod, look left/right/up/down, tilt left/right, eyes closed, open mouth, draw-a-circle-with-your-nose |
| ๐ฒ Anti-replay | random action order (and optionally a random pick of actions) per session, so a pre-recorded video can't follow the script |
| ๐ต๏ธ Anti-spoof, zero ML | static-feed guard (catches frozen or injected camera feeds), micro-motion analysis, frame-quality gates, opt-in color-flash challenge, plus hooks for server challenges, attestation and your own anti-spoof model |
| ๐ธ Evidence capture | a photo at the peak of each action, full video, or a works-everywhere frame sequence โ your server verifies, not just the phone |
| ๐ Any backend | onResult hands you everything; built-in multipart uploader with progress, errors and retries, or bring dio/S3/Firebase/anything |
| ๐จ Fully yours | theme every color and string (localizable), or replace whole UI layers with your own widgets โ see the branded fintech example |
| ๐งโ๐คโ๐ง Assisted mode | agent points the back camera at the customer โ torch lighting, auto-flipped left/right |
| ๐ชถ Featherlight | pure Dart + Google ML Kit. No TensorFlow, no 20 MB downloads, minSdk 24 |
๐บ๏ธ Pick your path #
| You want toโฆ | Go to |
|---|---|
| Add a working liveness check to your app | Simple setup โ about 10 minutes |
| See every example, or run one | Examples |
| Make it look like your app (colours, text, your own screens) | Make it look like your app and the fintech example |
| Use it for KYC or anything with real consequences | Bind sessions to your server |
| Have an agent verify someone else | Assisted mode |
| Know the limits before shipping | Honest notes |
๐ Simple setup #
Four steps to a working check. Everything here uses defaults; the advanced sections cover the rest.
1. Install and set up the platforms #
dependencies:
liveness_flutter: ^0.5.0
Needs Flutter 3.38+ (Dart 3.10+), the minimum for package:camera
0.12.
Android โ set minSdkVersion 24 in your app.
iOS โ add to ios/Runner/Info.plist (without it the app crashes on
camera use):
<key>NSCameraUsageDescription</key>
<string>Camera is used for liveness verification.</string>
Also set platform :ios, '15.5' in ios/Podfile (the face detection
library needs iOS 15.5+).
You don't need a permissions package: the camera plugin asks for access
the first time. If the user says no, the session ends with
failureReason == LivenessFailureReason.permissionDenied.
2. Show the liveness screen #
import 'package:liveness_flutter/liveness_flutter.dart';
class LivenessPage extends StatelessWidget {
const LivenessPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: LivenessDetector(
config: const LivenessConfig(
actions: [
LivenessAction.blink,
LivenessAction.smile,
LivenessAction.lookLeft,
],
shuffleActions: true, // a new order every time
capture: {CaptureType.images}, // photos your server can check
),
onResult: (result) {
// If the user pressed back, this screen is gone or animating
// out, and popping would close the *previous* screen. Check first.
if (!context.mounted) return;
if (ModalRoute.of(context)?.isCurrent ?? false) {
Navigator.pop(context, result);
}
},
),
);
}
}
// Somewhere in your app:
final result = await Navigator.push<LivenessResult>(
context,
MaterialPageRoute(builder: (_) => const LivenessPage()),
);
โ ๏ธ
onResultis called exactly once per session โ even if the screen is closed before the check finishes (system back, route popped). In that case the result iscancelledwithmetadata['cancelledBy'] == 'dispose'and arrives just after the screen is gone, which is why the guard above matters.cancelledByis'user'for the close button and'lifecycle'when the app goes to the background.
๐ก Why shuffle? If actions always come in the same order, someone could record a video of a person doing that exact sequence and play it to the camera. Random order means yesterday's recording won't match today's.
๐ก๏ธ Include at least one motion action (
blink,nod,openMouth,drawCircleWithNose). Pose-only actions (smile,tiltLeft/tiltRight,lookUp/lookDown) can be faked with a photo tilted or swapped at the right moment.
3. Read the result #
The essentials of LivenessResult:
successโ did the person complete all actions in time?failureReasonโ why not (took too long, face left the screen, more than one face, static/injected input suspected, camera access denied, user cancelledโฆ)confidenceScoreโ 0 to 1. A clean run on a real camera scores 0.9+.imagesโ the photos, each labelled with its action andkind(reference, orpeak: the moment the action was clearest, e.g. eyes shut for a blink)sessionIdโ a unique audit ID (e.g.LV-018F3A2B9C4E-D7E31F08)
debugPrint(result.toString()) prints a readable summary, and
result.toJson() gives a JSON-safe one (no image bytes). The
full list of fields is further down.
4. Send it to your server #
try {
await HttpLivenessUploader(
endpoint: Uri.parse('https://api.example.com/liveness'),
headers: {'Authorization': 'Bearer โฆ'},
onProgress: (sent, total) => progress.value = sent / total,
).upload(result);
} on LivenessUploadException catch (e) {
// Your server answered with an error (e.statusCode, e.body).
}
It sends a metadata field with result.toJson() (session ID,
confidence, actions, a SHA-256 of every photoโฆ), one file per photo named
like blink_peak_812ms.jpg, and an X-Liveness-Session header. Any
transport works instead: LivenessUploader.custom((result) async { ... })
wraps dio, S3, Firebase or anything else.
โณ Show a progress bar. With photos an upload can be several MB. A common pattern: close the camera screen straight away and upload from the previous screen with a
LinearProgressIndicatordriven byonProgress.
That's it โ you have a working liveness check. The complete runnable
version of these steps is example/lib/main.dart.
Before using it for anything with real consequences, read
Bind sessions to your server
and the honest notes.
๐ Examples #
Each file in example/ runs on its own on a physical
device (simulators have no usable front camera):
| Example | Shows | Run from example/ |
|---|---|---|
| Minimal | The simple setup above, in ~90 lines | flutter run |
| Branded fintech KYC flow | A complete, fully custom-designed verification flow (more below) | flutter run -t lib/recipes/fintech/main.dart |
| Custom UI | Your own overlay and instructions, with detection following your window | flutter run -t lib/recipes/custom_ui.dart |
| Controller | Your own Cancel / Try again buttons and live state | flutter run -t lib/recipes/controller.dart |
| Server-bound | A challenge from your backend, then upload with error handling | flutter run -t lib/recipes/server_bound.dart --dart-define=BACKEND_URL=https://โฆ |
| Test bench | Every option behind a settings screen, for trying the package on a device | flutter run -t lib/test_bench/main.dart |
Clone the repository to get the example with its Android and iOS folders
ready: git clone https://github.com/maykhid/liveness_flutter && cd liveness_flutter/example. (The copy on pub.dev leaves the platform
folders out; the example README explains how to
generate them.)
๐ฆ Example: a fully branded KYC flow #
example/lib/recipes/fintech/
shows how far customisation goes: a complete "upgrade your account"
verification flow, the way a mobile bank or fintech app would ship it,
built for a fictional brand ("AcmePay"). None of the package's default UI
is visible โ only its detection.
- Account home โ balance card, current tier, "Upgrade to Tier 2".
- Intro โ where the user is in the upgrade (phone โ, BVN โ, face check), tips for passing first time, and what the photos are used for.
- Face check โ a light screen with a circular window, a progress ring
split into one segment per action, and a card with an icon, the current
step, a hint that turns red when something needs fixing, and a
countdown. The circle is passed as
targetRegion, so the face must be inside what's drawn. A branded page handles denied camera access. - Verifying โ success or failure โ success unlocks Tier 2; failure
explains what went wrong for each
LivenessFailureReasonand offers Try again.
Run it:
cd example
flutter run -t lib/recipes/fintech/main.dart
Make it yours: everything brand-specific โ name, colours, wording,
which actions it asks for โ is in
brand.dart. Change primary
to your colour and hot reload. The "Verifying" step simulates your
backend's approval: replace _serverApproves in outcome_screens.dart
with your upload and your server's decision.
๐งฐ Advanced #
Independent topics โ read the ones you need.
๐จ Make it look like your app #
You need this if the default dark oval and white text don't match your design.
Theme and text (no custom widgets): LivenessTheme sets colours,
borders, text styles, the target's size, position and shape (oval, circle
or rounded rectangle โ this is also where the face must be), the close
button, and every piece of text through LivenessStrings:
LivenessDetector(
theme: LivenessTheme(
ovalShape: TargetShape.circle,
progressColor: Colors.teal,
strings: LivenessStrings(
actionInstructions: {LivenessAction.blink: 'Clignez des yeux'},
stepCounter: (current, total) => 'รtape $current sur $total',
),
),
...
)
Text maps (actionInstructions, guidanceMessages, failureMessages)
are merged over the English defaults, so you can override just a few
entries. Both LivenessTheme and LivenessStrings have copyWith. For
layout: instructionAlignment, closeButtonBuilder, and theme fields
like closeIconColor and resultHoldDuration.
Your own widgets: overlayBuilder replaces the dimmed overlay and
instructionBuilder the instruction area. Both receive the live
LivenessSessionState and rebuild on every change. Useful fields:
| Field | What it is |
|---|---|
phase |
searching, centering, performing an action, completed, failedโฆ |
currentAction, actionPlan |
the action now, and every action in this session's order (known from the first frame) |
actionProgress, overallProgress |
0โ1 for the current action and for the whole session |
remaining, actionTimeout, sessionRemaining |
countdowns |
faceInPosition, guidance |
whether the face is in place, and what's wrong if not (tooFar, lowLightโฆ) |
failureReason |
why it failed |
If you draw your own window, tell detection where it is with
targetRegion (normalised to the widget, e.g.
Rect.fromLTWH(0.15, 0.2, 0.7, 0.5)); otherwise the theme's oval still
decides where the face must be. Working code:
custom UI recipe and the
fintech example.
DetectorTuning sets how strict each action is (how big a smile counts,
how far to turn, how long to holdโฆ). Tested defaults, all adjustable.
๐ฎ Control it from outside: LivenessController #
You need this if you hide the built-in close button, want a Try again without leaving the screen, or need the state outside the widget.
final controller = LivenessController(); // create in initState, dispose in dispose
LivenessDetector(
controller: controller,
showCloseButton: false,
config: ...,
onResult: ...,
);
controller.cancel(); // onResult gets a cancelled result (cancelledBy: 'user')
await controller.restart(); // fresh session: new sessionId, new shuffle
controller.state; // live LivenessSessionState (it's a ChangeNotifier)
controller.actionPlan; // the actions in the order this session runs them
Every session still gets exactly one onResult: restarting a session
that's still running delivers it as cancelled with
cancelledBy: 'restart'. See the
controller recipe.
๐ Bind sessions to your server (recommended for KYC) #
You need this if a passing result unlocks anything valuable. On its own, the phone's verdict is unsigned: your server can't tell which actions it expected or whether the result was edited. Three hooks fix that:
LivenessConfig.challenge: your server issues aLivenessChallenge(single-use nonce, the actions in its chosen order, an expiry). The session runs exactly that order and echoes the nonce inresult.nonce. An expired challenge fails withchallengeExpired.- Image hashes:
result.toJson()(and so the uploader'smetadata) lists the SHA-256 of every photo and frame, so the server can prove the files weren't swapped. LivenessConfig.attestor: plug in Play Integrity or App Attest through theLivenessAttestorinterface. It signs a hash ofsessionId|nonce|success|actions|image hashes; the token lands inresult.attestation. The package doesn't implement the platform APIs.
Without a server challenge, randomActionCount: 3 makes each session
pick 3 actions at random from your list and shuffle them โ from all 13
actions that's 1,716 possible sequences instead of 6.
Server verification guide โ covers what your backend should check, with a payload-rebuild snippet. Working client code: the server-bound recipe.
Bring your own anti-spoof model (optional). The package bundles no ML
model, but LivenessConfig.frameAnalyzers lets you plug one in (a
TFLite/ONNX presentation-attack detector, or a cloud call):
class MyPadModel extends LivenessFrameAnalyzer {
@override
String get id => 'pad-v2';
@override
Future<double?> analyze(LivenessFrame frame) async {
// frame.jpeg: the upright full frame; frame.faceBox: the face (0..1).
return myModel.spoofProbability(frame.jpeg, frame.faceBox); // 0โ1
}
}
It runs on the reference frame and on each action's evidence frame. Scores
land in metadata['analyzers']['pad-v2'] and lower confidenceScore by
analyzerWeight (default 0.5) ร the mean spoof probability.
Uploads, in more detail: a non-2xx answer throws
LivenessUploadException(statusCode, body); each attempt is limited by
timeout (default 60 s); set maxRetries to retry network errors,
timeouts and 5xx with exponential backoff (4xx is never retried).
๐ธ Photos, video, and "frame sequence" โ which do I pick? #
You need this if you want more evidence than one photo per action.
Capturing nothing is the default. With an empty capture,
result.images and result.frameSequence come back empty and
videoPath is null. Nothing is encoded, kept in memory, or written to
disk. You still get the verdict: success, completedActions,
confidenceScore, sessionId, and metadata โ a few hundred bytes.
The trade-off: with no captured media, your server has nothing to
independently verify โ you're fully trusting the on-device result. Fine
for low-stakes flows (gating a selfie upload); for KYC or anything with
real consequences, capture at least {CaptureType.images}.
| You want | Use | Notes |
|---|---|---|
| A photo of each completed action | CaptureType.images |
Smallest uploads. Works everywhere. Best default. |
| A real video file of the session | CaptureType.video |
Great on iPhones. On many Android phones the camera can't record and detect at the same time โ see below. |
| Something video-like that works on every phone | CaptureType.frameSequence |
Several photos per second for the whole session. A list of photos, not a video file โ but played back they look like one. |
๐ค The Android video problem, in plain words (click to expand)
Detecting your face and recording a video both need the camera at the same time. iPhones handle that fine. Many Android phones can't โ and there's no official way to ask a phone in advance. So this package gives you three tools:
-
LivenessCapabilities.supportsVideoCapture()โ call it once when your app starts (takes 2โ3 seconds, remembers the answer). It quietly tries recording and tells youtrue/false:final canRecord = await LivenessCapabilities.supportsVideoCapture(); final capture = canRecord ? {CaptureType.images, CaptureType.video} : {CaptureType.images, CaptureType.frameSequence}; -
If you skip that and video fails mid-session anyway, the check doesn't break โ it quietly continues without video and marks
result.metadata['videoUnavailable'] = trueso you know. -
Frame sequence as the works-everywhere alternative. If your server wants a real video file from those photos, one command turns them into an MP4:
ffmpeg -framerate 8 -pattern_type glob -i 'frame_*.jpg' \ -c:v libx264 -pix_fmt yuv420p session.mp4
Media size & cleanup:
- Photo size:
maxImageDimension(default 720 px longest side) andjpegQuality(default 85; lower = smaller files). - Video size:
cameraResolutionon theLivenessDetectorwidget. - Frame rate:
frameSequenceFps(default 8, max 15) andframeSequenceMaxFrames(default 300) cap memory. Encoding runs in background isolates โ capture never stalls detection. - Cleanup: photos live only in memory โ gone when you're done with the
result. The video is a real file and is not deleted automatically:
upload or copy it in
onResult, then delete it yourself or setautoDeleteVideo: trueto remove it when the camera screen closes.
๐ Stop video replays: the color-flash challenge #
You need this if you worry about someone playing a video of a real person to the camera โ the hardest cheap attack on any action-based check, because the actions in the video were real when recorded.
enableFlashChallenge: true adds a defense: right after the actions
succeed, the screen flashes a short color sequence (red/green/blue, in a
random order each session, ~2.5 seconds, "Hold stillโฆ"). A real face
is lit by the phone's screen, so the camera sees each color reflected on
the skin. A replayed video was recorded before this session's random order
existed โ its "face" doesn't reflect the right colors at the right times.
LivenessConfig(
actions: [...],
shuffleActions: true,
enableFlashChallenge: true,
)
It's a soft signal, on purpose โ and lighting is why. The trick depends on the phone screen being a meaningful light source on the face:
| Environment | What to expect |
|---|---|
| ๐ Dim / evening indoor | Strong signal โ reflections clearly measurable |
| ๐ Normal indoor lighting | Good signal โ reliable for most users |
| ๐ข Bright office / large windows | Weak โ real faces may score 'inconclusive' or 'failed' |
| โ๏ธ Outdoors in daylight | Little to no signal โ results not meaningful |
How it decides. Only pixels inside the detected face are sampled
(background doesn't reflect the screen), the first 150 ms of each colour
is skipped while the display and camera catch up, and auto-exposure is
locked for the duration where the device allows it. Each colour must
raise its own channel clearly above the frame-to-frame noise measured
before the flash, both against that baseline and against the other
colours. By default all three colours must pass
(flashAllowedMisses: 0). In simulation, pure camera noise passes 0 times
in 5,000 runs, where the pre-0.5 rule passed about 1 in 20. White balance
can't be locked through the camera plugin, so strongly tinted light can
still skew it.
Other weakeners: phone held far from the face, very low screen brightness,
strongly colored ambient light. To help, the package raises the screen
to full brightness automatically during the session and restores it
afterward (app window only; opt out with boostScreenBrightness: false).
A failed challenge lowers confidenceScore by 0.35 and sets
metadata['flashChallenge'] = 'failed' โ it never rejects the user by
itself. Treat a failure as "review this one", not "this is fraud."
Log the metadata for a few weeks and learn your real users' pass rate
before enforcing anything. Bonus: the flash moment is captured in your
video and frame sequence โ a real face visibly changes color, which your
server can check too.
๐งโ๐คโ๐ง Assisted mode: verifying someone else #
You need this if a bank agent or field officer holds the phone and verifies another person โ common in branch onboarding and doorstep KYC:
LivenessConfig(
actions: [...],
cameraMode: LivenessCameraMode.assisted,
)
Understand what assisted mode means before using it:
- The back camera is used, pointed at the subject. The operator watches the screen and must read each instruction out loud ("please blink", "turn your head left") โ the subject cannot see the screen.
- "Left" and "right" always mean the subject's left/right; detection signs are flipped automatically for the unmirrored back camera.
- The device torch turns on to light the subject's face (the screen,
which normally does that job, faces the operator). Opt out with
assistedTorchEnabled: false. Skipped on devices without a torch. - The color-flash challenge is automatically skipped (the screen's
colors can't reach the subject's face):
metadata['flashChallenge'] = 'skippedAssistedMode'. Static-feed guard, micro-motion, and quality gates still run. metadata['cameraMode']tells your backend which mode was used โ decide whether assisted sessions need extra review, since the operator (not the subject) controls the device.
๐ฆ Everything in the result #
LivenessResult, in full:
success,failureReasonโ the verdict and why. Cancelled results say who inmetadata['cancelledBy'].confidenceScoreโ 0 to 1. Duplicate frames, a frozen head, many bad-quality frames, a failed flash challenge or anti-spoof model scores pull it down. Raw counters are inmetadataunderconfidence_*andidentity_*keys.sessionIdโ unique audit ID.nonce/attestationโ the server challenge's nonce and the attestor's token, when you use them.completedActionsโ which actions, in the order performed.imagesโ the photos, each withaction,kind,timestampMsand asha256Hex.frameSequenceโ the steady-stream photos, each with a timestamp.videoPathโ where the video file is, if you recorded one.metadataโ extras like how long each action took.toJson()/toString()โ a JSON-safe summary (no media bytes) and a readable log block.
๐ง Developer tools #
- Debug overlay โ
showDebugOverlay: trueshows live head angles, eye/smile probabilities, brightness, and static-feed guard counters on screen, and draws a green box where the detector thinks your face is (if it doesn't sit on your face, please open an issue with your device). Perfect for tuningDetectorTuningthresholds on real devices. - Per-action callbacks โ
onActionStarted/onActionCompleted(sync or async; never awaited, so detection never stalls on your code). - Feedback & accessibility โ
onFeedbackfires on action started, half-way, completed, and session passed/failed, for your own sounds, haptics or text-to-speech.hapticFeedback: trueadds built-in haptics (handy foreyesClosed, which users can't see finish). Instructions are a screen-reader live region, so each new one is announced. - Permission handling in the screen โ
permissionDeniedBuilder: (context, retry) => ...shows your own page when camera access is denied; callingretrystarts a fresh session. - Test bench โ
example/lib/test_bench/puts every option behind a settings screen, logs every callback live, and runs the server-side checks on each result.
๐ Honest notes โ read before shipping #
Plain-language notes on the rough edges. Every one has a setting you can change; nothing requires forking the package.
This is not bank-grade security on its own. All checking happens on the phone, and a determined attacker controls their own phone. Treat a passing result as a good first gate, and have your server double-check the photos/video you upload (compare against an ID photo, look for signs of screens or prints). That's exactly why this package captures media during the actions. Server challenges, image hashes and attestation make tampering detectable, but the face itself still has to be judged server-side โ see the server verification guide.
Memory adds up if you turn everything up. Photos are kept in memory
until the result is delivered. Defaults use roughly 15โ35 MB per session.
Raising the frame rate and photo size together can OOM cheap Android
phones. frameSequenceMaxFrames caps the total as a safety net.
Every face and phone is a little different. How confidently the phone
detects a smile or head turn varies with the camera, lighting, glasses,
and the person. If one action fails too often for your users, loosen its
setting in DetectorTuning. Most sensitive: fullTeethSmile (depends on
lip-contour quality) and eyesClosed (brief false "eye open" flickers are
ignored; eyesOpenTolerance controls how brief).
drawCircleWithNose is a fun extra, not a workhorse. People must move
their whole head in a circle, and small/slow circles don't count. Expect
more retries. Test on your users' actual phones before making it
mandatory.
Left and right, up and down. lookLeft means the user's left.
Calibrated for Android and iPhone; if some device gets it backwards,
mirrorYaw: false flips it. invertPitch: true does the same for
lookUp, lookDown and nod (the up/down sign hasn't been confirmed on
every iPhone yet; showDebugOverlay shows the live pitch).
Give people time โ but not forever. Each action has a 15-second limit
(actionTimeout), users get 10 seconds to return to a neutral face
between actions (neutralTimeout), and the whole session ends after 2
minutes (sessionTimeout, null to disable). For users who find the
actions difficult, use fewer/easier actions (blink, smile), longer
timeouts, and requireNeutralBetweenActions: false. A second face in the
background is ignored if it's small, and only fails the session if it
stays for more than half a second (multipleFacesGrace).
It needs light โ but it tells the user. Too-dark, overexposed, and
blurry frames pause the session with a hint like "Find better lighting".
The action's timer keeps running while paused, so a room that stays too
dark ends in actionTimeout rather than hanging. Thresholds:
brightnessMin, brightnessMax, sharpnessMin; disable with
enableQualityChecks: false.
Same person throughout? With ML Kit tracking (on unless an action
needs contours), a face ID that changes while a face stays in view lowers
the confidence score and shows up in metadata['identity_*'], along with
jumps in rough face geometry. Set failOnFaceChange: true to fail such
sessions outright. It's off by default because a very fast head turn can
make some devices re-assign the ID. This is no substitute for server-side
face matching.
The anti-spoof checks are honest heuristics, not magic. The
static-feed guard (called the replay guard before 0.5; the setting is
still enableReplayGuard) catches input that can't come from a live
camera: pixel-identical frames fail the session, and near-identical frames
with a frozen face box (a re-encoded still injected as a camera) lower the
confidence score. Micro-motion flags unnaturally still sessions the same
way. None of it detects a photo, screen or video held up to a real
camera: the real camera adds real noise and the hand adds real motion.
That's what the actions, the colour flash, and server-side review of the
captured media are for. If the guard ever misfires on a device (it
shouldn't โ real sensors are noisy), enableReplayGuard: false turns it
off.
Found a bug? Have an idea? Open an issue โ anyone can, and PRs are welcome too. ๐
Made with โค๏ธ for developers who'd rather not pay per verification.