block/buzz · error · IngestError::Rejected

ttl tag must have a value (seconds, or empty string to clear

Error message

ttl tag must have a value (seconds, or empty string to clear)

What it means

Thrown when a kind:9002 event carries a bare ["ttl"] tag with no value. The relay rejects this deliberately: clearing the TTL must be an explicit ["ttl", ""] so that a value-less tag can never be mistaken for 'make permanent'.

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:580

            // Validate ttl values before storage. Empty string clears the TTL
            // (channel becomes permanent); any other value must parse as a
            // positive integer number of seconds. A bare tag with no value is
            // rejected so clearing is always explicit (`["ttl", ""]`).
            for t in event.tags.iter() {
                if t.kind().to_string() == "ttl" {
                    match t.content() {
                        Some("") => {}
                        Some(v) => match v.parse::<i32>() {
                            Ok(n) if n > 0 => {}
                            _ => {
                                return Err(anyhow::anyhow!(
                                    "invalid ttl value: {v} (must be a positive integer of seconds, or empty to clear)"
                                ));
                            }
                        },
                        None => {
                            return Err(anyhow::anyhow!(
                                "ttl tag must have a value (seconds, or empty string to clear)"
                            ));
                        }
                    }
                }
            }

            // name/about/archived/visibility/ttl require owner/admin;
            // topic/purpose allow any member.
            let has_privileged_tag = event.tags.iter().any(|t| {
                let k = t.kind().to_string();
                k == "name" || k == "about" || k == "archived" || k == "visibility" || k == "ttl"
            });
            if has_privileged_tag {
                let members = state.db.get_members(tenant.community(), channel_id).await?;
                let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);
                match actor_member {
                    Some(m) if m.role == "owner" || m.role == "admin" => Ok(()),

View on GitHub (pinned to f956e6fe06)

Solutions

  1. To clear the TTL, send the explicit empty string: ["ttl", ""].
  2. To set a TTL, send seconds: ["ttl", "3600"].
  3. Fix any tag serializer that filters out empty strings; the empty value is meaningful here.
  4. If TTL should stay unchanged, omit the ttl tag entirely.

Example fix

// before — serializer dropped the empty value
{ kind: 9002, tags: [["h", ch], ["ttl"]] } // was meant to clear TTL

// after — explicit empty string survives serialization
const tags = [["h", ch], ["ttl", ""]]; // build literal pairs; do not filter empty values
Defensive patterns

Strategy: validation

Validate before calling

// The empty-string value is meaningful — never filter it out
function ttlTag(seconds: number | null): string[] {
  return ["ttl", seconds === null ? "" : String(seconds)];
}

Type guard

function isWellFormedTtlTag(t: string[]): boolean {
  // must have exactly the value slot; only "" or a positive-integer string is valid
  return t.length >= 2 && (t[1] === "" || /^\d+$/.test(t[1]));
}

Try / catch

try {
  await sdk.publish(editEvent);
} catch (e) {
  if (String(e).includes("ttl tag must have a value")) {
    throw new Error('Use ["ttl", ""] to clear or ["ttl", "3600"] to set — bare ["ttl"] is rejected');
  } else throw e;
}

Prevention

When it happens

Trigger: Publishing [["ttl"] as a single-element tag, or an encoding path that strips empty strings from tag values before send.

Common situations: Serialization layers that drop empty-string tag values (JSON→Nostr tag mappers); clients sending a null ttl hoping to unset it; generic tag builders that omit falsy values.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/e3912f80eb9fcaa1. Report an issue: GitHub.