Use Bytes for binary data, Time for a point on the UTC timeline, and Duration for elapsed time. These values are immutable. Keeper includes the conversions and methods below, along with text methods and SHA-256.
Byte values
Write a hexadecimal byte literal with the b prefix:
let marker: Bytes = b"deadbeef"
let empty: Bytes = b""
Each pair of hexadecimal digits represents one byte. Both letter cases are accepted. The literal requires complete pairs, without a 0x prefix, spaces, or string escapes.
| Expression | Result |
|---|---|
payload.count() |
Number of bytes as Integer |
payload[0] |
First byte as an Integer from 0 through 255 |
payload.slice(start, end) |
Bytes from start, inclusive, to end, exclusive |
payload.contains(marker) |
Whether the contiguous byte sequence occurs |
payload.startsWith(marker) |
Whether the sequence is a prefix |
payload.endsWith(marker) |
Whether the sequence is a suffix |
payload + other |
Concatenated Bytes |
payload == other |
Equality of byte contents |
Indexes start at zero. A slice requires 0 <= start <= end <= payload.count(). Invalid indexes and slice bounds fail evaluation. An empty search sequence matches every byte value, including empty bytes.
Conversions are explicit:
Bytes.fromHex("deadbeef")
Bytes.fromBase64("3q2+7w==")
Bytes.fromUtf8("hello")
b"deadbeef".toHex()
b"deadbeef".toBase64()
b"68656c6c6f".toUtf8()
crypto.sha256(b"68656c6c6f")
toHex() returns lowercase digits without a prefix. Base64 uses the standard alphabet and canonical padding. toUtf8() rejects malformed UTF-8. Hash functions accept and return Bytes.
Text methods
Text supports count(), indexing, slice(start, end), contains(text), startsWith(text), and endsWith(text).
Counts, indexes, and slice bounds use Unicode code points. "a😀é".count() is 3, and "a😀é".slice(1, 2) is "😀". Combining marks remain separate code points. Comparisons and searches preserve case and Unicode spelling.
Searches match literal text. Regular expressions are unavailable.
Time and duration
Construct a time from an ISO timestamp with an explicit UTC offset, Unix seconds, or Unix milliseconds:
Time.parse("2026-11-01T10:00:00Z")
Time.seconds(0)
Time.millis(1500)
Time.seconds(value) and Time.millis(value) accept exact numbers, including fractions. Values must fit nanosecond precision and the supported time range. A value finer than one nanosecond fails. time.toSeconds() and time.toMillis() return exact Number values. time.toText() returns a UTC ISO timestamp.
Create a duration with Duration.millis(value), Duration.seconds(value), Duration.minutes(value), Duration.hours(value), or Duration.days(value). A day is exactly 86,400 seconds. Durations support toSeconds() and toMillis().
| Operation | Result |
|---|---|
time - earlier |
Duration |
time + duration |
Time |
time - duration |
Time |
duration + time |
Time |
duration + other |
Duration |
duration - other |
Duration |
-duration |
Negated Duration |
<, <=, >, >= |
Compare two times or two durations |
Time.now() reads the current time once per policy evaluation. Every call in one evaluation, including every action in a batch, reads the same value. It is unavailable in authority constants, defaults, and other compile-time expressions.
Calendar methods require a time zone:
Time.now().hour("Asia/Dubai")
Time.now().dayOfWeek("UTC")
Time.now().startOfDay("America/New_York")
hour(zone) returns an integer from 0 through 23. dayOfWeek(zone) returns an uppercase name such as "MONDAY". startOfDay(zone) returns the first valid time of the local date. Calendar boundaries follow the zone's daylight saving rules. Elapsed durations retain their fixed length.
Custom JSON inputs
A JSON Bytes field accepts a Base64 string. A Time field accepts an ISO timestamp with an explicit offset. A Duration field accepts an ISO duration such as "PT5M".
This policy permits a payload with the required prefix while its expiry is at most one hour away:
authority "payload-window" {
intent = "custom"
version = "1"
input request {
payload: Bytes
expiresAt: Time
}
policy "payload-window" {
let now = Time.now()
allow when {
request.payload.startsWith(b"deadbeef")
now < request.expiresAt
request.expiresAt - now <= Duration.hours(1)
}
}
}
The matching artifact.typed uses this JSON:
{"payload":"3q2+7w==","expiresAt":"2026-11-01T11:00:00Z"}
EVM bytes and bytesN, Solana bytes, and Soroban bytes arguments expose Bytes. EVM fixed byte arguments retain their declared length. X.509 validity fields expose optional Time values.
Protocol functions
Protocol functions are available when Keeper is built with the corresponding feature. See Build and features to select features.
| Keeper feature | Functions |
|---|---|
| Included in every build | Bytes conversions and methods, text methods, Time, Duration, crypto.sha256(bytes) |
evm |
evm.validAddress(text), evm.addressEquals(left, right), crypto.keccak256(bytes) |
bitcoin |
bitcoin.validAddress(text, network) |
agentic-payments |
payment.minorUnits(amount, currency), payment.currencyScale(currency), payment.matchesIdentity(actual, expected) |
EVM validation checks address syntax and mixed-case checksums. addressEquals validates both operands before comparing their addresses. Bitcoin validation checks the address checksum and the network argument, such as "mainnet" or "testnet".
Payment amounts use quoted decimal text and an ISO currency code. payment.minorUnits("50.00", "USD") returns the integer 5000. Excess fractional digits and unknown currencies fail. Identity matching checks the configured id and website, or the payment instrument's id and type. It compares every configured field.
Limits
Byte values, searches, conversions, and intermediate results share the policy's resource limits. Invalid encodings, unsupported time values, or exhausted limits fail evaluation. See Errors and execution limits.